The Rust Engineering Handbook / Chapter 74
Miri, Sanitizers, Fuzzing, and Unsafe Audits
Build a layered unsafe-code assurance campaign, interpret each tool's limits, and record residual risk under change control.
The release meeting stalled on one word: sound.
Every wireview job was green or explicitly unavailable, yet no participant could point to evidence that covered every safe caller, allocation history, target, and schedule. The dashboard was useful only after the team rewrote it as a ledger of bounded claims:
| Evidence | Result | What it supports | What remains open |
|---|---|---|---|
| safety-case review | two caller obligations clarified | documented range and immutability contract matches the unsafe operation | reviewer may have missed a reachable state |
| deterministic corpus | short, bad-tag, boundary values, trailing bytes pass | named parser partitions behave as specified | corpus is tiny relative to all byte strings |
| differential property | raw kernel agrees with safe oracle for generated inputs | no observed semantic divergence under exercised inputs | the oracle can share a mistake; pointer misuse may precede comparison |
| Miri | command specified; runner availability recorded separately | when run, interpreter can detect many invalid operations in exercised Rust paths | unsupported foreign code, schedules, and unexecuted paths remain |
| native sanitizers | target/toolchain jobs required where supported | instrumented executions can expose classes of native memory or thread faults | each sanitizer has platform, instrumentation, and coverage gaps |
| binding smoke tests | native caller and adapter lifecycle traces pass | packaging, symbols, statuses, and selected ownership paths compose | hostile misuse and all runtime shutdown races are not exhausted |
No row says “sound.” That word requires a universal claim: every safe caller permitted by the API preserves Rust’s safety invariants on every supported execution. Dynamic tools inspect particular executions under particular models and instrumentation. They can falsify a safety argument with one counterexample; silence does not prove the argument complete.
The operational conclusion is therefore not “tests passed.” It is: confidence in unsafe code accumulates from complementary evidence, while residual risk remains named, owned, and constrained by change control. This is the closing discipline for unsafe Rust. The safety case explains why the code should be valid; tools challenge different assumptions; governance keeps later changes from silently invalidating the argument.
Start with an inventory that is smaller than grep unsafe
An audit begins by finding every place where unchecked obligations enter or change. Search for unsafe blocks, functions, traits, implementations, extern declarations, unsafe attributes, raw pointer construction and arithmetic, MaybeUninit, unions, manual auto-trait implementations, ManuallyDrop, FFI shims, allocator boundaries, generated bindings, inline assembly, and platform intrinsics. Include build scripts and generated sources. Record dependency-provided unsafe code when it materially participates in the trust boundary.
The count of unsafe tokens is only a discovery aid. One five-line constructor can support a large safe surface; a safe-looking wrapper can make an unsound call through a dependency; generated code can contain no handwritten unsafe yet encode a mismatched ABI. Inventory by safety case, not only file and line.
For each item, capture:
- identifier, owner, source location, and introduction/change commit;
- safe callers that can reach it and exported foreign callers;
- caller obligations and implementer invariants;
- allocation, provenance, alignment, initialization, aliasing, lifetime, thread, panic, and destruction assumptions that apply;
- platforms, targets, features, profiles, panic strategies, and dependency versions;
- existing review, Miri, sanitizer, fuzz, property, differential, integration, and negative evidence;
- qualified reviewers and next review trigger;
- residual risk and containment boundary.
This inventory is not a spreadsheet graveyard. CI should fail when an unsafe site or exported unsafe function appears without an inventory entry, when an entry loses its owner, or when a high-risk site changes without the required review. A machine-generated discovery report can be compared with the curated inventory, but humans still group sites into coherent safety cases.
Risk-rank the campaign. A raw pointer used only to call a well-specified OS API is not automatically low risk, and a large block is not automatically high. Consider exposure to safe callers, hostile input, concurrency, allocation lifetime, custom data structures, FFI, platform variation, frequency of change, reviewer familiarity, and blast radius. Audit the assumptions with the greatest combination of uncertainty and consequence first.
Write the proof obligation before choosing a tool
The lab’s unsafe kernel is intentionally narrow:
/// # Safety
/// `ptr` must be non-null, aligned for `u8`, and point to `len` initialized bytes
/// in one live allocation for the call. No concurrent mutation may occur.
pub unsafe fn parse_raw(
ptr: *const u8,
len: usize,
) -> Result<u32, ParseError> {
if len < 5 {
return Err(ParseError::Short);
}
let input = unsafe { std::slice::from_raw_parts(ptr, len) };
reference_parse(input)
}
The safety comment must justify the actual from_raw_parts requirements, not merely say that length was checked. The pointer must be non-null even for cases where the parser would read nothing under the API’s chosen contract; the range must reside in one allocation, be initialized and readable, fit address-space constraints, and remain immutable for the borrow. The returned slice’s lifetime is confined to the call. The five-byte check proves only that later indexing is in bounds.
Now map assumptions to evidence. Code review examines whether the public wrapper can establish the pointer contract. Miri can challenge invalid pointer use and aliasing in executed Rust paths. AddressSanitizer can catch some native out-of-bounds and use-after-free executions. MemorySanitizer can catch some uninitialized reads in sufficiently instrumented builds. ThreadSanitizer can expose some data races. A fuzzer searches input-dependent branches. Differential testing compares observable behavior against reference_parse. Binding integration tests challenge ABI, packaging, and ownership transfer. None is a substitute for the others because they ask different questions.
The figure deliberately ends in residual risk rather than certification. Evidence can lower uncertainty and uncover defects. It cannot enumerate every future safe caller, compiler transformation, target, allocation history, or thread schedule.
Miri challenges Rust’s abstract-machine obligations
Miri interprets Rust’s mid-level representation instead of running the program as ordinary native machine code. Under its model, it can detect many forms of undefined behavior during executed paths: out-of-bounds and misaligned access, use after free, invalid values, some aliasing violations, and violations exposed by its concurrency and isolation checks. It is particularly valuable for small unsafe kernels because failures often point near the operation whose precondition was violated.
Run the narrowest tests first, then the crate suite:
cargo +nightly miri setup
cargo +nightly miri test
Miri is generally delivered through a nightly toolchain and its behavior evolves with the compiler. Record the exact nightly, target, command, environment flags, features, and test selection. A stable compiler version alone does not identify a Miri run. If the component is unavailable in a runner, record “not run: component unavailable,” not “pass.” Provision a supported job or document why the platform is outside the Miri matrix.
Strict-provenance checking makes address/provenance assumptions more visible. Prefer pointer APIs that preserve provenance and avoid integer round-trips unless an exposed-address protocol is genuinely required. Miri flags and models in this area are version-sensitive; use the current official Miri documentation for the pinned nightly rather than copying a stale incantation. When a strict-provenance run fails, determine whether the code requires an operation explicitly outside the supported model or whether the failure exposes a real assumption that the safety case omitted.
Miri explores executions, not source text. Uncalled functions and untaken branches receive no evidence. Its scheduler can vary thread interleavings, but it does not exhaust all schedules. Long-running, I/O-heavy, SIMD, inline-assembly, operating-system, and FFI paths may be unsupported or intentionally isolated. It cannot interpret arbitrary C or C++ libraries as though their behavior were Rust MIR. Extract unsafe logic into dependency-light Rust tests and separately test the native boundary.
Do not “fix” Miri by skipping the operation that matters. A conditional if cfg!(miri) { return; } may be legitimate for an unsupported system call if a model/shim test covers the invariant, but it creates a coverage hole that must appear in the campaign record. Prefer isolating platform code behind a trait, testing the Rust state machine under Miri, and exercising the real platform adapter with native instrumentation.
Miri can discover a counterexample to an aliasing argument. Passing Miri cannot establish that a contested aliasing model is the final language specification. Distinguish the interpreter’s current model from documented language guarantees and keep unsafe designs conservative under model evolution.
Sanitizers observe instrumented native executions
Sanitizers compile checks into native code, so they reach platform libraries and FFI scenarios that Miri may not. The major families answer different questions:
- AddressSanitizer (ASan) targets out-of-bounds access, use after free, and related address errors.
- MemorySanitizer (MSan) targets uses of uninitialized memory, but useful results require instrumented dependencies and runtime support.
- ThreadSanitizer (TSan) targets data races in executed schedules and has platform/runtime constraints.
- UndefinedBehaviorSanitizer (UBSan) instruments selected undefined operations, especially useful on C/C++ portions; coverage and available checks depend on the compiler and language.
Rust sanitizer support, flags, targets, and runtime linkage are toolchain-sensitive. The typical shape uses a nightly compiler and target-specific -Zsanitizer=... flags, but the release process must take commands from the current rustc documentation and pin them. Do not paste one Linux command into a macOS, Windows, cross, or stable job and call the matrix complete.
Instrument all relevant sides when possible. An ASan-instrumented Rust library loaded by an uninstrumented C++ process may need explicit runtime linkage and loader configuration. MSan reports become noisy or incomplete when dependencies return uninitialized bytes without instrumentation. TSan results depend on synchronization the runtime understands. Test the packaged call path, not only a Rust unit binary.
Run sanitizer families separately unless the toolchain explicitly supports a combination. Their instrumentation and runtimes can conflict, and each run has a different performance and memory overhead. Preserve symbols and suitable debug information so reports identify source. Disable leak checks only with a documented reason; a leak across an FFI ownership protocol is often exactly the defect under investigation.
Sanitizer findings are security-relevant until triaged. Store the report, input, binary/toolchain identity, and stack. First reproduce under the same environment. Minimize the input or schedule without erasing the failure. Decide whether the defect lies in Rust unsafe code, foreign code, declarations, runtime integration, or the harness. Add a regression at the narrowest enforceable layer and update the safety case—not only the test.
Clean sanitizer jobs do not mean the native program is free of the targeted defect. Instrumentation can miss uninstrumented modules, custom allocators, unsupported operations, races not scheduled, or paths not executed. Platform exclusions belong in residual risk, and high-risk excluded targets may require another tool or architectural containment.
Treat the harness and dashboard as production code
Assurance infrastructure can lie. A fuzz target may return before calling the parser, a Miri job may select no tests under a feature gate, sanitizer flags may apply to the Rust library but not the native caller, and a coverage report may count the safe oracle while the unsafe path is dead. Validate the validator with controlled failures.
Keep one small canary per layer where practical: a test-only invalid raw access that Miri must reject, an instrumented native fixture ASan must report, a known race for the TSan runner, and a seed that forces the fuzz harness’s semantic assertion. Canaries must be isolated from ordinary artifacts and expected to fail in a job that inverts or explicitly recognizes the result. Their purpose is to prove that instrumentation and result parsing still work, not to normalize real failures.
Record denominator facts beside results: tests selected, corpus size, executions, unique paths/edges according to the chosen fuzzer, elapsed CPU time, sanitizer-instrumented modules, target, and unsupported skips. These metrics diagnose a stalled campaign. They are not acceptance targets by themselves. Ten billion executions of a shallow harness can provide less evidence than one focused property over ownership sequences.
Quarantine is especially dangerous for unsafe evidence. A flaky concurrency test moved to an optional job removes the schedule most likely to expose a defect. First preserve logs and seeds, classify whether the flake is harness, environment, or product behavior, and place a time-bounded owner on repair. If a required tool cannot run reliably, the release decision must name the missing evidence and compensate through containment or reduced support.
Store minimized reproducers in a durable, reviewed corpus. Treat inputs as potentially sensitive: fuzzers can synthesize fragments derived from proprietary seed data, crashes can dump process memory, and sanitizer logs can contain paths or payloads. Use synthetic seeds where possible, redact reports without deleting technical value, and control access to security findings.
Measure campaign health over changes: newly reachable unsafe cases, corpus regressions replayed, time to triage, stale inventory entries, and high-risk changes merged without the specified evidence. Avoid rewarding raw unsafe-line reduction; moving unchecked logic into an opaque dependency can lower the count while increasing uncertainty. The useful outcome is a smaller, clearer set of obligations with evidence that still runs.
Fuzz the contract with an oracle
A fuzz harness is an API consumer optimized for rapid, varied calls. It should be deterministic for a given input, avoid irrelevant I/O, bound allocations and work, and convert a semantic violation into a crash or assertion. Fuzz the smallest stable surface that preserves the suspected defect class.
The lab uses a differential property:
pub fn differential_property(input: &[u8]) {
let expected = reference_parse(input);
let actual = if input.len() < 5 {
Err(ParseError::Short)
} else {
unsafe { parse_raw(input.as_ptr(), input.len()) }
};
assert_eq!(actual, expected);
}
The safe oracle and raw implementation must agree for all generated byte strings admitted by the wrapper. This is stronger than “does not crash,” because a parser can silently accept malformed values without memory unsafety. It is still bounded: both implementations may share an endian or format misunderstanding. Add independent examples from the format specification and, where possible, compare with an implementation from a different codebase or language.
Targeted properties should follow the contract:
- inputs shorter than five bytes always return
Shortand never touch an output; - the wrong tag always returns
BadTagregardless of trailing bytes; - accepted values equal big-endian decoding of bytes one through four;
- appending trailing bytes does not change the decoded first record when that is the specified grammar;
- parsing never retains the input pointer;
- repeated parse/close sequences preserve handle state and release exactly once;
- wrapper outputs remain unchanged on failure.
Seed the corpus with empty, one-byte, exact-boundary, wrong-tag, all-zero, all-one, and trailing-data cases. Add minimized regressions permanently. A good seed reaches semantic states; a huge production sample can slow mutation and trap the fuzzer in one path. Dictionaries of tags and length markers can help structured formats, while structure-aware generation may be justified when random bytes rarely pass early validation.
Fuzz coverage is a search signal, not a quality score. Line or edge coverage can reveal a dead harness or untouched parser state. It cannot show that pointer lifetimes, thread schedules, allocation reuse, or arithmetic values were exhaustively explored. Compare coverage over time under the same instrumentation and corpus; do not turn a percentage into a soundness threshold.
Set campaign budgets explicitly: wall time, parallel jobs, maximum input, timeout, resident memory, and acceptable slow-input behavior. A timeout may reveal algorithmic denial of service rather than memory unsafety. Treat out-of-memory and stack overflow according to the threat model. For a parser handling hostile data, resource failures are findings even if Rust memory safety remains intact.
Run fuzz targets under sanitizers when supported and keep a fast non-sanitized campaign for throughput. Miri is usually too slow for ordinary coverage-guided fuzz loops; minimize interesting cases and replay a focused corpus under Miri instead. This composition gives the generator breadth and the interpreter deeper checks on selected paths.
Property tests and differential tests make expectations executable
Coverage-guided fuzzing excels at discovering inputs that reach new control-flow edges. Property-based tests excel at generating structured values and shrinking counterexamples around declared invariants. Example tests excel at preserving named requirements and readable regressions. Use all three when the risk warrants it.
A targeted generator for wireview can vary tag, declared length, available bytes, alignment offset in a larger allocation, repeated operation sequence, and callback behavior. Generate valid and invalid cases intentionally so rejection paths are not left to chance. For unsafe APIs, keep the harness itself within the documented caller contract unless the objective is an isolated negative test. Calling parse_raw with an arbitrary dangling pointer does not test a promise made to safe callers; it invokes undefined behavior in the harness.
Differential testing needs independent behavior and normalized results. Comparing an optimized parser to its earlier implementation helps catch divergence, but shared utility functions reduce independence. Comparing Rust to a mature C parser adds independence while bringing FFI and sanitizer obligations. Normalize categories such as malformed input and ignore only documented diagnostic differences. When implementations disagree, the specification—not majority vote—decides.
Metamorphic properties help when no full oracle exists. Round-trip encode/decode, parse-then-reserialize stability, irrelevant-padding invariance, chunking equivalence, and deterministic repeated execution can expose contradictions. State the domain carefully: arbitrary serialization may canonicalize representation, floating values may have multiple encodings, and a streaming parser may intentionally distinguish chunk boundaries.
Audit the safe surface and the unsafe kernel together
An unsafe audit is not a line-by-line reading of blocks in isolation. Review outward from each unchecked operation and inward from every safe or foreign entry point until the obligations meet.
For parse_raw, the kernel review asks whether from_raw_parts requirements are stated and established. The safe-wrapper review asks whether a slice actually supplies those requirements. The FFI review asks whether foreign pointer and length claims can be checked, which invalid claims remain caller undefined behavior, and whether an in-process malicious caller is inside the trust model. The binding review asks whether a runtime buffer can move or mutate during the call. The packaging review asks whether declarations match the loaded library.
Use an audit checklist as a prompt for reasoning, not a substitute for it:
- Validity and initialization: can any typed value be constructed from invalid or uninitialized bits?
- Bounds and arithmetic: are offsets, lengths, capacities, layouts, and integer conversions checked before use?
- Provenance and alignment: does each pointer originate from a suitable live allocation and remain correctly aligned?
- Aliasing and mutation: can shared and mutable access overlap, including through callbacks or foreign threads?
- Lifetime: can a reference, slice, view, callback, or handle outlive its owner?
- Ownership and drop: is allocation paired with exactly one compatible release on success, failure, panic, cancellation, and close?
- Concurrency: are manual
Send/Sync, atomics, locks, and thread-affine resources justified as one composed proof? - Panic and reentrancy: are invariants restored before unwind, and can callbacks reenter or destroy observed state?
- Representation and ABI: are layout, discriminants, calling convention, symbols, and exceptions correct for every supported target?
- Maintenance: which safe API, representation, dependency, compiler, target, or feature change invalidates the argument?
Reviewers need qualifications proportional to the risk. At least one reviewer must understand Rust validity, aliasing, ownership, and the specific unsafe primitives. FFI work also needs the foreign ABI/runtime and build chain. Concurrent unsafe code needs memory-model and synchronization expertise. Domain-specific parsing may need format/security knowledge. A large team vote does not replace one reviewer capable of finding a false safety premise.
Independence matters. The author can prepare the safety case and tools, but a high-risk audit should include a reviewer who did not design the abstraction. Record disagreements and unresolved assumptions. External review is valuable for cryptography, custom allocators, lock-free structures, sandbox boundaries, and other high-consequence kernels, but the organization still owns remediation and change control.
Turn findings into governed changes
Classify findings by violated contract and reachability, not by which tool found them. A Miri aliasing failure reachable through safe code is a soundness defect. An ASan report requiring a foreign caller to violate a documented unsafe precondition may still justify hardening or process isolation, but it is a different API claim. A fuzzer timeout on hostile input is an availability/security defect. A mismatched generated declaration is an ABI defect that can become memory corruption.
For each finding, record:
- exact reproducer, environment, tool version, and artifact hash;
- violated invariant and reachable callers;
- severity, exploitability assumptions, and affected versions/targets;
- containment, fix, and regression evidence;
- whether the public safety contract or compatibility promise changes;
- downstream notification, advisory, or release action when required;
- residual risk after remediation.
Do not delete the minimized input after fixing it. Add it to unit/property corpora and, when appropriate, Miri and sanitizer replay suites. A regression test guards the observed manifestation; the revised safety case guards the defect class.
Change control should trigger focused re-audit when unsafe code changes, but also when its assumptions change: new safe methods, representation changes, generic bounds, manual auto traits, panic strategy, allocator, callback timing, FFI header, generator, dependency, compiler/edition, target, feature, optimization profile, or supported runtime. A diff with no modified unsafe token can still invalidate the proof.
Require an audit delta in review:
safety cases affected:
obligations added/removed:
new reachable callers:
platform or feature matrix change:
evidence rerun:
residual risk decision:
qualified approvers:
Protect tool configuration like code. A CI job that silently stops running on a renamed target produces false reassurance. Pin nightly dates where needed, monitor failures to install components, retain logs, and make required jobs non-optional for the supported matrix. Scheduled longer fuzz campaigns complement per-change smoke budgets; they do not excuse a broken pull-request harness.
The wireview audit campaign
Run the campaign as a sequence with artifacts at every transition.
1. Scope and inventory
Scope the Rust parser kernel, WireviewHandle lifecycle, C exports, header, C smoke caller, and one representative managed adapter. Inventory raw-slice construction, Box::into_raw/from_raw, callback invocation, manual Send/Sync nearby, C layout, no-mangle exports, panic containment, and generated declarations. Record supported Linux target/compiler/runtime versions and mark other targets unassessed rather than implicitly supported.
2. Contract review
Trace allocation to release, input borrow to call return, output initialization to status, callback registration to drain, and panic to translated error. For every unsafe block, verify the preceding checks establish every library precondition. Red-team null/nonzero length, zero length, usize::MAX, wrong tag, overlapping output/input, stale handle, double free, callback-triggered close, concurrent parse, callback panic/exception, and loader version mismatch. Classify each as safely rejected, defined, outside the safe wrapper, or unresolved.
3. Harness portfolio
Keep readable corpus cases for protocol partitions. Run the safe/raw differential property over generated bytes. Add operation-sequence properties for new/parse/visit/free. Compile and run the C caller against the produced library. Add runtime-specific close and exception tests. Isolate intentional invalid foreign misuse in subprocess/sanitizer jobs so it cannot corrupt the main test process.
4. Execution matrix
Run stable formatting, checks, tests, doctests, and Clippy at the declared Rust snapshot and MSRV. Run Miri on the dependency-light Rust paths using a recorded nightly. Run ASan on Rust plus native caller where supported, TSan on concurrent handle/callback tests, and MSan only with a sufficiently instrumented environment. Run a bounded per-change fuzz smoke and a longer scheduled campaign. Replay all minimized findings across relevant layers.
5. Triage and residual risk
Suppose all deterministic and differential tests pass, no dynamic tool reports a defect, but the Windows loader matrix, Python free-threaded runtime, callback-after-unregister stress schedule, and malicious in-process caller remain unassessed. The release record must list those gaps. It may restrict supported targets, disable asynchronous callbacks, require process isolation for untrusted plugins, and schedule qualified follow-up. “Green dashboard” is not an acceptable substitute.
6. Change policy
Protect the inventory check, generator drift check, symbol allowlist, safe/raw differential harness, C smoke caller, Miri replay set, sanitizer matrix, and fuzz corpus. Require an unsafe-audit owner for changes to handle representation, callback threading, parser storage, ABI, generator, or runtime support. Set a periodic review date for evolving provenance and tool models.
The campaign deliverable is a decision record with evidence links and residual risks, not a screenshot of CI. A senior reviewer should be able to reproduce the core commands, identify which claim each job supports, and see where the evidence stops.
Practical exercise: commission the audit
Prepare an audit packet for wireview containing:
- an unsafe-code inventory grouped into safety cases, with owners and reachability;
- a caller-obligation/implementer-invariant table for raw parsing, handles, callbacks, and FFI;
- a risk-ranked tool matrix mapping each assumption to review, Miri, sanitizer, fuzz/property, differential, and integration evidence;
- a deterministic corpus plus at least five targeted properties and one independent differential or metamorphic oracle;
- exact pinned commands, targets, features, budgets, artifact retention, and unavailable-tool notation;
- one triaged hypothetical finding from reproducer through revised safety case and regression;
- reviewer qualifications and independence requirements;
- a residual-risk decision and change-trigger policy.
Then introduce three changes separately: retain the input pointer in the handle, make callbacks asynchronous, and replace a copied status payload with a borrowed foreign view. Do not begin by running tools. First identify which proof obligations changed, which existing evidence became irrelevant, and which new schedules or lifetimes must be generated. The exercise succeeds when the audit delta predicts the risky evidence gaps before a tool discovers them.
Use this final review card:
- Is every unsafe operation attached to a named safety case and reachable safe surface?
- Does the prose state all pointer, validity, aliasing, lifetime, panic, thread, and drop obligations?
- Does each tool job name what it can falsify and what it cannot establish?
- Are Miri/nightly, sanitizer target, fuzzer, dependencies, features, and artifact hashes pinned?
- Do fuzz targets have semantic oracles, bounded resources, useful seeds, and permanent regressions?
- Is coverage used to improve search rather than certify safety?
- Are native/managed adapters and packaged artifacts represented in integration evidence?
- Are findings classified by violated contract and reachability?
- Are reviewers qualified for Rust plus the foreign/runtime/domain risks involved?
- Do safe-surface, dependency, compiler, target, and packaging changes trigger audit deltas?
- Are unsupported targets and unexecuted tools explicit residual risks?
Unsafe Rust becomes maintainable in production when the proof obligation is compact, its challengers are reproducible, and future changes cannot bypass the record unnoticed. Miri, sanitizers, fuzzers, properties, differential implementations, and human audits are most powerful as an argument under pressure—not as independent badges.
Sources and version notes
The boundary-assurance-lab targets Rust 2024, declares Rust 1.85 as its MSRV, and uses a dependency-free safe oracle plus unsafe kernel. Core sources are the official Miri repository and documentation, the rustc book’s current sanitizer documentation, the Rust Reference on behavior considered undefined, and standard documentation for slice::from_raw_parts. Miri and sanitizer commands are toolchain- and target-sensitive; pin and revalidate them rather than treating the examples above as timeless stable interfaces. Fuzzer behavior and coverage instrumentation are tool-specific and should be recorded with the selected implementation and version.
Continue reading
Full table of contents