Rust projects are built in one of two ways:
- Native harness builds (
--lang libfuzzer,ziggy,afl) run the fuzzer's own build command. - Plain cargo builds (
--lang rust, and the Rust half ofmixed) runcargo build.
Both add --emit=llvm-bc to every rustc call and collect one bitcode file per
crate and codegen unit. rustc's LLVM must not be newer than the analyzer's;
see llvm-support.md. With --capture rllvm, both instead
take the bitcode from the linked artifact; see
Capturing with rllvm.
The outputs on this page come from reachability 1.2-dev (commit f5adeab)
with LLVM 23 and rustc 1.99.0-nightly (2026-07-12, LLVM 22.1.8).
--lang |
Build command | Clean command afterwards |
|---|---|---|
libfuzzer |
cargo fuzz build |
cargo clean in fuzz/ |
ziggy |
cargo ziggy build --no-honggfuzz |
cargo ziggy clean |
afl |
cargo afl build |
cargo afl clean |
The driver sets RUSTC_WRAPPER to a small script. The script adds
--emit=llvm-bc to every rustc call and copies each crate's bitcode into a
private directory. The bitcode therefore has the same cfg(fuzzing), features
and instrumentation as the binary you fuzz, and it does not matter where the
fuzzer writes its output. Build scripts and proc-macro crates are skipped.
--profile releaseadds--releaseto the command.--build-cmdreplaces the command, for example to pick features or one binary:--build-cmd 'cargo afl build --features afl --bin afl_runner'.--codegen-unitsand--build-stddo not apply; the harness build decides.
Source-faithful by default. The wrapper also adds -Copt-level=0, so no
function is inlined away and the report matches what llvm-cov reports.
Because that build is not the one you want to fuzz, the driver runs the clean
command afterwards, and your next fuzzing build compiles from scratch.
--optimize keeps the harness's own optimization and skips the clean.
AFL++ plugins. cargo-afl and ziggy builds run with
AFLRS_REQUIRE_PLUGINS=1. Without the AFL++ LLVM plugins they then fail
instead of quietly building with weaker instrumentation that does not match
your fuzzer. The plugins are built per rustc version, so after a nightly update
run cargo afl config --build --plugins --force again, with an llvm-config
that matches rustc's LLVM (for example LLVM_CONFIG=llvm-config-22).
Cached builds. A build that compiles nothing runs rustc for no crate, so
there is no bitcode and the run fails with "the build was CACHED". Use
--clean.
fixtures/rust_indirect is a ziggy harness:
$ reachability run --lang ziggy --project fixtures/rust_indirect
note: added 1 root(s) referenced by name (dlsym/dlopen-by-name reachability): dlsym_resolved_target
rust bitcode (native build): 8 crate modules
reachable 1122 / defined 2022 (191 indirect-only, 47 low-confidence, 900 unreachable) [backend=type-based]
Its entries are main, the Rust main _RNvCsTPCUaflBzq_25rust_ziggy_indirect_calls4main
and the name-lookup root dlsym_resolved_target.
Tests: test_cli.py::test_native_build_cmd_defaults,
::test_native_build_cmd_release, ::test_native_clean_uses_harness_command,
::test_native_clean_libfuzzer_targets_fuzz_dir,
::test_acquire_native_cleans_when_not_optimize,
::test_acquire_native_skips_clean_when_optimize,
::test_run_rust_indirect_fixture, and in test_acquire_rust.py
::test_native_sets_opt0_env_by_default, ::test_native_optimize_clears_opt0_env
and the ::test_bc_wrapper_* tests.
For --lang rust and mixed the driver runs
cargo build --message-format=json-render-diagnostics [--release] [-Zbuild-std --target <host>]with these rustc flags in CARGO_ENCODED_RUSTFLAGS:
--emit=llvm-bc -Cembed-bitcode=yes -Ccodegen-units=N;- your own flags:
CARGO_ENCODED_RUSTFLAGSorRUSTFLAGSif set, otherwisebuild.rustflagsfrom the cargo config files, merged from$CARGO_HOMEdown to the project the way cargo does it; - without
--optimize:-Copt-level=0, and-Cdebug-assertionsand-Coverflow-checksset to the profile's values; - the mangling flags.
Step 2 keeps project flags such as --cfg tokio_unstable, which a plain
RUSTFLAGS=… would replace. Step 3 is needed because rustc turns debug
assertions on at opt-level 0, which would change which functions are compiled.
The bitcode list comes from cargo's messages about this build, not from a
search of the target directory, so files from earlier builds are never picked
up. Up to cargo 1.99 the files are in target/<profile>/deps/; from cargo
1.100 each unit has its own target/<profile>/build/<pkg>/<hash>/out/. Both
are searched, and CARGO_TARGET_DIR is honoured.
The final link may fail under --emit=llvm-bc; that is expected and ignored.
A compile error in any crate stops the run, because the bitcode would be
incomplete. A build that compiles nothing prints a warning that the bitcode may
be stale; use --clean.
Tests in test_acquire_rust.py: ::test_compose_rustflags_merges_project_config,
::test_compose_rustflags_keeps_caller_env, ::test_config_rustflags_merges_hierarchy,
::test_compose_rustflags_appends_opt0_by_default,
::test_compose_rustflags_opt0_wins_over_inherited,
::test_compose_rustflags_pins_release_assertions_off,
::test_compose_rustflags_pins_debug_assertions_on,
::test_compile_errors_flags_real_failure_not_link,
::test_failed_cargo_never_uses_stale_bitcode,
::test_build_bc_paths_finds_per_unit_output_directory and
::test_target_dir_honors_environment; also
test_reachable_sets.py::test_rust_flag_switch_without_clean_uses_fresh_bitcode.
Set these to match the fuzz binary's build. Both only apply to plain cargo builds.
--profile {debug,release} picks the cargo profile; the default is
debug, like a bare cargo build. Without --optimize the opt level is
always 0, and the profile decides debug_assertions, overflow_checks and the
profile's other settings. With --optimize it also decides the opt level,
which turns rustc's generic sharing on (unoptimized) or off (optimized). That
decides which crate instantiates each generic, and so which instances exist and
what they are called.
--codegen-units N is passed to rustc as -Ccodegen-units. With
--optimize the units set the inlining boundaries, so they decide which
instances survive as functions. When you leave it out, the driver uses:
codegen-unitsfrom[profile.<name>]inCargo.toml, searched from--projectup to the workspace root (<name>isdevfordebug);- otherwise cargo's default: 256 for
debug, 16 forrelease.
So you usually only set --profile. Pass --codegen-units when the fuzz build
uses a value that is in neither place, for example one set with
-Ccodegen-units in RUSTFLAGS. -v prints the values used, such as
profile=debug, codegen-units=256.
Tests in test_acquire_rust.py: ::test_resolve_codegen_units_explicit_wins,
::test_resolve_codegen_units_from_manifest,
::test_resolve_codegen_units_cargo_defaults,
::test_manifest_codegen_units_walks_up_to_workspace_root and
::test_workspace_root_profiles_override_member_profiles; also
test_reachable_sets.py::test_workspace_profile_semantics_compile_time.
Without --build-std the standard library comes precompiled, so its functions
appear as external_declarations. --build-std builds it from source with
-Zbuild-std --target <host>, so std functions become part of the graph. It
needs a nightly rustc and the rust-src component, and only applies to plain
cargo builds. The added --target changes v0 names, see
below.
Test: test_acquire_rust.py::test_build_std_is_only_a_cargo_option.
rustc has two symbol-mangling schemes. Legacy names look like
_ZN8url_fuzz4main17h58c435803ec45a52E, v0 names like
_RNvCscjF3EmfBC9s_8url_fuzz4main. Legacy and v0 names of the same function
share nothing, so the report only joins with a build that uses the same scheme.
--mangling sets the scheme of the analysis build, for every Rust --lang:
| Value | Effect |
|---|---|
auto |
Adds nothing; rustc's default applies. That is v0 on rustc 1.99, stable and nightly, and legacy on older versions such as 1.88. |
v0 |
Adds -Csymbol-mangling-version=v0 |
legacy |
Adds -Zunstable-options -Csymbol-mangling-version=legacy; needs a nightly rustc |
Pick the scheme of the build you join against:
- cargo-afl, ziggy and cargo-fuzz binaries use the toolchain default, so
automatches them when both use the same toolchain; - a
-Cinstrument-coveragebuild is always v0, so use--mangling v0.
The report's top-level mangling field says which scheme the bitcode has.
How the build-dependent end of a Rust name is handled is described in
output.md.
Tests: in test_acquire_rust.py the ::test_compose_rustflags_mangling_* and
::test_native_carries_mangling_* tests; in test_v0_mangling_probe.py
::test_v0_flag_produces_v0_symbols, ::test_instrument_coverage_matches_explicit_v0
and ::test_v0_stable_across_opt_levels.
key and the * in the lists only cover the build-dependent end of a Rust
name. Two more things must match:
- The set of functions. A wildcard cannot bring back a function that one
build inlined away and the other kept. Match
--profileand--codegen-unitsfor plain cargo builds; native builds use the harness's own build and match it already. - The crate disambiguators (v0 only). A v0 name contains every crate with
a
Cs<hash>_disambiguator, which rustc derives from cargo's-C metadata. The profile, the features and whether cargo gets--targetchange it; extraRUSTFLAGSdo not. For example,dep::dep_checksumstarts with_RNvCs5zheXYwTW8r_3dep…in a dev build,_RNvCsiqyxleFSJ0j_3dep…with--release, and_RNvCs8ybOMRUuePv_3dep…with--target x86_64-unknown-linux-gnu. Legacy names fold the same value into the hash thatkeydrops, so legacy joins do not depend on it.
So for v0: native builds match automatically. Plain --lang rust builds
without --target (unless --build-std adds it), so pass the fuzz build's
--profile, and analyze a fuzz binary whose build passes --target
(cargo-fuzz always does) through its native route. When names still do not
match, a consumer with source locations can fall back to file and line, as
cov-analysis does.
--entry main finds both the C main that starts the Rust runtime and the
real Rust main, so you never type a mangled name. A staticlib harness has
no main; pass --entry LLVMFuzzerTestOneInput. fuzz_target! means
LLVMFuzzerTestOneInput and rust_fuzzer_test_input.
Tests: test_analyzer_core.py::test_rust_entry_rooting,
::test_entry_main_resolves_rust_and_c, ::test_entry_fuzz_target_alias, and
test_reachable_sets.py::test_rust_main_entry and ::test_rust_only_entry_rooting.