- Rust IR encoder (encodeRustIr): walks HIR expr trees, fires SafetyObserver hooks - Swift SIL frontend (fold-swift): swiftc -emit-sil parser, demangling, NDJSON output - SwiftBackend: --mode swift CLI routing, shells out to fold-swift - Binary-to-fuzz: Mach-O export discovery, ARM64 ABI param-count inference, dlopen harness generation - Language-agnostic IR: json_keys canonical constants, Call/Member/ArraySubscript/Cast/HeapNew nodes - Dogfood fixes: SARIF kind field, cross-detector dedup, harness_file in predictions, Swift stoi safety - Corpuses: swift-sil-z3-corpus.md, binary-fuzz-corpus.md - AGENTS.md: cross-language wire format rule
223 lines
8.7 KiB
C++
223 lines
8.7 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "fold/fuzz/harness_emitter.hpp"
|
|
#include "fold/fuzz/witness_recorder.hpp"
|
|
|
|
using fold::fuzz::emitHarness;
|
|
using fold::fuzz::HarnessArtifacts;
|
|
using fold::fuzz::makeSafeName;
|
|
using fold::fuzz::ParamWitness;
|
|
using fold::fuzz::PathWitness;
|
|
using fold::fuzz::PointerShape;
|
|
using fold::fuzz::WitnessKind;
|
|
|
|
namespace {
|
|
|
|
PathWitness twoParamIntegerWitness() {
|
|
PathWitness w;
|
|
w.function = "ns::target";
|
|
w.cpp_return_type = "void";
|
|
w.finding_id = "null-deref:test.c:10:1";
|
|
|
|
ParamWitness x;
|
|
x.param_index = 0;
|
|
x.name = "x";
|
|
x.cpp_type = "int";
|
|
x.kind = WitnessKind::Integer;
|
|
x.bit_width = 32;
|
|
x.is_signed = true;
|
|
|
|
ParamWitness y;
|
|
y.param_index = 1;
|
|
y.name = "y";
|
|
y.cpp_type = "unsigned char";
|
|
y.kind = WitnessKind::Integer;
|
|
y.bit_width = 8;
|
|
y.is_signed = false;
|
|
|
|
w.params = {x, y};
|
|
return w;
|
|
}
|
|
|
|
PathWitness pointerWitness() {
|
|
PathWitness w;
|
|
w.function = "raw_target";
|
|
w.cpp_return_type = "void";
|
|
w.finding_id = "null-deref:test.c:42:1";
|
|
|
|
ParamWitness p;
|
|
p.param_index = 0;
|
|
p.name = "p";
|
|
p.cpp_type = "char*";
|
|
p.kind = WitnessKind::Pointer;
|
|
p.pointer.shape = PointerShape::NullableBuffer;
|
|
p.pointer.buffer_bytes = 16;
|
|
|
|
w.params = {p};
|
|
return w;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("makeSafeName sanitizes :: and template chars", "[fuzz][harness][safe_name]") {
|
|
REQUIRE(makeSafeName("ns::target") == "ns_target");
|
|
REQUIRE(makeSafeName("ns::Vec<int>::push_back") == "ns_Vec_int_push_back");
|
|
REQUIRE(makeSafeName("f") == "f");
|
|
REQUIRE(makeSafeName("a::") == "a");
|
|
}
|
|
|
|
TEST_CASE("makeSafeName strips anonymous namespace markers", "[fuzz][harness][safe_name]") {
|
|
REQUIRE(makeSafeName("fold::(anonymous namespace)::helper") == "fold_helper");
|
|
REQUIRE(makeSafeName("(anonymous namespace)::fn") == "fn");
|
|
REQUIRE(makeSafeName("a::(anonymous namespace)::b::(anonymous namespace)::c") == "a_b_c");
|
|
}
|
|
|
|
TEST_CASE("emitHarness rejects witnesses with all unsupported params", "[fuzz][harness]") {
|
|
fold::fuzz::PathWitness w;
|
|
w.function = "fn_with_ast_types";
|
|
w.finding_id = "test:file:1:1";
|
|
fold::fuzz::ParamWitness p1;
|
|
p1.kind = fold::fuzz::WitnessKind::Unsupported;
|
|
p1.cpp_type = "clang::BinaryOperator*";
|
|
w.params.push_back(p1);
|
|
fold::fuzz::ParamWitness p2;
|
|
p2.kind = fold::fuzz::WitnessKind::Unsupported;
|
|
p2.cpp_type = "clang::ASTContext&";
|
|
w.params.push_back(p2);
|
|
auto art = emitHarness({w});
|
|
REQUIRE(!art.error.empty());
|
|
REQUIRE(art.error.find("unsupported") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness rejects empty witness list", "[fuzz][harness]") {
|
|
auto art = emitHarness({});
|
|
REQUIRE(!art.error.empty());
|
|
}
|
|
|
|
TEST_CASE("emitHarness rejects witnesses spanning multiple functions", "[fuzz][harness]") {
|
|
PathWitness a = twoParamIntegerWitness();
|
|
PathWitness b = twoParamIntegerWitness();
|
|
b.function = "other::name";
|
|
auto art = emitHarness({a, b});
|
|
REQUIRE(art.error == "witnesses span multiple functions");
|
|
}
|
|
|
|
TEST_CASE("emitHarness computes total_input_bytes from slot sizes", "[fuzz][harness]") {
|
|
auto art = emitHarness({twoParamIntegerWitness()});
|
|
REQUIRE(art.error.empty());
|
|
REQUIRE(art.total_input_bytes == 5); // 4 (int32) + 1 (uint8)
|
|
REQUIRE(art.safe_name == "ns_target");
|
|
REQUIRE_FALSE(art.partial);
|
|
}
|
|
|
|
TEST_CASE("emitHarness writes nested-namespace forward declaration", "[fuzz][harness]") {
|
|
auto art = emitHarness({twoParamIntegerWitness()});
|
|
REQUIRE(art.harness_source.find("namespace ns {") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("void target(int, unsigned char);") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness emits LLVMFuzzerTestOneInput with size-gate", "[fuzz][harness]") {
|
|
auto art = emitHarness({twoParamIntegerWitness()});
|
|
REQUIRE(art.harness_source.find("extern \"C\" int LLVMFuzzerTestOneInput") !=
|
|
std::string::npos);
|
|
REQUIRE(art.harness_source.find("if (size < 5u) return 0;") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness packs integer parameters via memcpy at correct offsets", "[fuzz][harness]") {
|
|
auto art = emitHarness({twoParamIntegerWitness()});
|
|
REQUIRE(art.harness_source.find("std::memcpy(&arg0, data + 0, 4);") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("std::memcpy(&arg1, data + 4, 1);") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("ns::target(arg0, arg1);") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness heap-allocates and frees buffers for pointer params",
|
|
"[fuzz][harness][pointer]") {
|
|
auto art = emitHarness({pointerWitness()});
|
|
REQUIRE(art.error.empty());
|
|
REQUIRE(art.total_input_bytes == 16);
|
|
|
|
REQUIRE(art.harness_source.find("new unsigned char[16]") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("reinterpret_cast<char*>") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("delete[] arg0_buf;") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness null-pointer shape passes nullptr", "[fuzz][harness][pointer]") {
|
|
PathWitness w = pointerWitness();
|
|
w.params[0].pointer.shape = PointerShape::ConcreteNull;
|
|
w.params[0].pointer.buffer_bytes = 0;
|
|
auto art = emitHarness({w});
|
|
REQUIRE(art.total_input_bytes == 0);
|
|
REQUIRE(art.harness_source.find("char* arg0 = nullptr;") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("new unsigned char") == std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness schema_json includes per-parameter byte offsets",
|
|
"[fuzz][harness][schema]") {
|
|
auto art = emitHarness({twoParamIntegerWitness()});
|
|
REQUIRE(art.schema_json.find("\"function\": \"ns::target\"") != std::string::npos);
|
|
REQUIRE(art.schema_json.find("\"total_bytes\": 5") != std::string::npos);
|
|
REQUIRE(art.schema_json.find("\"byte_offset\": 0") != std::string::npos);
|
|
REQUIRE(art.schema_json.find("\"byte_offset\": 4") != std::string::npos);
|
|
REQUIRE(art.schema_json.find("\"kind\": \"integer\"") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness schema_json records partial flag", "[fuzz][harness][schema]") {
|
|
PathWitness w = twoParamIntegerWitness();
|
|
w.partial = true;
|
|
auto art = emitHarness({w});
|
|
REQUIRE(art.schema_json.find("\"partial\": true") != std::string::npos);
|
|
REQUIRE(art.partial);
|
|
}
|
|
|
|
TEST_CASE("emitHarness schema_json records pointer shape", "[fuzz][harness][schema][pointer]") {
|
|
auto art = emitHarness({pointerWitness()});
|
|
REQUIRE(art.schema_json.find("\"kind\": \"pointer\"") != std::string::npos);
|
|
REQUIRE(art.schema_json.find("\"pointer_shape\": \"nullable_buffer\"") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness wraps the forward decl in extern \"C\" for C-linkage targets",
|
|
"[fuzz][harness][c_linkage]") {
|
|
PathWitness w = twoParamIntegerWitness();
|
|
w.function = "leaky"; // simple C-style name
|
|
w.is_c_linkage = true;
|
|
auto art = emitHarness({w});
|
|
REQUIRE(art.error.empty());
|
|
// extern "C" wrapper, no namespace blocks
|
|
REQUIRE(art.harness_source.find("extern \"C\" {") != std::string::npos);
|
|
REQUIRE(art.harness_source.find("namespace ") == std::string::npos);
|
|
REQUIRE(art.harness_source.find("void leaky(int, unsigned char);") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness honors c_linkage in schema_json", "[fuzz][harness][schema][c_linkage]") {
|
|
PathWitness w = twoParamIntegerWitness();
|
|
w.is_c_linkage = true;
|
|
auto art = emitHarness({w});
|
|
REQUIRE(art.schema_json.find("\"c_linkage\": true") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitHarness defaults c_linkage to false (C++ path)",
|
|
"[fuzz][harness][schema][c_linkage]") {
|
|
auto art = emitHarness({twoParamIntegerWitness()});
|
|
REQUIRE(art.schema_json.find("\"c_linkage\": false") != std::string::npos);
|
|
// C++ path still emits the namespace blocks
|
|
REQUIRE(art.harness_source.find("namespace ns {") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitBinaryHarness generates valid dlopen C code", "[fuzz][harness][binary]") {
|
|
auto harness = fold::fuzz::emitBinaryHarness("/tmp/test.dylib", "target_fn", 3);
|
|
REQUIRE(harness.find("dlopen") != std::string::npos);
|
|
REQUIRE(harness.find("dlsym") != std::string::npos);
|
|
REQUIRE(harness.find("target_fn") != std::string::npos);
|
|
REQUIRE(harness.find("arg0") != std::string::npos);
|
|
REQUIRE(harness.find("arg1") != std::string::npos);
|
|
REQUIRE(harness.find("arg2") != std::string::npos);
|
|
REQUIRE(harness.find("LLVMFuzzerTestOneInput") != std::string::npos);
|
|
REQUIRE(harness.find("__attribute__((constructor))") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("emitBinaryHarness zero-param produces valid skeleton", "[fuzz][harness][binary]") {
|
|
auto harness = fold::fuzz::emitBinaryHarness("/tmp/lib.dylib", "init", 0);
|
|
REQUIRE(harness.find("dlopen") != std::string::npos);
|
|
}
|