fold/tests/unit/test_harness_emitter.cpp
trvon ffc7917957
feat: symbolic IR, swift backend, binary-to-fuzz pipeline, SARIF fixes
- 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
2026-05-27 15:29:14 -04:00

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);
}