fix(model): bound subtree-list allocation on untrusted block parse + fuzz#1005
Conversation
|
🤖 Claude Code Review Status: Complete Current Review: No issues found This PR correctly addresses a security vulnerability (unbounded allocation from untrusted input) with an appropriate fix and comprehensive testing: Core Fix (model/Block.go:286) Test Coverage (model/block_fuzz_test.go)
The fix aligns with the existing maxCoinbaseBUMPSize pattern at line 329 and follows AGENTS.md security rules. |
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-06-01 15:37 UTC |
…fuzz
readBlockFromReader pre-sized the subtree slice straight from the block's
subtree-count varint:
block.Subtrees = make([]*chainhash.Hash, 0, block.subtreeLength)
A block from an untrusted peer claiming ~33M subtrees (or more) forces a
multi-hundred-MB-to-GB allocation before the first missing hash fails the
read — a cheap OOM that takes down the core sidecar via the ServiceManager
errgroup. NewBlockFromBytes' recover() does not save an OOM. The
CoinbaseBUMP read just below was already bounded (1 MB); the subtree list
was not.
Cap the speculative capacity (min(subtreeLength, 1024)); the loop already
appends, so a bogus count now errors on the first missing hash instead of
pre-allocating. Valid blocks parse unchanged.
Add fuzz coverage for the block deserializer:
- FuzzNewBlockFromBytes (public entry; guards the OOM/alloc class that
its recover() cannot catch),
- FuzzReadBlockFromReader (inner body parser, no recover wrapper, so
masked panics in varint/subtree/coinbase/bump parsing surface), and
- TestBlock_HostileSubtreeLengthIsBounded, a deterministic regression
asserting the hostile body allocates < 4 MB (was ~256 MB) before
erroring.
Both fuzz targets ran clean (~525K and ~860K execs, no crashers).
Second in the deserializer-fuzzing series after the utxopersister fix.
15e237e to
97b8eea
Compare
|
Good catch on the implicit init — you're right that it's safe because |
|
ordishs
left a comment
There was a problem hiding this comment.
Approve. Minimal, correct fix for a real unbounded-allocation OOM vector on the untrusted block-parse path.
min(subtreeLength, 1024)cap is safe — the append loop validates each hash and errors on the first missing one; valid blocks parse identically.- Confirmed this was the only allocation sized from an untrusted count in
readBlockFromReader(CoinbaseBUMP was already bounded at 1MB). - Verified locally: regression test passes (<4MB alloc), FuzzReadBlockFromReader ran clean (~190K execs, no crashers), seed fixtures load.
Optional, non-blocking: line 307 passes the block.Subtrees slice to a %d verb (should be len(...)) — pre-existing and now effectively unreachable, but a one-line cleanup while you're here.
oskarszoon
left a comment
There was a problem hiding this comment.
Reviewed. Correct, minimal fix for a real OOM vector — pre-sizing Subtrees from an untrusted varint. Capping the capacity hint is right; the append loop and post-loop length check preserve parse semantics, so valid blocks are unaffected. Fuzz + deterministic regression are well-targeted.
No blocking issues. One non-blocking nit: the allocDelta comment (block_fuzz_test.go:107) says the reading "is not racy" because it's "single-goroutine" — but TotalAlloc is process-wide. The test is fine (monotonic counter + ~512× margin), but the justification is misleading and could trip up someone who later tightens the threshold. Worth a one-line comment fix.
Optional: a deterministic regression through the public NewBlockFromBytes (not just readBlockFromReader) would close the OOM story at the API boundary — fuzz already covers it.
…tegration Merge stu/blockvalidation-inmemory-checkold (ca7ef31) — off-chain prefetch for checkOldBlockIDs plus the upstream/main it carried (bsv-blockchain#1005, bsv-blockchain#993, bsv-blockchain#1022). Conflict resolutions: - services/legacy/netsync/manager.go: kept integration's gap-watchdog self-heal (requestBlocksFromTip / maybeResyncOnMissingParent / missingParentResyncInterval); the incoming side had no content there. - stores/utxo/aerospike/batch_create.go: pass nil for the new GetBinsToStore arena param (main added *bt.Arena; integration-only BatchCreate caller predates it). nil = non-arena route, output-equivalent per create_arena_test.



Second in the deserializer-fuzzing series (after #1004). Targets the block parser and fixes the unbounded-allocation bug it surfaces.
Bug
readBlockFromReaderpre-sized the subtree slice directly from the block's subtree-count varint:A block from an untrusted peer claiming ~33M subtrees (or more, up to a varint's range) forces a multi-hundred-MB-to-GB allocation before the first missing hash fails the read — a cheap OOM that takes down the
coresidecar via the ServiceManager errgroup.NewBlockFromBytes'recover()converts ordinary panics to errors but does not save a genuine OOM. Notably theCoinbaseBUMPread just below was already bounded (1 MB); the subtree list was the gap.Fix
Cap the speculative capacity at
min(subtreeLength, 1024). The loop alreadyappends and validates each hash, so a bogus count now errors on the first missing hash instead of pre-allocating. Valid blocks parse unchanged.Tests
FuzzNewBlockFromBytes— public entry; guards the OOM/allocation class that itsrecover()cannot catch. Seeded frommodel/testdata/*.block.FuzzReadBlockFromReader— the inner body parser fuzzed without therecover()wrapper, so a panic in the varint / subtree-list / coinbase / bump parsing surfaces as a fuzz failure instead of being silently converted to an error in production.TestBlock_HostileSubtreeLengthIsBounded— deterministic regression: a tiny body claiming a huge subtree count allocates < 4 MB (was ~256 MB) before erroring. Fails on the old code, passes on the fix.Verification
268 MBobserved), passes after.FuzzReadBlockFromReader— ~525K execs, no crashers (no recover-masked panics surfaced; the inner reader also exercises go-bt/go-wire and stayed clean).FuzzNewBlockFromBytes— ~860K execs, no crashers.go test -race(block tests + fuzz corpus replay),go vet,gofmt,gci— clean.Last planned target (separate PR):
pkg/fileformatheader parsing.