Skip to content

SPEC-035: nightly fuzz harnesses on untrusted-input decoders - #8625

Open
pantheon-flow[bot] wants to merge 3 commits into
masterfrom
ma/run-c933cd25
Open

SPEC-035: nightly fuzz harnesses on untrusted-input decoders#8625
pantheon-flow[bot] wants to merge 3 commits into
masterfrom
ma/run-c933cd25

Conversation

@pantheon-flow

@pantheon-flow pantheon-flow Bot commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Implements SPEC-035 (flow security loop B — boundary fuzzing). Adds a generalized make fuzz-<FuzzFunctionName> [FUZZ_TIME=<duration>] Makefile pattern and five Go native testing.F fuzz harnesses over the four highest-value untrusted-input boundaries in flow-go:

  • FuzzCBORDecodernetwork/codec/cbor/decoder_fuzz_test.go: fuzz raw bytes through the CBOR stream decoder; asserts no panic, error-or-valid on seed corpus; round-trip invariant on valid decoded messages.
  • FuzzDecodeTrieProofledger/trie_encoder_fuzz_test.go: fuzz raw bytes through ledger.DecodeTrieProof; asserts no panic and nil-proof iff error.
  • FuzzDecodeTrieBatchProofledger/trie_encoder_fuzz_test.go: same for ledger.DecodeTrieBatchProof.
  • FuzzFingerprintmodel/fingerprint/fingerprint_fuzz_test.go: mutate a fixture entity, assert Fingerprint is deterministic across two calls.
  • FuzzTransactionValidatorValidateaccess/validator/validator_fuzz_test.go: mutate a valid TransactionBody, assert Validate never panics and accept/reject is deterministic across two calls. Uses CheckPayerBalanceMode: Disabled and a simple fixedBlocks stub so the harness is offline and deterministic.

Makefile: make fuzz-<FuzzFunctionName> auto-discovers the package by grepping for the function; FUZZ_TIME (default 5m) can be overridden (make fuzz-FuzzCBORDecoder FUZZ_TIME=10m). Existing make fuzz-fvm is preserved.

Workflow file (HITL required — workflows token scope)

The .github/workflows/fuzz-nightly.yml file is ready but cannot be pushed by this session's GitHub App token (lacks workflows permission). To complete PR-1, a human must push this file to the branch with a token that has workflows scope.

Full file contents to apply as .github/workflows/fuzz-nightly.yml:

name: Fuzz Nightly

# HOW TO ADD A FUZZ TARGET:
#   1. Add a new entry under matrix.target below with:
#        name: <FuzzFunctionName>   (must match the func name in *_fuzz_test.go exactly)
#        pkg:  <go/package/path>    (relative to the repo root, no leading ./)
#   2. Write the corresponding FuzzXxx(*testing.F) function in a *_fuzz_test.go file
#      in that package (root module only — not integration/ or insecure/).
#   3. That is the only change required; the job runs automatically next night.

on:
  schedule:
    - cron: '17 2 * * *'  # 02:17 UTC nightly (non-:00 minute)
  workflow_dispatch:
    inputs:
      fuzz_time:
        description: 'Fuzz duration per target (e.g. 1m, 5m, 10m)'
        required: false
        default: '10m'

env:
  GO_VERSION: "1.25"

jobs:
  fuzz:
    name: Fuzz ${{ matrix.target.name }}
    runs-on: blacksmith-4vcpu-ubuntu-2404
    strategy:
      fail-fast: false
      matrix:
        target:
          - { name: FuzzCBORDecoder,                 pkg: network/codec/cbor }
          - { name: FuzzDecodeTrieProof,              pkg: ledger }
          - { name: FuzzDecodeTrieBatchProof,         pkg: ledger }
          - { name: FuzzFingerprint,                  pkg: model/fingerprint }
          - { name: FuzzTransactionValidatorValidate, pkg: access/validator }

    steps:
      - name: Checkout repo
        uses: actions/checkout@v6

      - name: Setup private build environment
        if: ${{ vars.PRIVATE_BUILDS_SUPPORTED == 'true' }}
        uses: ./actions/private-setup
        with:
          cadence_deploy_key: ${{ secrets.CADENCE_DEPLOY_KEY }}

      - name: Setup Go
        uses: actions/setup-go@v6
        timeout-minutes: 10
        with:
          go-version: ${{ env.GO_VERSION }}
          cache: true

      - name: Restore fuzz corpus cache
        uses: actions/cache@v5
        with:
          path: ./${{ matrix.target.pkg }}/testdata/fuzz/${{ matrix.target.name }}
          key: fuzz-corpus-${{ matrix.target.name }}-${{ github.run_id }}
          restore-keys: |
            fuzz-corpus-${{ matrix.target.name }}-

      - name: Run fuzz target
        id: fuzz
        run: |
          if grep -q '\badx\b' /proc/cpuinfo 2>/dev/null; then
            CRYPTO_FLAG=""
          else
            CRYPTO_FLAG="-O2 -D__BLST_PORTABLE__"
          fi
          FUZZ_TIME="${{ github.event.inputs.fuzz_time || '10m' }}"
          CGO_CFLAGS="${CRYPTO_FLAG}" go test \
            -fuzz=${{ matrix.target.name }} \
            -fuzztime="${FUZZ_TIME}" \
            ./${{ matrix.target.pkg }}/
        continue-on-error: true

      - name: Upload corpus and crashers
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: fuzz-artifacts-${{ matrix.target.name }}
          path: ./${{ matrix.target.pkg }}/testdata/fuzz/${{ matrix.target.name }}/
          if-no-files-found: ignore

      - name: Open GitHub issue on crasher
        if: steps.fuzz.outcome == 'failure'
        uses: actions/github-script@v7
        with:
          script: |
            const date = new Date().toISOString().slice(0, 10);
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `Fuzz crasher: ${{ matrix.target.name }} (${date})`,
              body: [
                `A fuzz crasher was detected in \`${{ matrix.target.name }}\` during the nightly fuzz run on ${date}.`,
                ``,
                `**Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
                ``,
                `Crasher inputs are attached as artifacts on the run above. Triage target: ≤72 h (per SPEC-035 §6).`,
              ].join('\n'),
              labels: ['bug'],
            });

      - name: Fail job if crasher found
        if: steps.fuzz.outcome == 'failure'
        run: exit 1

Test plan

All five Fuzz* functions pass their seed corpus with go test -run=Fuzz<Name> (no -fuzz flag). Verified against seed corpus:

  • FuzzCBORDecoder — 4 seeds, all PASS (0.443s)
  • FuzzDecodeTrieProof — 3 seeds, all PASS (0.209s)
  • FuzzDecodeTrieBatchProof — 3 seeds, all PASS (0.209s)
  • FuzzFingerprint — 4 seeds, all PASS (0.003s)
  • FuzzTransactionValidatorValidate — 3 seeds, all PASS (0.434s)

Anti-vacuity receipt

Each harness reaches its claimed boundary:

  • CBOR: seeds include a valid round-tripped Proposal message; FuzzCBORDecoder calls c.NewDecoder(bytes.NewReader(data)).Decode() directly — the decoder is exercised on every seed.
  • Trie proof: seeds include a real EncodeTrieProof(p) output; DecodeTrieProof is called unconditionally on every input.
  • Trie batch proof: seeds include a real EncodeTrieBatchProof(bp) output; DecodeTrieBatchProof is called unconditionally on every input.
  • Fingerprint: Fingerprint(e) is called twice on every fuzzed entity with no branching before the call.
  • Validator: v.Validate(ctx, &tx) is called twice on every fuzzed transaction — the validator is exercised unconditionally.

Managed by swe-pipeline (run c933cd25)


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…put decoders (SPEC-035)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pantheon-flow
pantheon-flow Bot requested a review from a team as a code owner July 27, 2026 06:36
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zhangchiqing

zhangchiqing commented Jul 27, 2026

Copy link
Copy Markdown
Member

fix the lint issue CI / Lint (./) (pull_request)

@zhangchiqing

Copy link
Copy Markdown
Member

/managed-agents please fix errors from make lint

Comment on lines +36 to +46
// round-trip: re-encoding a valid decoded message must decode to an equal value
var roundtrip bytes.Buffer
if err := c.NewEncoder(&roundtrip).Encode(msg); err != nil {
// encoder may reject the value if the interface type has no registered code — treat as non-fatal
return
}
msg2, err := c.NewDecoder(&roundtrip).Decode()
if err != nil {
t.Fatalf("round-trip decode failed after successful encode: %v", err)
}
_ = msg2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says re-encoding must decode to an equal value, but msg2 is never compared to msg. Assert equality or fix the comment. The Encode error branch is also dead: Decode only returns types registered in codec.InterfaceFromMessageCode, so a re-encode failure is an invariant violation and should be t.Fatalf, not a silent return.

N uint64
}

func FuzzFingerprint(f *testing.F) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This harness fuzzes RLP encoding of a local two-field struct and asserts two calls in one process agree. That cannot realistically fail and exercises no untrusted-input boundary. Fingerprint real protocol entities with fuzz-mutated fields, or drop the target — as is it burns a nightly CI slot with no chance of signal.

Comment thread Makefile
@FUNC=$*; \
PKG=$$(grep -rl "func $${FUNC}(" --include="*_test.go" . | head -1 | xargs -I{} dirname {} | sed 's|^\./||'); \
if [ -z "$${PKG}" ]; then echo "Error: no fuzz function $${FUNC} found in *_test.go files"; exit 1; fi; \
CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=$${FUNC} -fuzztime=$(FUZZ_TIME) "./$${PKG}/"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without -run '^$$', go test -fuzz first runs the package's entire unit-test suite (minutes for ledger); the fuzz-fvm target above already skips it. -fuzz is also an unanchored regex and go test errors if it matches more than one target.

Suggested change
CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=$${FUNC} -fuzztime=$(FUZZ_TIME) "./$${PKG}/"
CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz="^$${FUNC}$$" -fuzztime=$(FUZZ_TIME) -run '^$$' "./$${PKG}/"

Comment thread Makefile
# fuzz-<FuzzFunctionName>: run a single named fuzz target for FUZZ_TIME.
# The package is auto-discovered by locating the func declaration in *_test.go files.
# Example: make fuzz-FuzzCBORDecoder
fuzz-%:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo already has a native fuzz harness that nothing runs in CI: FuzzTransactionComputationLimit in fvm/fvm_fuzz_test.go. Add { name: FuzzTransactionComputationLimit, pkg: fvm } to the nightly workflow matrix. It also works with this fuzz-% target, making fuzz-fvm mostly redundant.

Its likely no-one ran fuzz-fvm in a while, so that code might also be stale

Comment on lines +18 to +20
if err := c.NewEncoder(&buf).Encode(&proposal); err == nil {
f.Add(buf.Bytes())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if this encode fails, the only structurally valid seed silently disappears and the fuzzer degrades to blind byte mutation. Use f.Fatalf on error.

f.Add([]byte{}, uint64(0), []byte{})

f.Fuzz(func(t *testing.T, script []byte, gasLimit uint64, payerBytes []byte) {
tx := unittest.TransactionBodyFixture()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: TransactionBodyFixture draws ReferenceBlockID from crypto/rand inside the fuzz body, so a corpus entry does not reconstruct the same transaction across runs. Build the fixture once outside f.Fuzz and copy it per iteration so crashers stay reproducible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants