Chio/Docs
LOGIN · JOIN

PlatformTesting & Adversarial Analysis

Formal Assurance

Fuzz infrastructure

libFuzzer harnesses exercise selected parser and verifier entry points, and inventory tests keep the target list, the owner map, and the corpora in step.

Standalone workspace by design

fuzz/Cargo.toml carries an empty [workspace] stanza so libFuzzer's nightly-only runtime requirements do not leak into the main stable workspace. The main workspace denies unwrap_used and expect_used; the fuzz workspace inherits the same lints so harnesses cannot mask panics behind a hidden .unwrap().

What fuzzing does

libFuzzer is a coverage-guided in-process fuzzer. It starts from a seed corpus, executes the harness, observes which code edges the input touched, and mutates the input toward inputs that cover new edges. When an input causes a panic, an assertion violation, or an ASAN/UBSAN report, libFuzzer captures it as a crash and shrinks toward a minimal reproducer.

Chio targets trust-boundary entry points: places where externally controlled bytes first enter Chio code. The contract for each target is that no input causes a panic, an unwrap failure, or undefined behavior; structurally invalid input must result in a typed Err return.


Fuzz targets

Every target is a [[bin]] entry in fuzz/Cargo.toml backed by a fuzz_target! in fuzz/fuzz_targets/<name>.rs. Three further files name the same set: the workflow matrix in .github/workflows/fuzz.yml, the trigger globs in fuzz/target-map.toml, and the owning crate in fuzz/owners.toml as [targets.<name>] with crate and path fields. The header of fuzz/target-map.toml requires all of them to move in one change set, and fuzz/tests/smoke.rs turns that requirement into a test rather than a convention.

The table below is the target list as fuzz/owners.toml declares it. The input-boundary column is the notes field each target carries in fuzz/target-map.toml.

TargetSourceOwning crateInput boundary
attest_verifyfuzz_targets/attest_verify.rschio-attest-verifySigstore bundle parser + cert chain verify; trust-boundary
jwt_vc_verifyfuzz_targets/jwt_vc_verify.rschio-credentialsJWT VC verifier; trust-boundary; constant-time compare assertions live here
oid4vp_presentationfuzz_targets/oid4vp_presentation.rschio-credentialsOID4VP holder response decode
did_resolvefuzz_targets/did_resolve.rschio-didchio-did parser plus resolver
anchor_bundle_verifyfuzz_targets/anchor_bundle_verify.rschio-anchorAnchor proof bundle plus checkpoint records; trust-boundary
mcp_envelope_decodefuzz_targets/mcp_envelope_decode.rschio-mcp-edgeMCP NDJSON decode plus edge dispatch; pipes into evaluator post-decode
a2a_envelope_decodefuzz_targets/a2a_envelope_decode.rschio-a2a-adapterA2A SSE parse plus per-event fan-out
acp_envelope_decodefuzz_targets/acp_envelope_decode.rschio-acp-edgeACP NDJSON plus handle_jsonrpc dispatch
wasm_preinstantiate_validatefuzz_targets/wasm_preinstantiate_validate.rschio-wasm-guardsComponentBackend, WasmtimeBackend, format detect; trust-boundary
wasm_guard_escapefuzz_targets/wasm_guard_escape.rschio-wasm-guardsRuntime-execution surface; 8 escape classes via evaluate
wasm_guard_smithfuzz_targets/wasm_guard_smith.rschio-wasm-guardswasm-smith modules and components through bounded load and evaluate paths
wit_host_call_boundaryfuzz_targets/wit_host_call_boundary.rschio-wasm-guardsGuardRequest/GuestDenyResponse serde deserialization; trust-boundary
chio_yaml_parsefuzz_targets/chio_yaml_parse.rschio-configchio-config YAML loader
openapi_ingestfuzz_targets/openapi_ingest.rschio-openapi-mcp-bridgeOpenApiMcpBridge::from_spec ingest path
receipt_log_replayfuzz_targets/receipt_log_replay.rschio-kernel-coreReceipt log replay decode plus chain-invariant state-machine, including cognition-market terminal and payment paths
eval_receipt_bundlefuzz_targets/eval_receipt_bundle.rschio-eval-receiptEval-report bundle parser and fail-closed verifier
canonical_jsonfuzz_targets/canonical_json.rschio-core-typesRound-trips canonical JSON; catches sort drift, float canonicalization, and signed cognition-market artifact regressions
capability_receiptfuzz_targets/capability_receipt.rschio-core-typesCapability + receipt round-trip; exercises the capability-algebra invariants
manifest_roundtripfuzz_targets/manifest_roundtrip.rschio-manifestTool-manifest decode plus canonicalization
revocation_oracle_merklefuzz_targets/revocation_oracle_merkle.rschio-revocation-oracleRevocation oracle sparse-Merkle insert, inclusion, and non-inclusion proof surface
federation_trust_establishmentfuzz_targets/federation_trust_establishment.rschio-federationKernel trust-establishment envelopes, peer pins, freshness, and fail-closed resolution
finding_worker_protocolfuzz_targets/finding_worker_protocol.rschio-finding-workerHosted worker capability, job, request, transfer, result, and attestation protocol validation
underwriting_policy_inputfuzz_targets/underwriting_policy_input.rschio-underwritingUnderwriting policy, decision, marketplace, and premium decode surfaces
fuzz_policy_parse_compilefuzz_targets/policy_parse_compile.rschio-policyHushSpec parser, validator, compiler, and YAML round-trip
policy_analyzefuzz_targets/policy_analyze.rschio-policyBounded policy relations, analyzer totality, and evaluator-confirmed refinement witnesses
fuzz_sql_parserfuzz_targets/sql_parser.rschio-data-guardsSQL parser and SQL guard fail-closed analysis across dialects
fuzz_merkle_checkpointfuzz_targets/merkle_checkpoint.rschio-kernelMerkle tree inclusion proofs, signed checkpoint validation, and cognition-market public status proof coverage
fuzz_tool_actionfuzz_targets/tool_action.rschio-guardsTool action classification and guard verdicts for egress, shell, SQL, memory, MCP, and HTTP authority projection

Each target carries the source-path globs that, when changed, must trigger it on the pull request. The header of fuzz/target-map.toml names the two build scripts the list stays in lockstep with: .clusterfuzzlite/build.sh and fuzz/oss-fuzz/build.sh.


The inventory tests

Four registries name the target set and nothing forces a new target into all four at once, so fuzz/tests/smoke.rs asserts the agreement as ordinary cargo test assertions. fuzz_workflow_matrix_matches_cargo_bins compares the parsed [[bin]] names against the .github/workflows/fuzz.yml matrix. owners_toml_covers_all_matrix_targets compares the owner map against the same matrix, so a target whose [targets.<name>] block is missing fails the test rather than failing scripts/promote_fuzz_seed.sh during a crash triage.

fuzz/tests/smoke.rs180-185rust
fn owners_toml_covers_all_matrix_targets() {
    assert_eq!(
        sorted(owners_toml_targets()),
        sorted(workflow_fuzz_targets())
    );
}

The third one is the seed floor. MINIMUM_SEED_COUNT is 3, and every target in the matrix has to meet it, so an empty or missing corpus directory is a test failure rather than a silently vacuous fuzz run.

fuzz/tests/smoke.rs188-196rust
fn all_matrix_targets_meet_seed_floor() {
    for target in workflow_fuzz_targets() {
        let count = each_seed(&target, |_| {});
        assert!(
            count >= MINIMUM_SEED_COUNT,
            "target {target} has {count} seeds; expected at least {MINIMUM_SEED_COUNT}"
        );
    }
}

A fourth test, all_matrix_targets_have_declared_smoke_posture, requires every matrix target to appear in exactly one of two declared lists: CORPUS_SMOKE_TARGETS, which get a per-target in-process smoke test that feeds every seed through the owning crate's fuzz entry point, and NO_IN_PROCESS_SMOKE_TARGETS, which are declared as deliberately outside that reusable harness. Adding a target without picking one of the two fails.


Harness shape

The smallest harnesses delegate to a fuzz_ entry point exported by the owning crate behind that crate's fuzz feature.

fuzz/fuzz_targets/jwt_vc_verify.rsrust
//! Trust-boundary fuzz target for `chio_credentials::verify_chio_passport_jwt_vc_json`.

#![no_main]

use chio_credentials::fuzz::fuzz_jwt_vc_verify;
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    fuzz_jwt_vc_verify(data);
});

Targets that need a structure-aware mutator declare one with fuzz_mutator!. The canonical-JSON mutator at fuzz/mutators/canonical_json.rs is re-exported through fuzz/src/lib.rs as chio_fuzz::canonical_json::canonical_json_mutate and is consumed by six targets: canonical_json, capability_receipt, manifest_roundtrip, mcp_envelope_decode, federation_trust_establishment, and underwriting_policy_input.

fuzz/fuzz_targets/capability_receipt.rsrust
//! Trust-boundary fuzz target for `chio-core-types` `CapabilityToken` and `ChioReceipt` canonical-JSON deserialization.

#![no_main]

use chio_core_types::{capability::token::CapabilityToken, receipt::body::ChioReceipt};
use chio_fuzz::canonical_json::canonical_json_mutate;
use libfuzzer_sys::{fuzz_mutator, fuzz_target};

fuzz_target!(|data: &[u8]| {
    // Try CapabilityToken first; on success, drive the verify path.
    if let Ok(token) = serde_json::from_slice::<CapabilityToken>(data) {
        // Signature verification is fail-closed: we accept Ok(false) /
        // Err(_) as expected outcomes. The point is that the call must
        // not panic on any structurally valid CapabilityToken.
        let _ = token.verify_signature();
    }

    // Try ChioReceipt independently. Same byte stream, different typed
    // deserializer, different fail-closed surface.
    if let Ok(receipt) = serde_json::from_slice::<ChioReceipt>(data) {
        let _ = receipt.verify_signature();
    }
});

fuzz_mutator!(|data: &mut [u8], size: usize, max_size: usize, seed: u32| {
    canonical_json_mutate(data, size, max_size, seed)
});

Two targets embed their seed corpus in the binary with include_bytes! so the first iteration always exercises a known-good shape: fuzz_targets/tool_action.rs and fuzz_targets/policy_parse_compile.rs.


Corpus

Seed corpora live under fuzz/corpus/<target>/. The cargo-fuzz convention is corpus/<target>/ for unprefixed targets and corpus/fuzz_<target>/ for the main-branch fuzz_* binaries. A typical corpus directory holds:

  • A handful of hand-curated seed inputs covering the major structural cases (one valid case, one boundary case, one intentionally malformed case).
  • Promoted seeds: every crash that triaged through scripts/promote_fuzz_seed.sh becomes a permanent corpus entry plus a regression test in the owning crate.

Adding a new seed:

bash
# Drop the input bytes
cp my-seed.bin fuzz/corpus/<target>/

# Run the target with the seed in the corpus
cd fuzz
cargo +nightly fuzz run <target>

# If the seed survives the run, commit it:
git add fuzz/corpus/<target>/my-seed.bin

Corpus snapshot

Seed counts per committed corpus directory under fuzz/corpus/, one directory per target and none of them empty. Each directory name matches its [[bin]] name, so the four targets whose binary name carries the fuzz_ prefix (fuzz_merkle_checkpoint, fuzz_policy_parse_compile, fuzz_sql_parser, fuzz_tool_action) take a fuzz_-prefixed corpus directory and the rest use the plain target name. The floor of three is the one all_matrix_targets_meet_seed_floor asserts, so the smallest counts below sit exactly on it.

TargetSeedsTargetSeeds
a2a_envelope_decode8fuzz_tool_action12
acp_envelope_decode7jwt_vc_verify5
anchor_bundle_verify6manifest_roundtrip7
attest_verify3mcp_envelope_decode7
canonical_json5oid4vp_presentation4
capability_receipt14openapi_ingest9
chio_yaml_parse7policy_analyze3
did_resolve6receipt_log_replay7
eval_receipt_bundle3revocation_oracle_merkle3
federation_trust_establishment3underwriting_policy_input5
finding_worker_protocol3wasm_guard_escape8
fuzz_merkle_checkpoint3wasm_guard_smith5
fuzz_policy_parse_compile6wasm_preinstantiate_validate7
fuzz_sql_parser10wit_host_call_boundary7

Targets sitting on the floor (e.g. attest_verify, fuzz_merkle_checkpoint) rely on the libFuzzer mutator to grow coverage from the three curated seeds. Targets with double-digit counts (e.g. capability_receipt, fuzz_tool_action, fuzz_sql_parser) carry promoted regression seeds from prior crash triages.

Promoted regression seeds also land as a tests/ file in the owning crate (per owners.toml) so the seed stays exercised even when the fuzz lane is offline.


ClusterFuzzLite integration

ClusterFuzzLite (CFLite) is a continuous fuzzing layer for repos that do not run on Google's OSS-Fuzz infrastructure. Three workflows share the target list, and the header of .github/workflows/fuzz.yml states the division between them:

  • .github/workflows/cflite_pr.yml · changed-target sampling on a pull request, 60 seconds per target. It computes which source-path globs changed, intersects them with the fuzz/target-map.toml triggers, and runs the matching subset.
  • .github/workflows/cflite_batch.yml · a nightly rotation that runs one target per night for 1,800 seconds, picked by day_index % cycle_size over the sorted target-map keys, with an inputs.target override on manual dispatch.
  • .github/workflows/fuzz.yml · a nightly native cargo fuzz matrix over the full inventory, 30 minutes per target under AddressSanitizer, with timeout-minutes: 45 per matrix job.

A shared budget guard, scripts/check-fuzz-budget.sh, counts all three toward a 1,800 runner-minute trailing-30-day cap. On the pull-request lane the guard runs with GH_FUZZ_BUDGET_CAP_MODE: warn, which the workflow explains as reporting budget exhaustion without blocking an unrelated pull request.

The build entrypoint is .clusterfuzzlite/build.sh, which its own header says mirrors fuzz/oss-fuzz/build.sh target for target. Corpus storage is a sibling private repo rather than GCS. The nightly rotation reads secrets.FUZZ_CORPUS_PAT into a storage-repo action input when the secret is present and runs a no-storage variant of every step when it is not. The pull-request lane passes no storage-repo at all; both of its run steps are named for running without corpus storage.

The pull-request check

The required check is cflite_pr / changed-target-sampling. A budget check runs first, then the sampling job with a 75-minute job timeout whose comment records the sizing assumption behind it. The fuzz: full label promotes the run to a 120-second-per-target full sweep, and labeled is in the trigger list so adding the label to an open pull request fires a fresh run.

.github/workflows/cflite_pr.yml56-80yaml
jobs:
  budget-check:
    name: fuzz-budget
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
      - name: Verify 30-day fuzz budget
        env:
          GH_TOKEN: ${{ github.token }}
          GH_FUZZ_BUDGET_RATE_LIMIT_MODE: warn
          GH_FUZZ_BUDGET_CAP_MODE: warn
          GH_FUZZ_BUDGET_WORKFLOWS: cflite_pr.yml,cflite_batch.yml,fuzz.yml
        run: |
          set -euo pipefail
          # PR-time CFLite is an advisory release qualification signal. Report
          # budget exhaustion without blocking unrelated PRs while the shared
          # trailing window is already over cap.
          scripts/check-fuzz-budget.sh "${{ github.repository }}"

  changed-target-sampling:
    name: changed-target-sampling
    needs: budget-check
    runs-on: ubuntu-latest
    # The 25-target full sweep has a 50-minute fuzz budget before build time.
    timeout-minutes: 75

The detection step diffs against the merge base rather than the base commit, glob-matches each fuzz/target-map.toml trigger against the changed paths, and falls back to the full inventory when the diff touches the control plane itself. If nothing fires, a following step fails the job and prints the changed files, so a diff with no mapped target is a visible failure rather than a silent pass.

.github/workflows/cflite_pr.yml119-163yaml
- name: Compute changed targets
  id: targets
  if: steps.mode.outputs.full == 'false'
  run: |
    set -euo pipefail
    base_sha="${{ github.event.pull_request.base.sha }}"
    head_sha="${{ github.event.pull_request.head.sha }}"
    merge_base="$(git merge-base "${base_sha}" "${head_sha}")"
    echo "merge-base: ${merge_base}"
    git diff --name-only "${merge_base}" "${head_sha}" > changed.txt
    mapfile -t targets < <(yq -p=toml -r '.targets | keys | .[]' \
      fuzz/target-map.toml)
    fired=()
    for tgt in "${targets[@]}"; do
      mapfile -t triggers < <(yq -p=toml -r \
        ".targets.${tgt}.triggers[]" fuzz/target-map.toml)
      for trig in "${triggers[@]}"; do
        regex="$(printf '%s' "${trig}" | sed -e 's|\.|\\.|g' \
          -e 's|\*\*|__DSTAR__|g' -e 's|\*|[^/]*|g' \
          -e 's|__DSTAR__|.*|g')"
        if grep -qE "^${regex}$" changed.txt; then
          fired+=("${tgt}")
          break
        fi
      done
    done
    # Inventory fallback: builder, workflow, and target-map edits
    # can break changed-target sampling without matching a source
    # trigger. Run the full inventory for those control-plane edits.
    if grep -qE '^(\.cargo/|\.clusterfuzzlite/|\.github/workflows/cflite_pr\.yml$|Cargo\.(toml|lock)$|fuzz/(Cargo\.toml|target-map\.toml)$|Makefile$|scripts/(ci-pr-tier|spec-drift-check|check-review-slices)\.(sh|py)$)' changed.txt; then
      echo "fuzz inventory edit detected; firing the full inventory"
      fired=("${targets[@]}")
    fi
    if ((${#fired[@]} == 0)); then
      : > fired.txt
    else
      printf '%s\n' "${fired[@]}" | sort -u > fired.txt
    fi
    fired_count="$(wc -l < fired.txt | tr -d ' ')"
    printf 'fired targets: %s\n' "${fired_count}"
    cat fired.txt
    {
      echo "count=${fired_count}"
      printf 'targets<<EOF\n%s\nEOF\n' "$(cat fired.txt)"
    } >> "${GITHUB_OUTPUT}"

The fuzz invocation is the upstream google/clusterfuzzlite action, pinned by commit SHA, fed the target list through the CHIO_CFLITE_TARGETS environment variable that .clusterfuzzlite/build.sh reads.

.github/workflows/cflite_pr.yml184-195yaml
- name: Run ClusterFuzzLite changed-target sampling without corpus storage
  if: steps.mode.outputs.full == 'false' && steps.targets.outputs.count != '0'
  uses: google/clusterfuzzlite/actions/run_fuzzers@82652fb49e77bc29c35da1167bb286e93c6bcc05
  env:
    CHIO_CFLITE_TARGETS: ${{ steps.targets.outputs.targets }}
  with:
    language: rust
    fuzz-seconds: ${{ steps.mode.outputs.fuzz_seconds }}
    mode: code-change
    sanitizer: address
    github-token: ${{ secrets.GITHUB_TOKEN }}
    report-unreproducible-crashes: false

Crashes upload as the cflite-pr-crashes-<PR> artifact from ./out/artifacts with a 14-day retention window. The nightly rotation picks its single target arithmetically from the sorted target-map keys, so the cycle length is the target count and every target comes round once per cycle.

.github/workflows/cflite_batch.yml73-100yaml
- name: Pick today's target
  id: pick
  env:
    FORCED_TARGET: ${{ inputs.target }}
  run: |
    set -euo pipefail
    mapfile -t targets < <(yq -p=toml -r '.targets | keys | .[]' \
      fuzz/target-map.toml | sort)
    if (( ${#targets[@]} == 0 )); then
      echo "fuzz/target-map.toml has no targets" >&2
      exit 1
    fi
    if [[ -n "${FORCED_TARGET}" ]]; then
      picked="${FORCED_TARGET}"
      if ! printf '%s\n' "${targets[@]}" | grep -Fxq -- "${picked}"; then
        echo "unknown forced fuzz target: ${picked}" >&2
        exit 1
      fi
    else
      day_index=$(( $(date -u +%s) / 86400 ))
      picked="${targets[$(( day_index % ${#targets[@]} ))]}"
    fi
    printf 'picked: %s (rotation cycle: %d targets)\n' \
      "${picked}" "${#targets[@]}"
    {
      echo "target=${picked}"
      echo "cycle_size=${#targets[@]}"
    } >> "${GITHUB_OUTPUT}"

The project config both CFLite workflows share names the security contact and the sanitizer set.

.clusterfuzzlite/project.yamlyaml
# ClusterFuzzLite project config for Chio.
#
# Storage backend: backbay-labs/chio-fuzz-corpus sibling private repo (no GCS).
# The repo is created out-of-band before the first cflite_batch.yml run.
#
# NOTE: ClusterFuzzLite reads the corpus storage-repo as an input to
# the `run_fuzzers` and `build_fuzzers` GitHub Actions, NOT from
# project.yaml. The `storage-repo:` input is wired with `FUZZ_CORPUS_PAT`
# in `.github/workflows/cflite_pr.yml` and `.github/workflows/cflite_batch.yml`.
language: rust
primary_contact: "security@backbay.io"
auto_ccs:
  - "security@backbay.io"
  - "fuzzing@backbay.io"
sanitizers:
  - address
  - undefined
architectures:
  - x86_64
fuzzing_engines:
  - libfuzzer
report_to_oss_fuzz: false

Running locally

Setup once:

bash
rustup toolchain install nightly
cargo install cargo-fuzz --locked

Run a single target. The cargo-fuzz wrapper builds the binary with libFuzzer instrumentation, links the seed corpus from fuzz/corpus/<target>/, and forwards the rest of the args to libFuzzer:

bash
cd fuzz
cargo +nightly fuzz run attest_verify

libFuzzer writes its own progress trailer to stderr as it runs: a line per pulse pairing the cumulative iteration count with the coverage edge count (cov), the feature count (ft), and the corpus size. The numbers are machine and seed dependent, so read the trend rather than the values.

Build only (the gate that mirrors CI's build_fuzzers action):

bash
cargo +nightly fuzz build attest_verify

Run with a maximum total time budget (useful for local soak runs):

bash
cargo +nightly fuzz run attest_verify -- -max_total_time=300

The scheduled matrix installs a nightly toolchain rather than pinning a dated one: rustup toolchain install nightly --profile minimal --component rust-src. The dudect lane installs the same way, and its comment gives the reason: keeping both lanes on one toolchain keeps cross-tool diffing and sanitizer runs comparable.


Crashes, minimization, promotion

When libFuzzer finds a crashing input it writes the bytes under the artifact prefix and prints a sanitizer report naming the frame that faulted. The triage path is reproduce, minimize, classify, fix, promote.

bash
cd fuzz

# Reproduce. cargo-fuzz consumes the crash-file path positionally, so
# libFuzzer replays that single input instead of running a fuzz loop.
cargo +nightly fuzz run <target> fuzz/artifacts/<target>/crash-<id>

# Minimize. tmin shrinks the input toward a smaller reproducer that
# still triggers the same crash signature.
cargo +nightly fuzz tmin <target> fuzz/artifacts/<target>/crash-<id>

Promotion is scripts/promote_fuzz_seed.sh, which takes named arguments rather than positional ones. It reads fuzz/owners.toml to resolve the owning crate, computes the seed's SHA-256, and in every mode moves the seed to fuzz/corpus/<target>/<sha>.bin so later runs keep exercising it. Re-promoting a seed that already sits at that canonical path is a no-op rather than a delete.

scripts/promote_fuzz_seed.sh5-37bash
# Reads fuzz/owners.toml to find the owning crate for the named fuzz
# target, computes sha256 of the input seed, then either:
#
#   --mode libfuzzer    Writes a libtest #[test] fn that calls the fuzz
#                       wrapper directly with the seed bytes. Output:
#                       crates/<owner>/tests/regression_<target>_<sha16>.rs.
#                       The 16-hex prefix plus target component avoids
#                       filename collisions when one owner crate hosts
#                       multiple targets or accumulates multiple promoted
#                       seeds.
#
#   --mode proptest     Writes the same regression test plus a paired
#                       proptest property when the owner crate has proptest
#                       in [dev-dependencies]. When proptest is missing,
#                       emits the plain regression test and prints a warning.
#
#   --mode adversarial  Promotes a libFuzzer crash that decodes cleanly
#                       through one of the trust-boundary surfaces into
#                       a triage-pending case under
#                       crates/core/chio-adversarial-suite/cases/<class>/<sha>.json
#                       with `expected_verdict: "DENY"` placeholder and
#                       `pending: true`. Requires --class (one of the
#                       eight attack classes) and --threat-id (a
#                       chio-threat-model.v1.json identifier). The
#                       pending flag is stripped only by a human triager.
#                       No regression .rs file is emitted; the
#                       adversarial harnesses already iterate the
#                       bundled cases.
#
# In all three modes the seed is moved into fuzz/corpus/<target>/<sha>.bin
# so future fuzz runs continue exercising it. Re-promoting a seed whose
# --input path already resolves to the canonical corpus location is a
# no-op (the script does not delete the corpus seed under itself).
bash
scripts/promote_fuzz_seed.sh   --target canonical_json   --input fuzz/artifacts/canonical_json/minimized-<id>   --mode libfuzzer

In libfuzzer mode the script writes crates/<owner>/tests/regression_<target>_<sha16>.rs, a libtest #[test] that calls the fuzz wrapper directly with the seed bytes. The 16-hex prefix plus the target name keeps two promoted seeds in one owner crate from colliding. Because the result is a normal cargo test, the seed stays exercised when the fuzz lane is not running.

proptest mode adds a paired proptest property when the owner crate carries proptest in [dev-dependencies], and warns and falls back to the plain regression when it does not. adversarial mode emits no Rust file at all: it writes a case under crates/core/chio-adversarial-suite/cases/<class>/<sha>.json carrying expected_verdict: "DENY" and pending: true, and the script's header records that only a human triager strips the pending flag. That mode requires --class from a fixed list of eight (clock_rewound, future_dated, replayed_nonce, partial_signature, scope_superset, revocation_rollback, anchor_grafted, sigstore_bundle_payload_mismatch) and a --threat-id matching ^[a-z][a-z0-9_]*$. Passing either flag with another mode is rejected with --class and --threat-id are only accepted with --mode adversarial.


Coverage tracking

cargo-fuzz can dump an HTML coverage report after a run:

bash
cd fuzz
cargo +nightly fuzz coverage <target>
cargo +nightly fuzz coverage <target> --html

The output lands in target/<target-triple>/coverage/<target>/ with per-line and per-region edge counts. Regions that the corpus does not reach are candidates for new seeds or for tightening the fuzz_target body to drive more state.

Corpus smoke test

fuzz/tests/smoke.rs runs on stable as a regular cargo test. Every target in CORPUS_SMOKE_TARGETS gets a <target>_smoke test that feeds its committed seeds through the owning crate's fuzz entry point; the targets in NO_IN_PROCESS_SMOKE_TARGETS are declared as outside that reusable harness and are covered by the seed-floor and inventory assertions only. Neither explores new inputs.

Reproduce

The stable-toolchain check of everything on this page is the smoke suite. It parses fuzz/Cargo.toml, .github/workflows/fuzz.yml and fuzz/owners.toml itself, so a disagreement between the target list, the matrix, the owner map, or the corpora fails it.

bash
cd fuzz
cargo test --test smoke

A single coverage-guided target needs the nightly toolchain and cargo-fuzz from the setup above.

bash
cd fuzz
cargo +nightly fuzz run attest_verify -- -max_total_time=60

Scope and limits

  • Trust-boundary only. Each target wraps the entry point where externally controlled bytes first hit Chio code. Internal helpers are not fuzzed directly; if they fail, the failure surfaces through one of the wrappers above.
  • Fail-closed contract. Targets accept Err(_) as an expected outcome; the harness rejects a panic, an assert failure, an unwrap, or a sanitizer report.
  • Sanitizers. .clusterfuzzlite/project.yaml declares both address and undefined. Each CFLite action invocation names one, and both the pull-request and the nightly-rotation steps pass sanitizer: address, so the undefined-behavior sanitizer is declared rather than exercised by those two lanes.
  • Coverage, not exhaustion. Fuzzing can find bugs reachable by mutating from the corpus within the run budget. It does not enumerate all inputs. Kani explores selected functions within configured bounds.

Next