fix: bound proof span in VerifySubtreeRootInclusion before allocating ranges#325
fix: bound proof span in VerifySubtreeRootInclusion before allocating ranges#325evan-forbes wants to merge 1 commit into
Conversation
VerifySubtreeRootInclusion called ToLeafRanges (allocating one LeafRange per ~subtreeWidth leaves of the attacker-controlled [proof.Start(), proof.End()) span) before the cheap len(subtreeRoots) count check. A small fixed subtreeWidth and a large proof.End() caused allocation to grow linearly with the attacker-chosen span. Reject up front any proof whose span exceeds len(subtreeRoots)*subtreeWidth leaves, using an overflow-safe ceil(span/subtreeWidth) comparison, so the expensive range construction never runs for such proofs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a security enhancement to the VerifySubtreeRootInclusion function by validating the proof span before performing potentially expensive allocations. This change prevents memory and CPU exhaustion attacks where an attacker could provide a large, controlled End value to trigger excessive memory allocation in ToLeafRanges. Additionally, a new test case, TestVerifySubtreeRootInclusion_allocationBound, has been added to verify that the short-circuit logic correctly rejects invalid spans before allocation occurs. I have no further feedback to provide.
| // Bound the proof span before constructing the leaf ranges. ToLeafRanges | ||
| // allocates one LeafRange per (at most) subtreeWidth leaves of the span | ||
| // [proof.Start(), proof.End()), and proof.End() is attacker-controlled. | ||
| // Each provided subtree root can cover at most subtreeWidth leaves, so a | ||
| // proof whose span exceeds len(subtreeRoots)*subtreeWidth leaves can never | ||
| // satisfy the len(subtreeRoots) == len(ranges) check below. Reject such | ||
| // proofs here, before allocating, to avoid memory/CPU amplification from a | ||
| // large attacker-chosen span. | ||
| if subtreeWidth <= 0 { | ||
| return false, fmt.Errorf("subtree root width %d should be strictly positive", subtreeWidth) | ||
| } | ||
| if len(subtreeRoots) == 0 { | ||
| return false, fmt.Errorf("number of subtree roots cannot be zero") | ||
| } | ||
| // span is guaranteed >= 1 because proof.Start() < proof.End() was checked above. | ||
| span := proof.End() - proof.Start() | ||
| // Each leaf range covers at most subtreeWidth leaves, so ToLeafRanges will | ||
| // produce at least ceil(span/subtreeWidth) ranges. If that already exceeds | ||
| // len(subtreeRoots) the count check below would fail, so reject now. The | ||
| // comparison is written with division to avoid overflowing the int | ||
| // multiplication len(subtreeRoots)*subtreeWidth on attacker-chosen spans: | ||
| // ceil(span/subtreeWidth) > len(subtreeRoots). | ||
| quotient := span / subtreeWidth | ||
| remainder := span % subtreeWidth | ||
| if quotient > len(subtreeRoots) || (quotient == len(subtreeRoots) && remainder != 0) { | ||
| return false, fmt.Errorf("proof span %d exceeds the maximum number of leaves that %d subtree roots of width %d can cover", span, len(subtreeRoots), subtreeWidth) | ||
| } |
There was a problem hiding this comment.
I feel the comments could be summarised to one-two lines maximum. This is too much documentation in the code
VerifySubtreeRootInclusioncallsToLeafRanges(allocating O((End-Start)/subtreeWidth) ranges from an attacker-controlledproof.End()) before the cheaplen(subtreeRoots)count check; this fix rejects proofs whose span exceedslen(subtreeRoots)*subtreeWidthbefore allocating.This is strictly hardening with very limited production accessibility: the only in-tree caller (celestia-node's
CommitmentProof.Verify) is not network-reachable and its width derivation already curbs allocation; the exposure is external consumers that verify untrusted proofs with a fixed small width, so fixing because it is cheap and correct.🤖 Generated with Claude Code