Skip to content

fix: Reject an inner node claimed at leaf depth in verifyProofPath - #7940

Draft
bthomee wants to merge 1 commit into
bthomee/shamap-stack-02-shamapnodeid-isprefixoffrom
bthomee/shamap-stack-03-reject-inner-node-leaf-depth
Draft

fix: Reject an inner node claimed at leaf depth in verifyProofPath#7940
bthomee wants to merge 1 commit into
bthomee/shamap-stack-02-shamapnodeid-isprefixoffrom
bthomee/shamap-stack-03-reject-inner-node-leaf-depth

Conversation

@bthomee

@bthomee bthomee commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Part 3/8 of a stack. Base: part 2 (bthomee/shamap-stack-02-shamapnodeid-isprefixof).

A SHAMap has 65 levels, and nibbles run out at level 64: selectBranch indexes
the key byte at depth / 2, so at depth 64 it reads byte 32 of a 32-byte key.
Only the leaf terminating a path may sit at that depth, but proof path nodes
come off the wire, so a peer could send 65 hash-chained inner nodes and drive
that read past the end of the buffer.

The existing length bound cannot be tightened to catch this: a path for two
keys sharing all 63 leading nibbles legitimately holds 64 inner nodes plus a
leaf, so 65 elements is valid. The claimed node type at the final depth is the
thing to reject, using >= rather than == to match the convention every
other kLeafDepth comparison in this codebase already follows.

Reachable from TMProofPathResponse via LedgerReplayMsgHandler; confirmed
under ASan with asserts compiled out that the unguarded read lands one byte
past a 32-byte heap allocation. Also closes a second gap: nothing checked the
terminal leaf's own key against key, so the hash chain alone let a peer
substitute any leaf whose subtree hashes matched at every level above it.
Pinned by tests: the 65-element path that must verify for both keys sharing
the deep prefix, and the forged all-inner path that must not.

Copilot AI left a comment

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.

Pull request overview

Hardens SHAMap proof-path verification against malformed network-supplied paths by rejecting an inner node claimed at leaf depth (preventing an out-of-bounds key read in selectBranch) and by binding the terminal leaf to the requested key (preventing leaf substitution when the hash chain alone matches).

Changes:

  • Reject inner proof-path nodes at depth >= kLeafDepth in SHAMap::verifyProofPath to avoid reading past the 32-byte key.
  • Verify the terminal leaf’s key matches key to tie the proof to the intended leaf.
  • Add regression tests covering (1) the legitimate 65-element deep path case and (2) the forged all-inner 65-element path rejection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/tests/libxrpl/shamap/SHAMap.cpp Adds tests asserting valid 65-element deep proof paths and rejecting forged all-inner paths at leaf depth.
src/libxrpl/shamap/SHAMapSync.cpp Updates verifyProofPath to reject inner nodes at leaf depth and validate the terminal leaf key matches the requested key.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@bthomee
bthomee force-pushed the bthomee/shamap-stack-02-shamapnodeid-isprefixof branch from ef6fda7 to cc83dd3 Compare August 4, 2026 00:12
@bthomee
bthomee force-pushed the bthomee/shamap-stack-03-reject-inner-node-leaf-depth branch from 24baeb7 to da20f92 Compare August 4, 2026 00:12
A SHAMap has 65 levels, and nibbles run out at level 64: `selectBranch` indexes
the key byte at `depth / 2`, so at depth 64 it reads byte 32 of a 32-byte key.
Only the leaf terminating a path may sit at that depth, but proof path nodes
come off the wire, so a peer could send 65 hash-chained inner nodes and drive
that read past the end of the buffer.

The existing length bound cannot be tightened to catch this: a path for two
keys sharing all 63 leading nibbles legitimately holds 64 inner nodes plus a
leaf, so 65 elements is valid. The claimed node type at the final depth is the
thing to reject, using `>=` rather than `==` to match the convention every
other `kLeafDepth` comparison in this codebase already follows.

Reachable from `TMProofPathResponse` via `LedgerReplayMsgHandler`; confirmed
under ASan with asserts compiled out that the unguarded read lands one byte
past a 32-byte heap allocation. Also closes a second gap: nothing checked the
terminal leaf's own key against `key`, so the hash chain alone let a peer
substitute any leaf whose subtree hashes matched at every level above it.
Pinned by tests: the 65-element path that must verify for both keys sharing
the deep prefix, and the forged all-inner path that must not.
@bthomee
bthomee force-pushed the bthomee/shamap-stack-02-shamapnodeid-isprefixof branch from cc83dd3 to bb0a62e Compare August 4, 2026 00:49
@bthomee
bthomee force-pushed the bthomee/shamap-stack-03-reject-inner-node-leaf-depth branch from da20f92 to 8af43fd Compare August 4, 2026 00:49
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