Skip to content

Fuzz Testing

Fuzz Testing #323

Workflow file for this run

name: Fuzz Testing
on:
schedule:
# Nightly at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
env:
CARGO_HOME: /home/runner/.cargo
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
jobs:
fuzz:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
# Existing — wire-format and high-level state targets
- decode_block
- decode_transaction
- nonce_update
- handshake
- chainsync_msg
- protocol_params
- address
- governance
- ledger_snapshot
- tx_validation
- block_apply
- encode_roundtrip
- lsm_operations
- mempool_admission
- epoch_transition
# Phase 1 — wire-protocol coverage (mux + every mini-protocol)
- mux_demux
- n2c_query
- n2c_tx_submission
- block_fetch_msg
- tx_submission2_msg
- keepalive_msg
- peer_sharing_msg
# Phase 2 — consensus primitives
- vrf_verify
- kes_verify
- opcert_verify
- ed25519_verify
- blake2b_hash
- chain_select
- header_field_sizes
# Phase 3 — storage layer (Mithril skipped — HTTP-coupled, no byte API)
- immutable_chunk_parse
- volatile_recovery
- chaindb_rollback
# Phase 4 — Plutus / Phase-2
# NOTE: plutus_script_decode is intentionally OMITTED — upstream uplc
# and pallas-codec panic on malformed input (see top-doc in that file
# and the catch_unwind guard in dugite-ledger/src/plutus.rs). The
# in-house decoder is covered by dugite_uplc_program_decode below.
- plutus_data_decode
- cost_model_eval
# Audit #544 — CBOR serialization strictness (D2/D3/D9/D10/D15)
- vkey_witness_sizes
- plutus_bignum
- cbor_skip_value_depth
# In-house decoders — production code path for phase-2 validation
- dugite_uplc_program_decode
- dugite_uplc_data_decode
# Issue #545 E5 — body-hash round-trip soundness
- body_hash
# Issue #613 — Byron block-header decode hardening
- byron_block_decode
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- name: Cache cargo registry and build
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
fuzz/target
key: ${{ runner.os }}-fuzz-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-fuzz-${{ matrix.target }}-
- name: Install cargo-fuzz
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
# The prebuilt cargo-fuzz from taiki-e/install-action is musl-static, and
# cargo-fuzz uses its compile-time `env!("HOST")` as the default --target.
# Without an explicit override it asks for x86_64-unknown-linux-musl, which
# the GNU nightly toolchain doesn't ship and sanitizer can't use anyway
# (musl static libc is incompatible with -Zsanitizer=address). See #492.
#
# rss_limit_mb is raised from the libFuzzer default (2048) to 12288 (12 GB):
# - The default cargo-fuzz build enables AddressSanitizer, which roughly
# doubles to triples the program's RSS via its shadow memory.
# - Over a 1-hour run libFuzzer accumulates a corpus (held in memory)
# and a feature/coverage map; combined with glibc's high-water-mark
# RSS behaviour the process slowly creeps upward. Successive caps of
# 2048 → 5120 → 6144 each got pierced by ~1-10 MB about an hour into
# the run by a different heavy decoder target (encode_roundtrip,
# dugite_uplc_program_decode). The growth is slow but unbounded
# enough to defeat any tight ceiling.
# ubuntu-24.04 hosted runners have 16 GB RAM, so 12 GB leaves ~4 GB for
# the OS, the artifact-upload step, and any concurrent matrix jobs the
# runner may co-host.
#
# max_len bounds the size of any single generated input to 4 KiB. The
# default cargo-fuzz max_len is 4 KiB; we previously raised it to 16 KiB
# then 8 KiB. After observing that nothing about Cardano wire-format
# decoding genuinely needs > 4 KiB of input to exercise the surface
# (transaction bodies, block headers, witness sets all fit comfortably),
# restore the default cap. This meaningfully shrinks the in-memory
# corpus size at 1-hour scale.
- name: Run fuzzer (1 hour)
run: cargo +nightly fuzz run fuzz_${{ matrix.target }} --target x86_64-unknown-linux-gnu -- -max_total_time=3600 -rss_limit_mb=12288 -max_len=4096
- name: Upload corpus
if: always()
uses: actions/upload-artifact@v7
with:
name: corpus-${{ matrix.target }}
path: fuzz/corpus/fuzz_${{ matrix.target }}/
if-no-files-found: ignore
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: crashes-${{ matrix.target }}
path: fuzz/artifacts/fuzz_${{ matrix.target }}/
if-no-files-found: ignore