- Rust IR encoder: null-deref, div-by-zero, int-overflow, oob (was null-deref only) - Swift SIL encoder: reuses RustExprNode-compatible IR from fold-swift - Locationless finding filter in filterAndDedup before SARIF/JSON emit - Encoder failures degrade to heuristic findings instead of silent skip - Z3 exception guard in encodeRustIr (per-function fault isolation) - Embedded fold skill via `fold skill` subcommand and --help - Tests: fuzz-prep Rust lane, dedup locationless filter, IR encoder Rust and Swift proved findings now flow into confirmation.tasks.json. Confidence: concrete-witness > proved > heuristic > syntactic.
209 lines
8 KiB
C++
209 lines
8 KiB
C++
// 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/*/<sha1>.bin
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <nlohmann/json.hpp>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<std::uint8_t> readBytes(const fs::path& p) {
|
|
std::ifstream f(p, std::ios::binary);
|
|
REQUIRE(f);
|
|
std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(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<std::string>();
|
|
if (fid.find("null-deref:") == 0) {
|
|
found_null_deref = true;
|
|
REQUIRE(p["partial"].get<bool>() == false);
|
|
witness_seed_path = out / p["seed"].get<std::string>();
|
|
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<std::string>().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);
|
|
}
|