fold/tests/unit/test_dict_miner.cpp
trvon 43c2e4d825
feat(fuzz): S3 — libFuzzer dictionary mining
For every Z3 model fold proved SAT, the concrete byte values it
picked for integer parameters often *are* the magic constants the
path guards on. Promoting them to a libFuzzer dictionary lets the
mutator splice those bytes into nearby inputs that would otherwise
fail the same guard.

Per libFuzzer's docs (https://llvm.org/docs/LibFuzzer.html#dictionaries),
dictionaries are the single largest mutation-quality lever for
formats with magic bytes, fixed headers, or length-prefixed records.

* dict_miner.{hpp,cpp} — mineDictionary(witnesses) -> dict-format
  string. One entry per unique non-trivial (Integer|Bool) byte
  sequence. Filters trivial values (0/1/all-FF) since S2 boundary
  derivatives + libFuzzer's mutator already cover those. Encodes
  printable ASCII verbatim, \xNN for non-printable.
* fuzz_prep_runner — emits one .dict per function under
  <prep>/dictionaries/<safe>.dict.
* FuzzRunOptions.dict_path + runner.cpp — when set, appends
  -dict=<path> to the libFuzzer argv at launch.
* fuzz_run_runner — auto-detects <prep>/dictionaries/<safe>.dict
  per function and sets ropts.dict_path. No new CLI flag.
* tests/unit/test_dict_miner.cpp — 10 cases / 12 assertions
  covering dedup, trivial filtering, ASCII passthrough, escape
  correctness for \" and \\, pointer skip, header counts.

Smoke: leaky(int*, int) with `flag == 0xAB` guard. dict contains
"\xab\x00\x00\x00"; libFuzzer crashes in 0.7s (vs 1s without
dict — gain is modest on a 1-byte guard but the architecture is
in place for the multi-byte magic-value formats where libFuzzer's
own docs cite 5-50x speedups).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:11 -04:00

111 lines
3.9 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <cstdint>
#include <string>
#include <vector>
#include "fold/fuzz/dict_miner.hpp"
#include "fold/fuzz/witness_recorder.hpp"
using fold::fuzz::mineDictionary;
using fold::fuzz::ParamWitness;
using fold::fuzz::PathWitness;
using fold::fuzz::PointerShape;
using fold::fuzz::WitnessKind;
namespace {
ParamWitness intParam(std::initializer_list<std::uint8_t> bytes) {
ParamWitness p;
p.kind = WitnessKind::Integer;
p.bit_width = static_cast<std::uint32_t>(bytes.size() * 8);
for (auto b : bytes)
p.bytes.push_back(static_cast<std::byte>(b));
return p;
}
PathWitness witnessWith(std::vector<ParamWitness> params) {
PathWitness w;
w.function = "f";
w.finding_id = "id:1";
w.params = std::move(params);
return w;
}
} // namespace
TEST_CASE("mineDictionary emits one entry per non-trivial witness value", "[fuzz][dict]") {
auto w = witnessWith({intParam({0xAB, 0x00, 0x00, 0x00})});
const auto dict = mineDictionary({w});
REQUIRE(!dict.empty());
REQUIRE(dict.find("\"\\xab\\x00\\x00\\x00\"") != std::string::npos);
}
TEST_CASE("mineDictionary dedups identical byte sequences across witnesses", "[fuzz][dict]") {
// 0xAB is non-printable so the encoded form uses `\xab` escapes,
// which keeps the needle unambiguous.
auto w1 = witnessWith({intParam({0xAB, 0x12, 0x00, 0x00})});
auto w2 = witnessWith({intParam({0xAB, 0x12, 0x00, 0x00})});
const auto dict = mineDictionary({w1, w2});
// Count occurrences of the encoded form. Two witnesses with the
// same value should still produce only one line.
std::size_t count = 0;
std::string needle = "\"\\xab\\x12\\x00\\x00\"";
for (std::size_t pos = 0; (pos = dict.find(needle, pos)) != std::string::npos;
pos += needle.size())
++count;
REQUIRE(count == 1);
}
TEST_CASE("mineDictionary filters trivial all-zero values", "[fuzz][dict]") {
auto w = witnessWith({intParam({0, 0, 0, 0})});
REQUIRE(mineDictionary({w}).empty());
}
TEST_CASE("mineDictionary filters trivial all-FF values", "[fuzz][dict]") {
auto w = witnessWith({intParam({0xFF, 0xFF, 0xFF, 0xFF})});
REQUIRE(mineDictionary({w}).empty());
}
TEST_CASE("mineDictionary filters single-byte 0x01", "[fuzz][dict]") {
auto w = witnessWith({intParam({0x01})});
REQUIRE(mineDictionary({w}).empty());
}
TEST_CASE("mineDictionary emits ASCII bytes literally", "[fuzz][dict]") {
// "GET " — 4 ASCII printable bytes.
auto w = witnessWith({intParam({'G', 'E', 'T', ' '})});
const auto dict = mineDictionary({w});
REQUIRE(dict.find("\"GET \"") != std::string::npos);
}
TEST_CASE("mineDictionary escapes \" and \\ properly", "[fuzz][dict]") {
// A "magic value" whose middle bytes are a quote and a backslash.
auto w = witnessWith({intParam({'a', '"', '\\', 'z'})});
const auto dict = mineDictionary({w});
REQUIRE(dict.find("\"a\\\"\\\\z\"") != std::string::npos);
}
TEST_CASE("mineDictionary skips pointer params", "[fuzz][dict]") {
ParamWitness p;
p.kind = WitnessKind::Pointer;
p.pointer.shape = PointerShape::NullableBuffer;
p.pointer.buffer_bytes = 64;
auto w = witnessWith({p});
REQUIRE(mineDictionary({w}).empty());
}
TEST_CASE("mineDictionary header reports unique count and witness count", "[fuzz][dict]") {
auto w1 = witnessWith({intParam({0xAA, 0xBB, 0xCC, 0xDD})});
auto w2 = witnessWith({intParam({0x11, 0x22, 0x33, 0x44})});
auto w3 = witnessWith({intParam({0xAA, 0xBB, 0xCC, 0xDD})}); // dup of w1
const auto dict = mineDictionary({w1, w2, w3});
REQUIRE(dict.find("2 unique value(s)") != std::string::npos);
REQUIRE(dict.find("from 3 witness(es)") != std::string::npos);
}
TEST_CASE("mineDictionary returns empty when nothing remains after filtering", "[fuzz][dict]") {
// All trivial values.
auto w = witnessWith({intParam({0}), intParam({1}), intParam({0xFF, 0xFF})});
REQUIRE(mineDictionary({w}).empty());
}