diff --git a/README.md b/README.md index 5476cb83..caf8c047 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ Ready-to-adapt workflow examples: --escape-model= loads external noescape rules for stack pointer escape analysis (`noescape_arg`) --buffer-model= loads external buffer write rules for copy/string overflow checks (`bounded_write`/`unbounded_write`) --resource-model= loads external acquire/release rules for generic resource lifetime checks +A model path given to one of these three options that is not a readable file stops the run with an error (exit code 1). A model that can be read but holds malformed rules is reported on stderr, and the analysis runs without it. --resource-cross-tu enables cross-TU resource summaries for resource lifetime analysis (default: on) --no-resource-cross-tu disables cross-TU resource summaries --resource-summary-cache-dir= sets cache directory for cross-TU resource summaries (default: .cache/resource-lifetime) diff --git a/include/StackUsageAnalyzer.hpp b/include/StackUsageAnalyzer.hpp index 7c9ccbb4..27d99883 100644 --- a/include/StackUsageAnalyzer.hpp +++ b/include/StackUsageAnalyzer.hpp @@ -93,6 +93,8 @@ namespace ctrace::stack std::string smtSecondaryBackend; std::string smtBackend = "interval"; std::string dumpIRPath; + // The directory relative input paths resolve from; empty: the current one at load. + std::string inputBaseDir; std::string escapeModelPath; std::string bufferModelPath; std::string resourceModelPath; diff --git a/run_test.py b/run_test.py index 02c0eee9..538241e7 100755 --- a/run_test.py +++ b/run_test.py @@ -2073,6 +2073,160 @@ def check_unknown_alloca_virtual_callback_escape() -> bool: return True +def check_resource_model_across_compile_directories() -> bool: + """ + A resource model given by a relative path is loaded, and used, whatever the job count, while + the files compile from compile-command directories of their own. Each directory holds the one + header its file includes through a relative -I, so a file compiles only from its own + directory; the analyzer must come back to the directory it started from, where the model and + the relative cache directories are. Every run starts with empty caches, and a leak that only + the model defines must be reported in every file. The relative paths must be anchored to the + start directory. The start directory also holds a header of the same name for every file, + one that drops the leak: a file compiled from there instead of its own directory loses it. + The files are given by absolute paths, then by paths relative to the start directory, which + must resolve there whatever directory another file is compiling from at the time; the + results name each file as it was given. + """ + print("=== Testing the resource model across compile-command directories ===") + unit_count = 8 + ok = True + with tempfile.TemporaryDirectory(prefix="ct_resource_model_cwd_") as tmp: + root = Path(tmp) + project = root / "project" + entries = [] + for i in range(unit_count): + unit_dir = project / f"unit{i}" + (unit_dir / "include").mkdir(parents=True) + (unit_dir / "include" / f"unit{i}.h").write_text( + "void* ct_open_widget(void);\nvoid ct_close_widget(void* w);\n" + ) + source = unit_dir / f"unit{i}.c" + source.write_text( + f'#include "unit{i}.h"\n\n' + f"int leak_widget_{i}(void)\n{{\n void* w = ct_open_widget();\n" + " return w != 0;\n}\n\n" + f"int keep_widget_{i}(void)\n{{\n void* w = ct_open_widget();\n" + " ct_close_widget(w);\n return 0;\n}\n" + ) + entries.append( + { + "directory": str(unit_dir), + "file": str(source), + "arguments": ["cc", "-Iinclude", "-c", str(source), "-o", f"unit{i}.o"], + } + ) + compdb = project / "compile_commands.json" + compdb.write_text(json.dumps(entries, indent=2)) + run_dir = root / "run" + (run_dir / "include").mkdir(parents=True) + for i in range(unit_count): + (run_dir / "include" / f"unit{i}.h").write_text( + "#define ct_open_widget() ((void*)0)\nvoid ct_close_widget(void* w);\n" + ) + (run_dir / "widget-model.txt").write_text( + "acquire_ret ct_open_widget Widget\nrelease_arg ct_close_widget 0 Widget\n" + ) + sources = [entry["file"] for entry in entries] + expected = {f"leak_widget_{i}" for i in range(unit_count)} + + analyzer = str(Path(RUN_CONFIG.analyzer).resolve()) + + def run_from_start_dir(args): + try: + return subprocess.run( + [analyzer, *args], + cwd=run_dir, + capture_output=True, + text=True, + timeout=RUN_CONFIG.analyzer_timeout, + ) + except subprocess.TimeoutExpired: + return None + + start_dir = run_dir.resolve() + result = run_from_start_dir( + [ + "--print-effective-config", + f"--compile-commands={compdb}", + "--resource-model=widget-model.txt", + "--compile-ir-cache-dir=caches-config/compile-ir", + sources[0], + ] + ) + printed = (result.stdout + result.stderr) if result else "" + for line in ( + f"resource-model: {start_dir / 'widget-model.txt'}", + f"compile-ir-cache-dir: {start_dir / 'caches-config' / 'compile-ir'}", + ): + if line in printed: + print(f" ✅ effective configuration: {line.split(':')[0]} anchored to start") + else: + print(f" ❌ effective configuration lacks '{line}'") + ok = False + + relative_sources = [os.path.relpath(source, run_dir) for source in sources] + runs = [ + (paths, given, mode) + for paths, given in (("absolute paths", sources), ("relative paths", relative_sources)) + for mode in ([], ["--jobs=1"], ["--jobs=4"]) + ] + for paths, given, mode in runs: + label = f"{paths}, {mode[0] if mode else 'default jobs'}" + for attempt in range(1, 4): + caches = f"caches-{label.replace('=', '').replace(', ', '-').replace(' ', '-')}-{attempt}" + cmd = [ + analyzer, + "--format=json", + f"--compile-commands={compdb}", + "--resource-model=widget-model.txt", + f"--compile-ir-cache-dir={caches}/compile-ir", + f"--resource-summary-cache-dir={caches}/resource", + *mode, + *given, + ] + result = run_from_start_dir(cmd[1:]) + if result is None: + print(f" ❌ {label}, run {attempt}: timed out") + ok = False + continue + problems = [] + if "cannot open model file" in result.stderr: + problems.append("the model failed to load") + try: + diagnostics = json.loads(result.stdout or "").get("diagnostics", []) + except json.JSONDecodeError: + diagnostics = None + problems.append(f"no JSON output (exit {result.returncode})") + if diagnostics is not None: + found = { + d.get("location", {}).get("function") + for d in diagnostics + if d.get("ruleId") == "ResourceLifetime.MissingRelease" + } + if found != expected: + problems.append(f"leaks reported in {sorted(found)}") + files = { + d.get("location", {}).get("file") + for d in diagnostics + if d.get("ruleId") == "ResourceLifetime.MissingRelease" + } + if found == expected and files != set(given): + problems.append(f"leaks not named as given: {sorted(files)}") + for cache in ("compile-ir", "resource"): + if not (run_dir / caches / cache).is_dir(): + problems.append(f"no {cache} cache under the start directory") + strays = sorted(str(p.relative_to(root)) for p in project.rglob(caches)) + if strays: + problems.append(f"caches under the compile directories: {strays}") + if problems: + print(f" ❌ {label}, run {attempt}: {'; '.join(problems)}") + ok = False + else: + print(f" ✅ {label}, run {attempt}: model used in {unit_count} files") + print() + return ok + + def check_resource_lifetime_cross_tu() -> bool: """ Regression: cross-TU resource summaries must propagate acquire/release effects @@ -3363,6 +3517,120 @@ def check_escape_model_rejects_unsupported_brackets() -> bool: return True +def check_compile_ir_cache_rebuilds_previous_schema() -> bool: + """ + A file used to be compiled from the current directory, and from its compile command's only + when that failed: with a header of the same name in both, the compile IR cache kept the IR + built with the wrong one. Compiling from the command's directory changes the IR of an + unchanged command, so the cache schema changed. + + The schema is in two places: the cache key, and the metadata of each entry. An entry stored + under a previous key is never looked up again; this check covers the other one. It rewrites + the entry of the current key as the previous pipeline left it: the previous schema, and the + IR built with the start directory's header (7, where the command's header gives 42), with + the sources and dependencies left valid. The next run must compile again and give 42, and + rewrite the entry; the run after it must reuse that entry and give 42 again. + """ + print("=== Testing that compile IR cached under a previous schema is rebuilt ===") + ok = True + with tempfile.TemporaryDirectory(prefix="ct_compile_ir_schema_") as tmp: + root = Path(tmp) + (root / "start" / "include").mkdir(parents=True) + (root / "start" / "include" / "config.h").write_text("#define VALUE 7\n") + (root / "project" / "build" / "include").mkdir(parents=True) + (root / "project" / "build" / "include" / "config.h").write_text("#define VALUE 42\n") + source = root / "project" / "value.c" + source.write_text("#include \nint value(void) { return VALUE; }\n") + compdb = root / "project" / "compile_commands.json" + compdb.write_text(json.dumps([{ + "directory": str(root / "project" / "build"), + "file": str(source), + "arguments": ["cc", "-Iinclude", "-c", str(source)], + }])) + cache = root / "compile-ir" + analyzer = str(Path(RUN_CONFIG.analyzer).resolve()) + + def run(name): + dump = root / f"ir-{name}" + try: + result = subprocess.run( + [analyzer, "--timing", f"--compile-commands={compdb}", + "--compile-ir-format=ll", f"--compile-ir-cache-dir={cache}", + f"--dump-ir={dump}/", str(source)], + cwd=root / "start", capture_output=True, text=True, + timeout=RUN_CONFIG.analyzer_timeout, + ) + except subprocess.TimeoutExpired: + return None, False + returned = re.findall(r"ret i32 (\d+)", "".join(p.read_text() for p in dump.glob("*.ll"))) + return (returned[0] if returned else None), "Compilation cache hit" in result.stderr + + value, _ = run("empty") + entries = sorted(cache.glob("*.json")) + if value != "42" or len(entries) != 1: + print(f" ❌ with an empty cache: returns {value}, {len(entries)} cache entries") + return False + metadata = json.loads(entries[0].read_text()) + current = metadata["schema"] + ir_file = entries[0].with_suffix(".ll") + metadata["schema"] = "compile-ir-cache-v2" + entries[0].write_text(json.dumps(metadata)) + ir_file.write_text(ir_file.read_text().replace("ret i32 42", "ret i32 7")) + + value, hit = run("previous") + rewritten = json.loads(entries[0].read_text())["schema"] + if value == "42" and not hit and rewritten == current: + print(" ✅ an entry of the previous schema is compiled again and rewritten") + else: + ok = False + print(f" ❌ with the previous schema: returns {value}, cache hit {hit}, entry schema {rewritten}") + value, hit = run("rebuilt") + if value == "42" and hit: + print(" ✅ the rewritten entry is reused") + else: + ok = False + print(f" ❌ after the rebuild: returns {value}, cache hit {hit}") + print() + return ok + + +def check_unreadable_model_fails() -> bool: + """ + A model the command line asks for, but that cannot be read, is a configuration error: the + analyzer must stop with a nonzero code naming the file, and run_code_analysis.py must fail, + instead of analyzing without the model and reporting success. + """ + print("=== Testing that a requested model that cannot be read fails the run ===") + ok = True + with tempfile.TemporaryDirectory(prefix="ct_unreadable_model_") as tmp: + tmpdir = Path(tmp) + source = tmpdir / "leak.c" + source.write_text("void* acquire_handle(void);\nvoid leak(void) { (void)acquire_handle(); }\n") + missing = tmpdir / "missing-model.txt" + for option in ("--resource-model", "--escape-model", "--buffer-model"): + for path, what in ((missing, "missing file"), (tmpdir, "directory")): + result = run_analyzer_uncached([f"{option}={path}", str(source)]) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0 and str(path) in output: + print(f" ✅ {option}, {what}: code {result.returncode}, file named") + else: + print(f" ❌ {option}, {what}: code {result.returncode}, file named: {str(path) in output}") + ok = False + script = _run_ci_script( + ["--analyzer", str(RUN_CONFIG.analyzer), "--fail-on", "warning", + f"--analyzer-arg=--resource-model={missing}", str(source)], + jobs=1, + ) + if script.returncode != 0: + print(f" ✅ run_code_analysis.py with a missing model: code {script.returncode}") + else: + print(" ❌ run_code_analysis.py passed with a missing model") + print((script.stdout or "") + (script.stderr or "")) + ok = False + print() + return ok + + def check_docker_entrypoint_guardrails() -> bool: """ Regression: docker wrapper should only create compatibility symlinks under @@ -5055,6 +5323,7 @@ def record_ok(ok: bool): check_exclude_dir_filter, check_multi_tu_folder_analysis, check_resource_lifetime_cross_tu, + check_resource_model_across_compile_directories, check_ownership_cross_tu, check_ownership_wrapper_metadata, check_uninitialized_cross_tu, @@ -5068,6 +5337,8 @@ def record_ok(ok: bool): check_noreturn_cross_tu, check_use_after_free_advanced_inter_tu, check_escape_model_rejects_unsupported_brackets, + check_compile_ir_cache_rebuilds_previous_schema, + check_unreadable_model_fails, check_human_vs_json_parity, check_diagnostic_rule_coverage_regression, check_diagnostic_cwe_coverage, diff --git a/src/analysis/InputPipeline.cpp b/src/analysis/InputPipeline.cpp index e08272e4..b19df826 100644 --- a/src/analysis/InputPipeline.cpp +++ b/src/analysis/InputPipeline.cpp @@ -38,23 +38,16 @@ namespace ctrace::stack::analysis { namespace { - std::mutex gCompileWorkingDirMutex; // compilerlib drives an in-process clang CompilerInstance. clang/LLVM keep global // mutable state (target registry, cl options, timers); two worker threads compiling // at once hung intermittently on the first compile of a run. Compilation is // serialised here; module analysis stays parallel, and the IR cache makes the - // compile step cheap on repeated runs. + // compile step cheap on repeated runs. A compile that needs its compile command's + // directory changes the working directory of the whole process: it holds the same + // lock from before that change until it is undone, so no other compile runs in a + // directory that is not its own, and two changes never interleave. std::mutex gCompileInvokeMutex; - std::string makeAbsolutePath(const std::string& path) - { - std::error_code ec; - std::filesystem::path absPath = std::filesystem::absolute(path, ec); - if (ec) - return path; - return absPath.lexically_normal().generic_string(); - } - void appendIfMissing(std::vector& args, const std::string& flag) { if (std::find(args.begin(), args.end(), flag) == args.end()) @@ -199,7 +192,7 @@ namespace ctrace::stack::analysis } } - constexpr llvm::StringLiteral kCompileIRCacheSchema = "compile-ir-cache-v2"; + constexpr llvm::StringLiteral kCompileIRCacheSchema = "compile-ir-cache-v3"; struct FileSnapshot { @@ -659,14 +652,15 @@ namespace ctrace::stack::analysis return true; } - bool buildCompileArgs(const std::string& filename, LanguageType language, - const AnalysisConfig& config, std::vector& args, - std::string& workingDir, std::string& error) + bool buildCompileArgs(const std::string& filename, const std::string& inputPath, + LanguageType language, const AnalysisConfig& config, + std::vector& args, std::string& workingDir, + std::string& error) { const CompileCommand* command = nullptr; if (config.compilationDatabase) { - command = config.compilationDatabase->findCommandForFile(filename); + command = config.compilationDatabase->findCommandForFile(inputPath); } if (command) @@ -711,7 +705,7 @@ namespace ctrace::stack::analysis args.push_back("-g"); appendIfMissing(args, "-fno-discard-value-names"); const bool useAbsolutePath = (command != nullptr); - args.push_back(useAbsolutePath ? makeAbsolutePath(filename) : filename); + args.push_back(useAbsolutePath ? inputPath : filename); return true; } @@ -820,13 +814,19 @@ namespace ctrace::stack::analysis using ctrace::stack::analyzer::ScopedHotspot; ModuleLoadResult result; const ScopedHotspot totalHotspot(config.timing, "input.load_module.total"); + // A relative input resolves from the directory the analysis started in, not from the + // current one: another thread may be compiling from its compile command's directory, + // which the whole process shares meanwhile. std::error_code cwdErr; - std::filesystem::path baseDir = std::filesystem::current_path(cwdErr); + const std::filesystem::path baseDir = config.inputBaseDir.empty() + ? std::filesystem::current_path(cwdErr) + : std::filesystem::path(config.inputBaseDir); + const std::string inputPath = makeAbsolutePathFrom(filename, baseDir.string()); using Clock = std::chrono::steady_clock; auto compileStart = Clock::now(); { const ScopedHotspot hotspot(config.timing, "input.detect_language"); - result.language = detectLanguageFromFile(filename, ctx); + result.language = detectLanguageFromFile(inputPath, ctx); } if (result.language == LanguageType::Unknown) @@ -844,8 +844,8 @@ namespace ctrace::stack::analysis bool compileArgsReady = false; { const ScopedHotspot hotspot(config.timing, "input.build_compile_args"); - compileArgsReady = buildCompileArgs(filename, result.language, config, args, - workingDir, compileError); + compileArgsReady = buildCompileArgs(filename, inputPath, result.language, config, + args, workingDir, compileError); } if (!compileArgsReady) { @@ -857,7 +857,7 @@ namespace ctrace::stack::analysis coretrace::log(coretrace::Level::Info, "Compiling {}...\n", filename); const bool preferBitcodeCompile = (config.compileIRFormat == CompileIRFormat::BC); const CompileIRCachePaths cachePaths = - buildCompileIRCachePaths(config, filename, result.language, args, workingDir); + buildCompileIRCachePaths(config, inputPath, result.language, args, workingDir); std::error_code tempDirErr; std::filesystem::path tempDir = std::filesystem::temp_directory_path(tempDirErr); @@ -872,14 +872,16 @@ namespace ctrace::stack::analysis const std::vector bitcodeArgs = buildBitcodeCompileArgs(args, tempBitcodePath); - auto compileWithOptionalWorkingDir = - [&](const std::vector& compileArgs, compilerlib::OutputMode outputMode, - bool useWorkingDir) -> std::optional + // A compile command's relative paths are relative to its directory: compile there, + // even when the current directory also resolves them, to another header. + auto compileInWorkingDir = + [&](const std::vector& compileArgs, + compilerlib::OutputMode outputMode) -> std::optional { - if (!useWorkingDir) + std::lock_guard compileLock(gCompileInvokeMutex); + if (workingDir.empty()) { const ScopedHotspot hotspot(config.timing, "input.compiler.invoke"); - std::lock_guard compileLock(gCompileInvokeMutex); return compilerlib::compile(compileArgs, outputMode); } @@ -891,38 +893,9 @@ namespace ctrace::stack::analysis return std::nullopt; } const ScopedHotspot hotspot(config.timing, "input.compiler.invoke.cwd"); - std::lock_guard compileLock(gCompileInvokeMutex); return compilerlib::compile(compileArgs, outputMode); }; - auto compileWithConfiguredWorkingDir = - [&](const std::vector& compileArgs, compilerlib::OutputMode outputMode, - bool& retriedWithWorkingDir) -> std::optional - { - retriedWithWorkingDir = false; - std::optional res; - const bool hasWorkingDir = !workingDir.empty(); - if (config.jobs > 1 && hasWorkingDir) - { - // Optimistic fast path for multi-job runs: most compdb commands use absolute - // paths. - res = compileWithOptionalWorkingDir(compileArgs, outputMode, false); - if (!res || !res->success) - { - // Fallback keeps correctness for relative include paths and avoids process - // cwd races. - std::lock_guard lock(gCompileWorkingDirMutex); - res = compileWithOptionalWorkingDir(compileArgs, outputMode, true); - retriedWithWorkingDir = true; - } - } - else - { - res = compileWithOptionalWorkingDir(compileArgs, outputMode, hasWorkingDir); - } - return res; - }; - if (cachePaths.enabled) { auto cached = [&]() -> std::optional @@ -1044,7 +1017,6 @@ namespace ctrace::stack::analysis } } - bool retriedWithWorkingDir = false; bool compiledViaBitcode = false; std::optional res; std::optional sourceSnapshot; @@ -1057,20 +1029,14 @@ namespace ctrace::stack::analysis preferBitcodeCompile ? bitcodeArgs : args; appendDependencyCaptureArgs(cacheCompileArgs, cachePaths.depFile); - bool retriedForDependencyCompile = false; - res = compileWithConfiguredWorkingDir(cacheCompileArgs, - preferBitcodeCompile - ? compilerlib::OutputMode::ToFile - : compilerlib::OutputMode::ToMemory, - retriedForDependencyCompile); - retriedWithWorkingDir = retriedForDependencyCompile; + res = compileInWorkingDir(cacheCompileArgs, + preferBitcodeCompile ? compilerlib::OutputMode::ToFile + : compilerlib::OutputMode::ToMemory); if (preferBitcodeCompile && (!res || !res->success)) { - bool retriedForFallbackCompile = false; - auto fallbackResult = compileWithConfiguredWorkingDir( - args, compilerlib::OutputMode::ToMemory, retriedForFallbackCompile); - retriedWithWorkingDir = retriedWithWorkingDir || retriedForFallbackCompile; + auto fallbackResult = + compileInWorkingDir(args, compilerlib::OutputMode::ToMemory); if (fallbackResult) res = std::move(fallbackResult); } @@ -1083,7 +1049,7 @@ namespace ctrace::stack::analysis "input.cache.parse_dependencies"); return parseDepfileDependencies(cachePaths.depFile, workingDir); }(); - const std::string sourcePath = makeAbsolutePathFrom(filename, workingDir); + const std::string& sourcePath = inputPath; sourceSnapshot = [&]() -> std::optional { const ScopedHotspot hotspot(config.timing, @@ -1100,21 +1066,17 @@ namespace ctrace::stack::analysis } else { - res = compileWithConfiguredWorkingDir(preferBitcodeCompile ? bitcodeArgs : args, - preferBitcodeCompile - ? compilerlib::OutputMode::ToFile - : compilerlib::OutputMode::ToMemory, - retriedWithWorkingDir); + res = compileInWorkingDir(preferBitcodeCompile ? bitcodeArgs : args, + preferBitcodeCompile ? compilerlib::OutputMode::ToFile + : compilerlib::OutputMode::ToMemory); if (res && res->success) { compiledViaBitcode = preferBitcodeCompile; } else if (preferBitcodeCompile) { - bool retriedForFallbackCompile = false; - auto fallbackResult = compileWithConfiguredWorkingDir( - args, compilerlib::OutputMode::ToMemory, retriedForFallbackCompile); - retriedWithWorkingDir = retriedWithWorkingDir || retriedForFallbackCompile; + auto fallbackResult = + compileInWorkingDir(args, compilerlib::OutputMode::ToMemory); if (fallbackResult) res = std::move(fallbackResult); } @@ -1140,8 +1102,7 @@ namespace ctrace::stack::analysis auto ms = std::chrono::duration_cast(compileEnd - compileStart) .count(); - coretrace::log(coretrace::Level::Info, "Compilation done in {} ms{}\n", ms, - retriedWithWorkingDir ? " (retry with working directory)" : ""); + coretrace::log(coretrace::Level::Info, "Compilation done in {} ms\n", ms); } std::string llvmIRForCache; @@ -1195,10 +1156,8 @@ namespace ctrace::stack::analysis { if (compiledViaBitcode) { - bool retriedForTextFallback = false; - auto textFallback = compileWithConfiguredWorkingDir( - args, compilerlib::OutputMode::ToMemory, retriedForTextFallback); - retriedWithWorkingDir = retriedWithWorkingDir || retriedForTextFallback; + auto textFallback = + compileInWorkingDir(args, compilerlib::OutputMode::ToMemory); if (!textFallback) return result; res = std::move(textFallback); @@ -1296,7 +1255,7 @@ namespace ctrace::stack::analysis cwdErr ? std::string() : baseDir.lexically_normal().generic_string(); const CompileIRCachePaths cachePaths = isTextIRInput - ? buildCompileIRCachePaths(config, filename, result.language, {}, cacheWorkingDir) + ? buildCompileIRCachePaths(config, inputPath, result.language, {}, cacheWorkingDir) : CompileIRCachePaths{}; if (isTextIRInput && cachePaths.enabled) @@ -1398,7 +1357,7 @@ namespace ctrace::stack::analysis const auto parseStart = Clock::now(); { const ScopedHotspot hotspot(config.timing, "input.parse_ir_file"); - result.module = llvm::parseIRFile(filename, err, ctx); + result.module = llvm::parseIRFile(inputPath, err, ctx); } if (config.timing) { @@ -1412,10 +1371,10 @@ namespace ctrace::stack::analysis { if (isTextIRInput && cachePaths.enabled) { - if (const auto sourceSnapshot = captureFileSnapshot(filename)) + if (const auto sourceSnapshot = captureFileSnapshot(inputPath)) { std::string sourceIR; - (void)readTextFile(filename, sourceIR); + (void)readTextFile(inputPath, sourceIR); std::string llvmBitcode; llvm::raw_string_ostream bitcodeStream(llvmBitcode); diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index 7a037ede..ed682262 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -835,6 +835,26 @@ static AppStatus configureDumpIRPath(const std::vector& inputFilena return AppStatus::success(); } +// A model the command line asks for is read by the analyses that use it, which only log a +// failure and run without it: an unreadable model would pass for a run that found nothing. +static AppStatus checkRequestedModelsReadable(const AnalysisConfig& cfg) +{ + const std::pair models[] = { + {"resource", &cfg.resourceModelPath}, + {"stack escape", &cfg.escapeModelPath}, + {"buffer", &cfg.bufferModelPath}, + }; + for (const auto& [kind, path] : models) + { + if (path->empty()) + continue; + std::error_code fsErr; + if (!std::filesystem::is_regular_file(*path, fsErr) || !std::ifstream(*path)) + return AppStatus::failure(std::string("Cannot read ") + kind + " model file: " + *path); + } + return AppStatus::success(); +} + static void printInterprocStatus(const AnalysisConfig& cfg, std::size_t inputCount, bool needsCrossTUResourceSummaries, bool needsCrossTUUninitializedSummaries, @@ -2616,6 +2636,10 @@ class RunPlanBuilder plan.sarifBaseDir = std::move(parsedArgs_.sarifBaseDir); plan.sarifOutPath = std::move(parsedArgs_.sarifOutPath); + AppStatus modelStatus = checkRequestedModelsReadable(plan.cfg); + if (!modelStatus.isOk()) + return AppResult::failure(std::move(modelStatus.error)); + if (parsedArgs_.compileCommandsExplicit) { AppStatus loadStatus = diff --git a/src/cli/ArgParser.cpp b/src/cli/ArgParser.cpp index d2677864..9348d6a1 100644 --- a/src/cli/ArgParser.cpp +++ b/src/cli/ArgParser.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1658,6 +1659,22 @@ namespace ctrace::stack::cli parsed.inputFilenames.emplace_back(std::move(argStr)); } + // The analysis opens these while other threads may be compiling from the directory of + // their compile command: they are anchored to the directory the analyzer starts from. + std::error_code startDirError; + const std::filesystem::path startDir = std::filesystem::current_path(startDirError); + if (!startDirError) + { + for (std::string* path : + {&cfg.escapeModelPath, &cfg.bufferModelPath, &cfg.resourceModelPath, + &cfg.resourceSummaryCacheDir, &cfg.compileIRCacheDir, &cfg.dumpIRPath, + &parsed.compileCommandsPath}) + { + *path = resolveConfigRelativePath(*path, startDir); + } + cfg.inputBaseDir = startDir.string(); + } + return result; }