Fuzz Testing #325
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |