#include #include #include #include #include #include #include #include #include #include #include namespace { struct TempDir { std::filesystem::path path; TempDir() : path(std::filesystem::temp_directory_path() / ("fold-rust-frontend-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))) { std::filesystem::create_directories(path); } ~TempDir() { std::filesystem::remove_all(path); } }; struct RustScanResult { int exit_code = 0; nlohmann::json report; std::string log; }; void writeFile(const std::filesystem::path& path, const std::string& content) { std::ofstream(path) << content; } void writeExecutable(const std::filesystem::path& path, const std::string& content) { writeFile(path, content); std::filesystem::permissions(path, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::owner_exec, std::filesystem::perm_options::replace); } std::string readFile(const std::filesystem::path& path) { std::ifstream in(path); std::ostringstream out; out << in.rdbuf(); return out.str(); } std::string shellQuote(const std::filesystem::path& path) { std::string out = "'"; for (char c : path.string()) { if (c == '\'') out += "'\\''"; else out += c; } out += "'"; return out; } std::filesystem::path locateFoldBinary() { if (const char* env = std::getenv("FOLD_BINARY")) { if (*env && std::filesystem::exists(env)) return std::filesystem::canonical(env); } auto cwd = std::filesystem::current_path(); for (const auto& rel : {"../../src/cli/fold", "../../../src/cli/fold", "../src/cli/fold"}) { auto candidate = cwd / rel; if (std::filesystem::exists(candidate)) return std::filesystem::canonical(candidate); } return {}; } std::filesystem::path locateFoldRustBinary() { if (const char* env = std::getenv("FOLD_RUST_BIN")) { if (*env && std::filesystem::exists(env)) return std::filesystem::canonical(env); } auto cwd = std::filesystem::current_path(); for (const auto& rel : {"../../tools/fold-rust/target/release/fold-rust", "../../../tools/fold-rust/target/release/fold-rust", "../tools/fold-rust/target/release/fold-rust"}) { auto candidate = cwd / rel; if (std::filesystem::exists(candidate)) return std::filesystem::canonical(candidate); } return {}; } std::optional runRustScanTarget(const std::filesystem::path& target, const std::string& detectors = {}, const std::map& env = {}) { const auto fold_bin = locateFoldBinary(); const auto fold_rust_bin = env.contains("FOLD_RUST_BIN") ? std::filesystem::path(env.at("FOLD_RUST_BIN")) : locateFoldRustBinary(); if (fold_bin.empty() || fold_rust_bin.empty()) { WARN("fold or fold-rust binary missing; skipping Rust frontend integration test"); return std::nullopt; } TempDir dir; const auto output = dir.path / "report.json"; const auto log = dir.path / "scan.log"; std::string cmd; cmd += "FOLD_RUST_BIN=" + shellQuote(fold_rust_bin) + " "; for (const auto& [key, value] : env) if (key != "FOLD_RUST_BIN") cmd += key + "=" + shellQuote(value) + " "; cmd += shellQuote(fold_bin) + " scan --mode=rust "; if (!detectors.empty()) cmd += "--detectors " + detectors + " "; cmd += shellQuote(target) + " --format sarif --output " + shellQuote(output) + " > " + shellQuote(log) + " 2>&1"; RustScanResult result; result.exit_code = std::system(cmd.c_str()); result.log = readFile(log); if (std::filesystem::exists(output)) result.report = nlohmann::json::parse(std::ifstream(output)); return result; } std::optional runRustScan(const std::string& source_text, const std::string& detectors = {}) { TempDir dir; const auto source = dir.path / "unsafe.rs"; writeFile(source, source_text); return runRustScanTarget(source, detectors); } std::filesystem::path fixturesDir() { if (const char* env = std::getenv("FOLD_FIXTURES_DIR")) { if (*env && std::filesystem::exists(env)) return std::filesystem::canonical(env); } auto cwd = std::filesystem::current_path(); for (const auto& rel : {"../fixtures", "../../tests/fixtures", "tests/fixtures"}) { auto candidate = cwd / rel; if (std::filesystem::exists(candidate)) return std::filesystem::canonical(candidate); } return {}; } const nlohmann::json& scanResults(const RustScanResult& scan) { return scan.report.at("runs").at(0).at("results"); } std::string correlationKind(const nlohmann::json& result) { return result.at("properties").value("correlationKind", std::string{}); } } // namespace TEST_CASE("fold-rust reports its rustc-private toolchain contract", "[rust][frontend]") { const auto fold_rust_bin = locateFoldRustBinary(); if (fold_rust_bin.empty()) { WARN("fold-rust binary missing; skipping toolchain contract test"); return; } TempDir dir; const auto output = dir.path / "toolchain.json"; const std::string cmd = shellQuote(fold_rust_bin) + " toolchain-info > " + shellQuote(output); REQUIRE(std::system(cmd.c_str()) == 0); const auto info = nlohmann::json::parse(std::ifstream(output)); REQUIRE(info.at("schema_version") == 1); REQUIRE(info.at("release").get().find("nightly") != std::string::npos); REQUIRE(info.at("requires").at("minimum_release") == "1.98.0"); REQUIRE(info.at("requires").at("components") == nlohmann::json::array({"rustc-dev", "rustfmt", "llvm-tools-preview"})); } TEST_CASE("rust frontend respects detector selection", "[rust][frontend]") { auto scan = runRustScan("pub unsafe fn read_raw(p: *const i32) -> i32 { *p }\n", "div-by-zero"); if (!scan) return; REQUIRE(scan->exit_code == 0); // no selected detector emitted findings REQUIRE(scanResults(*scan).empty()); } TEST_CASE("rust frontend runs the Rust IR safety pass for div-by-zero selection", "[rust][frontend]") { auto scan = runRustScan( "pub fn maybe_div(y: i32, flag: bool) -> i32 { let x = if flag { 0 } else { y }; 1 / x }\n", "div-by-zero"); if (!scan) return; REQUIRE(scan->log.find("rust shard: IR encoder processed ") != std::string::npos); } TEST_CASE("fold-rust cargo-check emits each source unit once", "[rust][frontend][cargo-check]") { const auto fold_rust_bin = locateFoldRustBinary(); const auto fixtures = fixturesDir(); if (fold_rust_bin.empty() || fixtures.empty()) { WARN("fold-rust binary or fixtures missing; skipping cargo-check integration test"); return; } TempDir dir; const auto output = dir.path / "cargo-check.ndjson"; const auto crate = fixtures / "rust" / "security_crate"; const std::string cmd = shellQuote(fold_rust_bin) + " cargo-check --crate-dir " + shellQuote(crate) + " > " + shellQuote(output); REQUIRE(std::system(cmd.c_str()) == 0); std::map functions; std::istringstream lines(readFile(output)); std::string line; while (std::getline(lines, line)) { if (line.empty()) continue; const auto record = nlohmann::json::parse(line); if (record.value("kind", std::string{}) == "function") ++functions[record.at("data").at("qualified_name").get()]; } REQUIRE_FALSE(functions.empty()); for (const auto& [name, count] : functions) CHECK(count == 1); } TEST_CASE("rust frontend emits Rust-specific unsafe API rule IDs", "[rust][frontend]") { auto scan = runRustScan("pub unsafe fn cast_bits(x: u32) -> i32 { " "std::mem::transmute::(x) }\n", "rust-unsafe-api"); if (!scan) return; REQUIRE(scan->exit_code != 0); // exit 1: actionable finding const auto& results = scanResults(*scan); REQUIRE(results.size() == 1); REQUIRE(results.at(0).at("ruleId") == "fold.rust-unsafe-api"); REQUIRE(results.at(0).at("properties").value("backend", std::string{}) == "rust"); REQUIRE(correlationKind(results.at(0)) == "rust-transmute"); } TEST_CASE("rust frontend flags unsafe memory API footguns", "[rust][frontend]") { auto scan = runRustScan( "pub unsafe fn cast_bits(x: u32) -> i32 { std::mem::transmute::(x) }\n" "pub unsafe fn raw_slice<'a>(p: *const u8, len: usize) -> &'a [u8] { " "std::slice::from_raw_parts(p, len) }\n" "pub unsafe fn trust_len(v: &mut Vec, len: usize) { v.set_len(len); }\n", "rust-unsafe-api"); if (!scan) return; REQUIRE(scan->exit_code != 0); const auto& results = scanResults(*scan); bool saw_transmute = false; bool saw_from_raw_parts = false; bool saw_set_len = false; for (const auto& result : results) { REQUIRE(result.at("ruleId") == "fold.rust-unsafe-api"); const auto hazard = correlationKind(result); saw_transmute = saw_transmute || hazard == "rust-transmute"; saw_from_raw_parts = saw_from_raw_parts || hazard == "rust-slice-from_raw_parts"; saw_set_len = saw_set_len || hazard == "rust-vec-set-len"; } REQUIRE(results.size() == 3); REQUIRE(saw_transmute); REQUIRE(saw_from_raw_parts); REQUIRE(saw_set_len); } TEST_CASE("rust frontend flags unchecked indexing APIs", "[rust][frontend]") { auto scan = runRustScan( "pub unsafe fn unchecked_read(xs: &[u8], i: usize) -> u8 { *xs.get_unchecked(i) }\n" "pub unsafe fn unchecked_write(xs: &mut [u8], i: usize) -> *mut u8 { " "xs.get_unchecked_mut(i) as *mut u8 }\n", "rust-unchecked-index"); if (!scan) return; REQUIRE(scan->exit_code != 0); const auto& results = scanResults(*scan); bool saw_read = false; bool saw_write = false; for (const auto& result : results) { REQUIRE(result.at("ruleId") == "fold.rust-unchecked-index"); const auto hazard = correlationKind(result); saw_read = saw_read || hazard == "rust-get-unchecked"; saw_write = saw_write || hazard == "rust-get-unchecked-mut"; } REQUIRE(results.size() == 2); REQUIRE(saw_read); REQUIRE(saw_write); } TEST_CASE("rust frontend flags path traversal sinks", "[rust][frontend]") { auto scan = runRustScan("use std::path::Path;\n" "pub fn join_user(base: &Path, user: &str) { let _ = base.join(user); }\n" "pub fn read_user(user: &str) { let _ = std::fs::read(user); }\n", "rust-path-traversal"); if (!scan) return; REQUIRE(scan->exit_code != 0); const auto& results = scanResults(*scan); bool saw_join = false; bool saw_read = false; for (const auto& result : results) { REQUIRE(result.at("ruleId") == "fold.rust-path-traversal"); const auto hazard = correlationKind(result); saw_join = saw_join || hazard == "rust-path-join"; saw_read = saw_read || hazard == "rust-fs-read"; } REQUIRE(results.size() == 2); REQUIRE(saw_join); REQUIRE(saw_read); } TEST_CASE("rust frontend flags command execution injection sinks", "[rust][frontend]") { auto scan = runRustScan("pub fn run_user_arg(user: &str) { " "let mut cmd = std::process::Command::new(\"echo\"); cmd.arg(user); }\n", "rust-command-injection"); if (!scan) return; REQUIRE(scan->exit_code != 0); const auto& results = scanResults(*scan); REQUIRE(results.size() == 1); REQUIRE(results.at(0).at("ruleId") == "fold.rust-command-injection"); REQUIRE(correlationKind(results.at(0)) == "rust-command-arg"); } TEST_CASE("rust frontend flags crypto misuse patterns", "[rust][frontend]") { auto scan = runRustScan("mod md5 { pub fn compute(_: &[u8]) -> [u8; 16] { [0; 16] } }\n" "pub fn digest_password(password: &[u8]) { let _ = md5::compute(password); }\n", "rust-crypto-misuse"); if (!scan) return; REQUIRE(scan->exit_code != 0); const auto& results = scanResults(*scan); REQUIRE(results.size() == 1); REQUIRE(results.at(0).at("ruleId") == "fold.rust-crypto-misuse"); REQUIRE(correlationKind(results.at(0)) == "rust-weak-hash"); } TEST_CASE("rust frontend scans a Cargo fixture crate", "[rust][frontend][fixture]") { const auto fixtures = fixturesDir(); if (fixtures.empty()) { WARN("fixtures directory missing; skipping Rust fixture-crate integration test"); return; } auto scan = runRustScanTarget(fixtures / "rust" / "security_crate", "rust-security"); if (!scan) return; REQUIRE(scan->exit_code != 0); const auto& results = scanResults(*scan); std::set rule_ids; std::set hazards; for (const auto& result : results) { rule_ids.insert(result.at("ruleId").get()); hazards.insert(correlationKind(result)); REQUIRE(result.at("properties").value("backend", std::string{}) == "rust"); } REQUIRE(rule_ids.contains("fold.rust-unsafe-api")); REQUIRE(rule_ids.contains("fold.rust-unchecked-index")); REQUIRE(rule_ids.contains("fold.rust-path-traversal")); REQUIRE(rule_ids.contains("fold.rust-command-injection")); REQUIRE(rule_ids.contains("fold.rust-crypto-misuse")); REQUIRE(hazards.contains("rust-transmute")); REQUIRE(hazards.contains("rust-slice-from_raw_parts")); REQUIRE(hazards.contains("rust-vec-set-len")); REQUIRE(hazards.contains("rust-get-unchecked")); REQUIRE(hazards.contains("rust-path-join")); REQUIRE(hazards.contains("rust-command-arg")); REQUIRE(hazards.contains("rust-weak-hash")); } TEST_CASE("rust frontend classifies raw pointer read/write and definite null hazards", "[rust][frontend]") { auto scan = runRustScan("pub unsafe fn read_raw(p: *const i32) -> i32 { *p }\n" "pub unsafe fn write_raw(p: *mut i32) { *p = 7; }\n" "pub unsafe fn null_read() -> i32 { *std::ptr::null::() }\n" "pub fn safe_ref(r: &i32) -> i32 { *r }\n"); if (!scan) return; REQUIRE(scan->exit_code != 0); // exit 1: actionable findings const auto& results = scanResults(*scan); bool saw_read = false; bool saw_write = false; bool saw_null_read = false; bool saw_z3_null = false; for (const auto& result : results) { REQUIRE(result.at("ruleId") == "fold.null-deref"); const auto hazard = correlationKind(result); saw_read = saw_read || hazard == "rust-raw-pointer-read"; saw_write = saw_write || hazard == "rust-raw-pointer-write"; saw_null_read = saw_null_read || hazard == "rust-raw-pointer-null-read"; // Z3-encoded findings have no correlationKind (hazard) from HIR. saw_z3_null = saw_z3_null || (result.at("properties").value("confidence", "") == "proved"); } REQUIRE(results.size() >= 3); // at least 3 HIR + possibly Z3 REQUIRE(saw_read); REQUIRE(saw_write); REQUIRE(saw_null_read); // Z3 encoder produces Proved findings when Z3 can prove null reachable. // Acceptable if Z3 times out or returns UNKNOWN on the parameter case. (void)saw_z3_null; // presence is best-effort with default budget } TEST_CASE("rust frontend fails closed on nonzero fake frontend exit", "[rust][frontend]") { const auto fold_bin = locateFoldBinary(); if (fold_bin.empty()) { WARN("fold binary missing; skipping Rust frontend integration test"); return; } TempDir dir; const auto source = dir.path / "input.rs"; writeFile(source, "pub fn demo() {}\n"); const auto fake = dir.path / "fake-fold-rust.sh"; writeExecutable(fake, "#!/bin/sh\n" "printf '%s\\n' '{\"kind\":\"error\",\"data\":{\"message\":\"synthetic " "rust frontend failure\"}}'\n" "printf '%s\\n' " "'{\"kind\":\"function\",\"data\":{\"qualified_name\":\"demo\",\"source_" "file\":\"input.rs\",\"start_line\":1,\"end_line\":1}}'\n" "exit 17\n"); auto scan = runRustScanTarget(source, {}, {{"FOLD_RUST_BIN", fake.string()}}); if (!scan) return; REQUIRE(scan->exit_code != 0); REQUIRE(scan->log.find("scan failed:") != std::string::npos); REQUIRE(scan->log.find("synthetic rust frontend failure") != std::string::npos); REQUIRE(scan->log.find("exit status 17") != std::string::npos); } TEST_CASE("rust frontend fails closed on timed out fake frontend", "[rust][frontend]") { const auto fold_bin = locateFoldBinary(); if (fold_bin.empty()) { WARN("fold binary missing; skipping Rust frontend integration test"); return; } TempDir dir; const auto source = dir.path / "input.rs"; writeFile(source, "pub fn demo() {}\n"); const auto fake = dir.path / "fake-fold-rust-timeout.sh"; writeExecutable( fake, "#!/bin/sh\n" "printf '%s\\n' '{\"kind\":\"error\",\"data\":{\"message\":\"synthetic rust timeout\"}}'\n" "sleep 2\n"); // Leave enough startup headroom for loaded parallel test runners while // keeping the subprocess well beyond the timeout boundary. auto scan = runRustScanTarget( source, {}, {{"FOLD_RUST_BIN", fake.string()}, {"FOLD_FRONTEND_SUBPROCESS_TIMEOUT_MS", "500"}}); if (!scan) return; REQUIRE(scan->exit_code != 0); REQUIRE(scan->log.find("scan failed:") != std::string::npos); REQUIRE(scan->log.find("timed out") != std::string::npos); REQUIRE(scan->log.find("synthetic rust timeout") != std::string::npos); }