Build the static-analysis-driven fuzzing pipeline alongside the radare2
+ z3 work. fold's symbolic engine already proves sink feasibility; this
adds the path that retains the Z3 model, lowers it to libFuzzer
harnesses + seed corpora, and writes a findings.predicted.json bridge
so a future fold fuzz-run can promote crashes to ConcreteWitness
confidence.
* fold::fuzz value types (PathWitness, ParamWitness) + WitnessRecorder
collector — process-local, lock-free hot-path check
* model_to_bytes — z3::model → little-endian std::byte at source width;
pointer null-vs-nullable disambiguation
* harness_emitter — generates .harness.cc + .schema.json; honors
is_c_linkage via `extern "C"` wrapper for C targets; nested-namespace
emission for C++
* corpus_writer — packs witness bytes per harness layout, FNV-1a hash
for filename, 3 canonical seeds (zeros / 0xFF / single byte)
* param_schema — schemaForFunction + recordSinkWitness +
recordSinkWitnessWithModel; latter looks up each parameter's z3::expr
via env->tryGet() and mines the model for byte payloads
* SafetyObserver: const SymbolicEnv* env added to onPointerDeref,
onDivision, onTruncation, onSignedUnsignedConversion,
onArraySubscript; encoder.cpp passes &env at every call site;
CompositeSafetyObserver in scan_runner fans through
* null_deref, div_by_zero, int_overflow, int_truncation,
sign_conversion now record witnesses with full model bytes;
oob_access records schema-only (sat sites don't capture a model yet)
* fold fuzz-prep subcommand — runs scan in-process (workers can't
ship recorder state), drains the recorder, emits one
{harness,schema,corpus/} bundle per function
Smoke validation: 4 detectors on a 5-line C fixture produce
non-partial witnesses whose bytes are the exact magic values that
trigger each sink (flag=0xAB for null-deref; x=0 for div-by-zero;
a+b that overflows INT_MIN for int-overflow).
Tests: 34 → 37 (add test_model_to_bytes, test_harness_emitter,
test_corpus_writer). All green.
Also folds in a small worker-toolchain optimization that had been
sitting in the working tree: preloadHostToolchainEnv() avoids
repeated popen calls during worker init.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
146 lines
5 KiB
C++
146 lines
5 KiB
C++
#include <z3++.h>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
|
|
#include "fold/fuzz/model_to_bytes.hpp"
|
|
|
|
using fold::fuzz::extractIntegerBytes;
|
|
using fold::fuzz::extractPointerWitness;
|
|
using fold::fuzz::packLittleEndian;
|
|
using fold::fuzz::ParamWitness;
|
|
using fold::fuzz::PointerShape;
|
|
using fold::fuzz::WitnessKind;
|
|
|
|
namespace {
|
|
|
|
// Make a model that binds `c` to the given concrete value, then return
|
|
// the model and the constant expression for the caller to use.
|
|
struct ModelFixture {
|
|
z3::context ctx;
|
|
z3::solver solver{ctx};
|
|
z3::expr c = ctx.bv_const("p", 64);
|
|
|
|
z3::model materialize(std::uint64_t value) {
|
|
solver.reset();
|
|
solver.add(c == ctx.bv_val(value, 64));
|
|
const auto res = solver.check();
|
|
REQUIRE(res == z3::sat);
|
|
return solver.get_model();
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("packLittleEndian: 8-bit emits one byte", "[fuzz][bytes]") {
|
|
auto out = packLittleEndian(0xABULL, 8);
|
|
REQUIRE(out.size() == 1);
|
|
REQUIRE(static_cast<std::uint8_t>(out[0]) == 0xAB);
|
|
}
|
|
|
|
TEST_CASE("packLittleEndian: 32-bit value packs in little-endian order", "[fuzz][bytes]") {
|
|
auto out = packLittleEndian(0xDEADBEEFULL, 32);
|
|
REQUIRE(out.size() == 4);
|
|
REQUIRE(static_cast<std::uint8_t>(out[0]) == 0xEF);
|
|
REQUIRE(static_cast<std::uint8_t>(out[1]) == 0xBE);
|
|
REQUIRE(static_cast<std::uint8_t>(out[2]) == 0xAD);
|
|
REQUIRE(static_cast<std::uint8_t>(out[3]) == 0xDE);
|
|
}
|
|
|
|
TEST_CASE("packLittleEndian: 64-bit full-width round-trip", "[fuzz][bytes]") {
|
|
auto out = packLittleEndian(0x1122334455667788ULL, 64);
|
|
REQUIRE(out.size() == 8);
|
|
REQUIRE(static_cast<std::uint8_t>(out[0]) == 0x88);
|
|
REQUIRE(static_cast<std::uint8_t>(out[7]) == 0x11);
|
|
}
|
|
|
|
TEST_CASE("packLittleEndian: non-byte-aligned width rounds up", "[fuzz][bytes]") {
|
|
// 9-bit value: 0x1FF (all 9 bits set). Should produce 2 bytes:
|
|
// low byte 0xFF, high byte with only bit-0 set (0x01).
|
|
auto out = packLittleEndian(0x1FFULL, 9);
|
|
REQUIRE(out.size() == 2);
|
|
REQUIRE(static_cast<std::uint8_t>(out[0]) == 0xFF);
|
|
REQUIRE(static_cast<std::uint8_t>(out[1]) == 0x01);
|
|
}
|
|
|
|
TEST_CASE("packLittleEndian: wide value masks to bit_width", "[fuzz][bytes]") {
|
|
// 32-bit width with a value whose upper 32 bits are nonzero.
|
|
// The upper bits must be dropped so the harness reads back the
|
|
// value the source-language type would actually observe.
|
|
auto out = packLittleEndian(0xFFFF'FFFF'AAAA'BBBBULL, 32);
|
|
REQUIRE(out.size() == 4);
|
|
REQUIRE(static_cast<std::uint8_t>(out[0]) == 0xBB);
|
|
REQUIRE(static_cast<std::uint8_t>(out[3]) == 0xAA);
|
|
}
|
|
|
|
TEST_CASE("packLittleEndian: zero bit_width emits empty buffer", "[fuzz][bytes]") {
|
|
auto out = packLittleEndian(0xDEADBEEFULL, 0);
|
|
REQUIRE(out.empty());
|
|
}
|
|
|
|
TEST_CASE("extractIntegerBytes: BV-64 round-trips a 32-bit value", "[fuzz][bytes][model]") {
|
|
ModelFixture fx;
|
|
auto model = fx.materialize(0xDEADBEEFULL);
|
|
|
|
ParamWitness w;
|
|
w.kind = WitnessKind::Integer;
|
|
w.bit_width = 32;
|
|
w.is_signed = false;
|
|
REQUIRE(extractIntegerBytes(model, fx.c, w));
|
|
REQUIRE(w.bytes.size() == 4);
|
|
REQUIRE(static_cast<std::uint8_t>(w.bytes[0]) == 0xEF);
|
|
REQUIRE(static_cast<std::uint8_t>(w.bytes[3]) == 0xDE);
|
|
}
|
|
|
|
TEST_CASE("extractIntegerBytes: bit_width defaults to sort width", "[fuzz][bytes][model]") {
|
|
ModelFixture fx;
|
|
auto model = fx.materialize(0x42ULL);
|
|
|
|
ParamWitness w;
|
|
w.kind = WitnessKind::Integer;
|
|
// Leave w.bit_width == 0 on purpose.
|
|
REQUIRE(extractIntegerBytes(model, fx.c, w));
|
|
REQUIRE(w.bit_width == 64);
|
|
REQUIRE(w.bytes.size() == 8);
|
|
REQUIRE(static_cast<std::uint8_t>(w.bytes[0]) == 0x42);
|
|
}
|
|
|
|
TEST_CASE("extractIntegerBytes: bool expression emits 0 or 1", "[fuzz][bytes][model]") {
|
|
z3::context ctx;
|
|
z3::solver solver(ctx);
|
|
z3::expr b = ctx.bool_const("b");
|
|
solver.add(b);
|
|
REQUIRE(solver.check() == z3::sat);
|
|
auto model = solver.get_model();
|
|
|
|
ParamWitness w;
|
|
w.kind = WitnessKind::Bool;
|
|
w.bit_width = 8;
|
|
REQUIRE(extractIntegerBytes(model, b, w));
|
|
REQUIRE(w.bytes.size() == 1);
|
|
REQUIRE(static_cast<std::uint8_t>(w.bytes[0]) == 1);
|
|
}
|
|
|
|
TEST_CASE("extractPointerWitness: zero value → ConcreteNull", "[fuzz][bytes][pointer]") {
|
|
ModelFixture fx;
|
|
auto model = fx.materialize(0);
|
|
|
|
ParamWitness w;
|
|
REQUIRE(extractPointerWitness(model, fx.c, /*default_buffer_bytes=*/64, w));
|
|
REQUIRE(w.kind == WitnessKind::Pointer);
|
|
REQUIRE(w.pointer.shape == PointerShape::ConcreteNull);
|
|
REQUIRE(w.pointer.buffer_bytes == 0);
|
|
}
|
|
|
|
TEST_CASE("extractPointerWitness: non-zero value → NullableBuffer", "[fuzz][bytes][pointer]") {
|
|
ModelFixture fx;
|
|
auto model = fx.materialize(0xDEAD'BEEFULL);
|
|
|
|
ParamWitness w;
|
|
REQUIRE(extractPointerWitness(model, fx.c, /*default_buffer_bytes=*/128, w));
|
|
REQUIRE(w.kind == WitnessKind::Pointer);
|
|
REQUIRE(w.pointer.shape == PointerShape::NullableBuffer);
|
|
REQUIRE(w.pointer.buffer_bytes == 128);
|
|
REQUIRE(w.pointer.buffer_contents.empty());
|
|
}
|