// End-to-end integration test for `fold fuzz-prep`. Materializes a small // C fixture with a null-deref-behind-magic-byte-guard, drives the fuzz- // prep runner in-process, and asserts that the resulting corpus contains // a Z3-derived seed encoding the magic value the guard checks for. // // This validates the whole Phase 1 pipeline end-to-end: // detector SAT → model extraction → schemaForFunction → packSeed → // findings.predicted.json + corpus/*/.bin #include #include #include #include #include #include #include #include #include #include "fold/cli/fuzz_prep_runner.hpp" namespace fs = std::filesystem; namespace { // Materialize the fixture C file + a single-entry compile_commands.json // under a fresh tmp dir. Returns the dir path. fs::path writeFixture(const std::string& source) { fs::path tmp = fs::temp_directory_path() / "fold-fuzz-prep-test"; fs::remove_all(tmp); fs::create_directories(tmp); fs::path csrc = tmp / "fixture.c"; std::ofstream(csrc) << source; nlohmann::json db = nlohmann::json::array(); db.push_back({ {"directory", tmp.string()}, {"command", "clang -c fixture.c -o fixture.o"}, {"file", csrc.string()}, }); std::ofstream(tmp / "compile_commands.json") << db.dump(2); return tmp; } std::vector readBytes(const fs::path& p) { std::ifstream f(p, std::ios::binary); REQUIRE(f); std::vector bytes((std::istreambuf_iterator(f)), {}); return bytes; } } // namespace TEST_CASE("fuzz-prep generates Rust harnesses from interface metadata", "[fuzz][rust]") { fs::path tmp = fs::temp_directory_path() / "fold-fuzz-prep-rust-test"; fs::remove_all(tmp); fs::create_directories(tmp); const fs::path source = tmp / "unsafe.rs"; std::ofstream(source) << "pub fn fuzz_me(data: &[u8], limit: u32) -> bool { " << "data.len() as u32 > limit }\n"; fold::cli::FuzzPrepOptions opts; opts.scan.mode = "rust"; opts.scan.input_type = "rust"; opts.scan.source_paths = {source.string()}; opts.scan.detectors = {"rust-unsafe-api"}; opts.output_dir = (tmp / "out").string(); const int rc = fold::cli::runFuzzPrep(std::move(opts)); REQUIRE(rc == 0); // RF-8: non-empty harness output auto predicted = nlohmann::json::parse(std::ifstream(tmp / "out" / "findings.predicted.json")); REQUIRE(predicted["predictions"].is_array()); REQUIRE_FALSE(predicted["predictions"].empty()); REQUIRE(fs::exists(tmp / "out" / "fuzz_me.harness.rs")); // Validate harness content — must contain the target function call // and unpack bytes into typed arguments. std::ifstream harness_in(tmp / "out" / "fuzz_me.harness.rs"); std::ostringstream harness_buf; harness_buf << harness_in.rdbuf(); const std::string harness_src = harness_buf.str(); REQUIRE(harness_src.find("fuzz_me(data, limit)") != std::string::npos); REQUIRE(harness_src.find("u32::from_le_bytes") != std::string::npos); // Verify confirmation task bridging: Rust findings flow into the // confirmation.tasks.json sidecar with ConfirmationLane::RustHir. if (fs::exists(tmp / "out" / "confirmation.tasks.json")) { auto tasks = nlohmann::json::parse(std::ifstream(tmp / "out" / "confirmation.tasks.json")); REQUIRE(tasks.is_array()); bool has_rust_lane = false; for (const auto& task : tasks) { if (task.value("lane", "") == "rust-hir") has_rust_lane = true; } REQUIRE(has_rust_lane); } // TODO(RF-8): add crash classification — compile with rustc + cargo-fuzz, // run against a seed, capture panics/aborts, and promote findings to // ConcreteWitness. See docs/internal/rust-frontend.md for the plan. fs::remove_all(tmp); } TEST_CASE("fuzz-prep emits a witness seed encoding the magic byte that flips a guard", "[fuzz][e2e]") { // The deref behind the guard fires when flag == 0xAB and p == NULL. // The corpus seed packs the function's two parameters: pointer first // (ConcreteNull → 0 bytes), then the int flag (4 bytes, low byte 0xAB). auto tmp = writeFixture(R"c( int leaky(int *p, int flag) { if (flag == 0xAB) { return *p; } return 0; } )c"); fold::cli::FuzzPrepOptions opts; opts.scan.compile_db = (tmp / "compile_commands.json").string(); opts.output_dir = (tmp / "out").string(); const int rc = fold::cli::runFuzzPrep(std::move(opts)); REQUIRE(rc == 0); fs::path out(tmp / "out"); REQUIRE(fs::exists(out / "findings.predicted.json")); // The predicted file maps every witness-derived seed back to the // exact Finding.id `fold scan` would emit. Check we found the // null-deref and that the witness is non-partial (R3 model bytes // populated, not zero-filled). auto predicted = nlohmann::json::parse(std::ifstream(out / "findings.predicted.json")); REQUIRE(predicted["predictions"].is_array()); REQUIRE(fs::exists(out / "confirmation.tasks.json")); auto tasks = nlohmann::json::parse(std::ifstream(out / "confirmation.tasks.json")); REQUIRE(tasks.is_array()); REQUIRE_FALSE(tasks.empty()); bool found_null_deref_task = false; for (const auto& task : tasks) { if (task.value("hazard", "") == "null-deref" && task.value("adapter", "") == "libfuzzer-asan") found_null_deref_task = true; } REQUIRE(found_null_deref_task); bool found_null_deref = false; fs::path witness_seed_path; for (const auto& p : predicted["predictions"]) { const std::string fid = p["finding_id"].get(); if (fid.find("null-deref:") == 0) { found_null_deref = true; REQUIRE(p["partial"].get() == false); witness_seed_path = out / p["seed"].get(); break; } } REQUIRE(found_null_deref); REQUIRE(fs::exists(witness_seed_path)); // Inspect the seed bytes. Layout per the schema: parameter 0 (`p`, // pointer with shape=null) contributes 0 bytes; parameter 1 (`flag`, // 32-bit int) contributes 4 bytes at offset 0 in little-endian. Z3 // must have chosen 0xAB to satisfy `flag == 0xAB`, so the first // byte must be 0xAB. auto seed = readBytes(witness_seed_path); REQUIRE(seed.size() == 4); REQUIRE(seed[0] == 0xAB); REQUIRE(seed[1] == 0x00); REQUIRE(seed[2] == 0x00); REQUIRE(seed[3] == 0x00); // Dogfood fix: predictions JSON must carry harness_file so fuzz-run // doesn't derive it from the function name (encoding-mismatch fix). bool has_harness_file = false; bool has_compile_flags = false; for (const auto& p : predicted["predictions"]) { if (p.contains("harness_file") && !p["harness_file"].get().empty()) has_harness_file = true; if (p.contains("compile_flags") && p["compile_flags"].is_array()) has_compile_flags = true; } REQUIRE(has_harness_file); // Dogfood fix: compile_flags may be empty for synthetic fixtures // (no compile_commands.json), but the field must be present. (void)has_compile_flags; // The harness source must exist and contain the extern "C" wrapper // (C-linkage detection from R1), the size-gate at the witness's // total_bytes (4), and the call to the target function. We don't // shell out to clang for the syntax check here — the unit-test // harness_emitter suite already covers that. fs::path harness = out / "leaky.harness.cc"; REQUIRE(fs::exists(harness)); std::ifstream harness_in(harness); std::ostringstream harness_buf; harness_buf << harness_in.rdbuf(); const std::string harness_src = harness_buf.str(); REQUIRE(harness_src.find("extern \"C\"") != std::string::npos); REQUIRE(harness_src.find("if (size < 4u) return 0;") != std::string::npos); REQUIRE(harness_src.find("leaky(arg0, arg1);") != std::string::npos); fs::remove_all(tmp); }