Skip to content

test: add fuzz harness for crypto/action/state codec + primitives#4835

Open
envestcc wants to merge 2 commits into
iotexproject:masterfrom
envestcc:test/fuzz-codec
Open

test: add fuzz harness for crypto/action/state codec + primitives#4835
envestcc wants to merge 2 commits into
iotexproject:masterfrom
envestcc:test/fuzz-codec

Conversation

@envestcc

Copy link
Copy Markdown
Member

Summary

Introduces seven Go native fuzz targets for the highest-value serialization and cryptographic primitive paths in crypto/, action/, and state/, plus a make fuzz-short Makefile target and an advisory CI step.

Also includes a small bug fix to crypto.Merkle.HashTree that was surfaced by the new harness during local validation — see commits.

This implements item P0.3 from the tracking issue (#4832).

What's added

crypto/fuzz_test.go    — FuzzMerkleHashTree, FuzzSort, FuzzSortCandidates
action/fuzz_test.go    — FuzzReceiptDeserialize, FuzzLogDeserialize
state/fuzz_test.go     — FuzzAccountDeserialize, FuzzCandidateListDeserialize
crypto/merkle.go       — single-leaf zero-hash root fix (3 lines)
Makefile               — `make fuzz-short` target, default FUZZTIME=30s
.github/workflows/ci.yaml — "Run Fuzz (short)" step, continue-on-error

Each fuzz target seeds its corpus with valid round-trip-able instances and asserts (via defer recover) that the deserializer/primitive never panics on arbitrary input. Findings beyond the seed corpus stay in testdata/fuzz/ on the runner — they are not committed back.

Bug surfaced and fixed in this PR

crypto.Merkle.HashTree() panicked with index out of range when constructed from a single leaf whose hash is hash.ZeroHash256. Root cause: NewMerkleTree sets mk.root = leaves[0], and HashTree's sentinel if mk.root != ZeroHash256 then misreads the state as "root not yet computed" and falls through to the multi-leaf path. Fix: short-circuit the single-leaf case (the root of a 1-leaf tree is the leaf itself).

CI integration

A new step "Run Fuzz (short)" invokes make fuzz-short (each target for 30s by default). It is currently continue-on-error: true so the harness can land while existing panic sites are triaged. Promote to required once the follow-ups below are resolved.

Follow-ups surfaced by the harness

A 15s/target local run found two panic sites in state.Account.FromProto that require a signature change (return error rather than panic) to fix and are intentionally out of scope here:

  • Account.FromProto panics on a non-decimal Balance string (new(big.Int).SetString(s, 10) returns !ok). Fuzz reproducer: any 32+ byte input where the balance field decodes to a non-decimal string.
  • Account.FromProto panics on an unknown AccountType enum (defensive panic("unknown account type")). Fuzz reproducer: any input where the type field decodes to an out-of-schema enum value.

Both should become error returns. I'll file these as separate issues after this PR lands.

Local validation

  • go test ./crypto/... ./action/... ./state/... — 289 existing tests pass, 37 fuzz seed tests pass.
  • make fuzz-short FUZZTIME=15s — all 7 targets except FuzzAccountDeserialize pass; that one fails as documented above.

Test plan

  • Existing test suites for the three touched packages still green.
  • All fuzz seed inputs pass under plain go test.
  • Merkle fix is exercised by an explicit single-leaf zero-hash seed in FuzzMerkleHashTree.
  • CI step format verified against existing job structure.

Refs #4832

envestcc added 2 commits May 19, 2026 09:38
NewMerkleTree(leaves) for size==1 sets mk.root = leaves[0]. When the
single leaf happens to be hash.ZeroHash256, HashTree's sentinel check
"if mk.root != ZeroHash256" misreads the state as "root not yet
computed" and falls through to the multi-leaf path. There, length
becomes mk.size >> 1 == 0, the inner merkle slice is empty, and the
final merkle[0] access panics with index out of range.

Short-circuit the single-leaf case at the top of HashTree: the root
of a 1-leaf tree is the leaf itself, regardless of its value.

Found by FuzzMerkleHashTree (added in the following commit).

Refs iotexproject#4832
Introduces seven Go native fuzz targets covering the highest-value
serialization and cryptographic primitive paths:

  crypto/  — FuzzMerkleHashTree, FuzzSort, FuzzSortCandidates
  action/  — FuzzReceiptDeserialize, FuzzLogDeserialize
  state/   — FuzzAccountDeserialize, FuzzCandidateListDeserialize

Each target seeds the corpus with valid round-trip-able instances drawn
from known-good shapes, then asserts via deferred recover that the
deserializer/primitive never panics on arbitrary input. Findings beyond
the seed corpus stay in testdata/fuzz/ on the runner; they are not
committed back to the repo.

CI integration: a new "Run Fuzz (short)" step in ci.yaml invokes
`make fuzz-short` (each target for FUZZTIME=30s, overridable). It is
marked continue-on-error while existing panic sites surfaced by the
harness are triaged in follow-ups; promote to required once those are
resolved.

Local 15s/target runs surface two known Account.FromProto panic sites
(invalid balance string, unknown AccountType enum) that require a
signature change to fix and are intentionally out of scope here.

Refs iotexproject#4832
@envestcc envestcc requested a review from a team as a code owner May 19, 2026 01:39
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants