
test-fuzz is a Cargo subcommand plus macro suite from Trail of Bits that automates corpus generation and harness construction for afl.rs, letting authorized developers fuzz Rust functions through ordinary cargo test runs.
| Tool | trailofbits/test-fuzz — Cargo subcommand and Rust macros that automate fuzzing tasks with afl.rs |
| Category | Fuzzing / developer security tooling |
| Primary Use | Generating fuzzing corpora and implementing harnesses for Rust functions annotated with #[test_fuzz], driven by cargo test and cargo test-fuzz |
| Safe Use | Intended for developers and authorized security researchers hardening their own Rust codebases in local or CI environments; purely defensive quality-assurance tooling |
| Telemetry Note | Leaves corpus files on disk under the target crate, writes no network traffic itself; the enable_in_production option can fill disks and is observable as unusual file-write volume |
test-fuzz is Trail of Bits' answer to the most tedious part of fuzzing Rust code: writing harnesses and seeding corpora. It ships as a pair of Cargo components — the test_fuzz and test_fuzz_impl proc macros plus the cargo test-fuzz subcommand — and it leans on afl.rs as the underlying fuzzing engine. The repo currently sits at 208 stars, is written in Rust, and carries an AGPL-3.0 license, which matters if you plan to embed it in a proprietary build pipeline.
The architectural insight behind the tool is its reuse of Rust's existing testing infrastructure. Instead of asking you to write a separate fuzz target binary, test-fuzz instruments your annotated function so that every call during a normal cargo test run serializes its arguments into a corpus file. The instrumentation is guarded by #[cfg(test)], so production builds remain untouched unless you explicitly opt out. The harness itself is injected as an additional test inside the cargo-test-generated binary, which is exactly why the project is named test-fuzz.
The workflow is deliberately three steps. First, you add serde and test-fuzz to the target crate's Cargo.toml and precede the function of interest with #[test_fuzz::test_fuzz]. Second, you run cargo test, which populates the corpus from real call sites in your test suite. Third, cargo test-fuzz foo hands that corpus to afl.rs and starts mutating. Installation is a single line — cargo install cargo-test-fuzz cargo-afl — and after a reboot you may need cargo afl system-config, which the README notes runs sudo internally to configure the system for AFL-style fuzzing.
Because targets are recorded via serde serialization, generic functions need special handling, and the macro's argument surface reflects that. The generic_args = "parameters" option pins the type parameters used during fuzzing — for example, forcing String for a target generic over T: Clone + Debug + Serialize — while impl_generic_args does the same for the Self type of methods inside impl blocks. There is a useful discovery mode here: only_generic_args records which instantiations your tests actually exercise, and cargo test-fuzz --display generic-args surfaces them as candidates, though the README is careful to note these results are merely suggestive since serializability at one instantiation does not imply deserializability at another.
Difficult types get two escape hatches. The convert = "X, Y" option routes serialization through an intermediate type Y implementing From<X> or the non-standard test_fuzz::FromRef<X> trait, with a corresponding test_fuzz::Into<X> for the return trip; the custom trait exists to dodge blanket-impl conflicts with std::convert::Into. Alternatively, Serde field attributes can be applied directly to function arguments — the README's example applies #[serde(skip)] to a Context struct containing a Mutex, which cannot derive Serialize, letting it default-construct on deserialization while the other arguments round-trip normally.
The test_fuzz_impl macro exists purely to work around a Rust language constraint: macro expansion adds a module to the enclosing scope, and modules cannot appear inside impl blocks. Any impl containing a #[test_fuzz] method must be preceded by #[test_fuzz_impl], which relocates the generated module outside the block. If you see error: module is not supported in 'trait's or 'impl's, that is the missing-macro smell. Name collisions are handled similarly: the generated module defaults to target_fuzz__ (or path_target_fuzz__ inside an impl), and rename = "name" lets you dodge conflicts.
The cargo test-fuzz command is the operational cockpit. Beyond the obvious --list and plain-target invocation, it supports --display over corpus, crashes, hangs, generic args, and work queue, with an -instrumented suffix variant for display under instrumentation. --replay crashes re-executes found crash inputs, --consolidate folds crashes, hangs, and work queues back into the corpus for the next campaign, --coverage generates coverage reports, and --cpus <N> bounds parallelism with a default of all but one core. --backtrace and --exit-code round out CI-friendly behavior, the latter mapping time-limit expiry to exit 0.
One option deserves explicit caution: enable_in_production. It makes the instrumentation write corpus files outside of test runs, gated on the TEST_FUZZ_WRITE environment variable (with TEST_FUZZ_MANIFEST_PATH for out-of-package execution). The README carries an unambiguous warning that this can introduce a denial-of-service vector — a heavily called function with varied arguments could fill the disk. The TEST_FUZZ_WRITE check is described as partial mitigation, not a guarantee, so treat this flag as a measured decision rather than a default.
For execute_with = "function", the tool offers a hook for targets that need environment setup: rather than calling the target directly, it constructs a closure of type FnOnce() -> R and passes it to your function, which can establish the call's context first. The README cites fuzzing Substrate externalities as the motivating case, which tells you the intended audience includes blockchain-runtime developers dealing with stateful execution environments — a domain where naive harness construction is genuinely painful.
Where this fits in an authorized workflow is straightforward: it is defensive quality infrastructure for code you own or are contracted to assess. The corpus comes from your own test suite, the fuzzing runs locally or in CI, and the output is crashes and hangs in your own functions — the raw material for patching bugs before an attacker finds them. Compared to hand-rolling afl.rs harnesses per function, the macro-driven approach scales across a codebase with minimal per-target boilerplate, and the --consolidate cycle makes long-running campaigns self-feeding. For any Rust team already invested in cargo test coverage, test-fuzz converts that existing test effort directly into fuzzing seed material, which is the kind of leverage security engineering budgets rarely get.
trailofbits/test-fuzz.Educational analysis for authorized security professionals. Use only in controlled, authorized environments.
0 comentários:
Post a Comment
Note: Only a member of this blog may post a comment.