Skip to content

Fuzz targets

These targets fuzz every untrusted manifest decoder in awc-manifest:

Target Decoder under test
decode_canonical awc_manifest::decode (strict canonical CBOR)
parse_file FileManifest::from_canonical_bytes
parse_tree TreeManifest::from_canonical_bytes
parse_snapshot SnapshotManifest::from_canonical_bytes

The targets assert two invariants:

  • decode_canonical requires byte-exact round-trips: any input the strict CBOR decoder accepts must be a fixed point of encode ∘ decode, because canonicality is the whole contract at that layer. The decoder also caps container nesting (128 levels); a hostile deeply-nested input is rejected with NestingTooDeep instead of overflowing the stack.
  • The three manifest parsers require a stable canonical form: parse the input, encode it, parse that again, and both the value and its encoding must be unchanged. The original input need not survive byte-for-byte — unknown fields are ignored and absent optional fields default, by design (spec §21.8 forward compatibility). What must hold is that one pass through parse ∘ encode reaches a fixed point with no information the decoder accepted being lost.

Malformed inputs (non-UTF-8 names, empty or oversized objects, wrong kinds via graph, non-contiguous chunks, invalid digests, unknown enum values, runaway nesting) must be rejected rather than panic.

See on-disk format for the canonical encoding these targets exercise.

Seed corpus

Each target starts from a committed seed corpus in fuzz/seeds/<target>/. The seeds are valid manifests and canonical values produced by the crate's own constructors, plus the structured golden vectors hex-decoded from tests/manifests/golden_vectors.json. Regenerate them with the stable toolchain from the repository root:

cargo run -p awc-manifest --example gen_fuzz_seeds

The generator is deterministic: re-running it must leave git status clean. The workspace test suite guards the corpus through crates/awc-manifest/tests/fuzz_seeds.rs, so a seed that stops decoding fails make test before any fuzz run.

fuzz/corpus/<target>/ is the writable directory libFuzzer fills during a run. It is git-ignored; fuzz/seeds/ is committed.

Scheduled runs

The fuzz workflow runs every target weekly (Monday 04:17 UTC) and on manual dispatch. The nightly toolchain is date-pinned, cargo-fuzz is installed from the stable toolchain, and a dispatch run accepts a max_total_time input (default 300 seconds per target). On failure the job uploads the crash artifacts and the grown corpus as a workflow artifact.

Why this is a separate workspace

The fuzz crate uses libfuzzer-sys, which requires the nightly toolchain and the sanitizer runtime. It is its own Cargo workspace (note the [workspace] table in Cargo.toml) so that cargo run from the repository root — and therefore make ci — never compiles it. The decoders it targets are otherwise fully covered by the workspace test suite. See Run the test suite.

Running

cargo +stable install cargo-fuzz --locked   # once
cd fuzz
mkdir -p corpus/decode_canonical
cargo +nightly fuzz run decode_canonical corpus/decode_canonical seeds/decode_canonical

libFuzzer writes new inputs only to the first corpus directory, so fuzz/seeds/ stays untouched. Crash artifacts land under fuzz/artifacts/ (git-ignored). A crash indicates either a non-canonical input that the decoder wrongly accepted or a panic in the decoding path. Either is a correctness bug.