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>
111 lines
3.9 KiB
C++
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());
|
|
}
|