fix: Clamp the depth used to index selectBranch's key byte - #7941
Draft
bthomee wants to merge 1 commit into
Draft
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens SHAMap node branch selection and node-ID deserialization against out-of-range depths, preventing potential out-of-bounds reads on 32-byte keys and centralizing prefix/mask validation logic.
Changes:
- Add an assertion + runtime clamp in
selectBranchto prevent indexing past the end of the key when depth reaches (or exceeds) leaf depth. - Factor prefix/mask checking into a shared
isPrefixOfAtDepthhelper and reuse it inisPrefixOfanddeserializeSHAMapNodeID. - Add focused unit tests covering prefix depth-sensitivity, deserialization depth rejection, leaf-depth child guards, and the
selectBranchclamp behavior under different build configurations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/libxrpl/shamap/SHAMapNodeID.cpp | Introduces isPrefixOfAtDepth, tightens deserialization validation, and clamps selectBranch depth to avoid OOB reads. |
| src/tests/libxrpl/shamap/SHAMapNodeID.cpp | Adds new test coverage for SHAMapNodeID prefix behavior, deserialization validation, and selectBranch depth clamping/assert behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| selectBranch(SHAMapNodeID const& id, uint256 const& hash) | ||
| { | ||
| auto const depth = id.getDepth(); | ||
| XRPL_ASSERT(id.getDepth() < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth"); |
Comment on lines
+124
to
+128
| #else | ||
| // In a debug build the assert is live and must reject this call outright, in a forked | ||
| // process so a failure here cannot take down the rest of the suite. | ||
| EXPECT_DEATH((void)selectBranch(leafDepthID, kTestKey), "depth below leaf depth"); | ||
| #endif |
bthomee
force-pushed
the
bthomee/shamap-stack-04-clamp-selectbranch-depth
branch
from
August 3, 2026 23:25
2bed752 to
ed28138
Compare
bthomee
force-pushed
the
bthomee/shamap-stack-03-reject-inner-node-leaf-depth
branch
from
August 4, 2026 00:12
24baeb7 to
da20f92
Compare
bthomee
force-pushed
the
bthomee/shamap-stack-04-clamp-selectbranch-depth
branch
from
August 4, 2026 00:12
ed28138 to
7b7d835
Compare
`selectBranch` reads the key byte at `depth / 2`, which is out of bounds for a 32-byte key once the depth reaches 64. Every branch selection in the map funnels through here, so this is the one place a stray depth can turn into a bad read. Assert the precondition for callers, then clamp anyway: a wrong answer for an input that should never occur is better than reading past the buffer. Verified under ASan that the unclamped form reads one byte past a 32-byte allocation while the clamped form does not. `depthMask`'s own 65-entry table had the same exposure one level up, reachable through the public `createID` factory rather than only from inside the map. A depth past `kLeafDepth` indexed that table out of bounds, confirmed under ASan as a 4-byte `global-buffer-overflow` immediately after `kMasks`. Both places that can set a depth now clamp it: the constructor, which is the single point every `SHAMapNodeID`'s `depth_` passes through, and `createID`, which needs its own bound because it picks the mask while evaluating the constructor's argument, before the constructor body could correct anything. Clamping rather than throwing, which is what `getChildNodeID` does for the analogous case: `createID` is reached from `getSHAMapNodeID` with a peer-supplied depth, and two of that function's three callers (`InboundTransactions::gotData`, `PeerImp::onMessage`) sit on paths with no handler between them and a thread boundary, so a throw there would end the process rather than the message. Clamping also has to fix up `id_` alongside `depth_`, since a node ID whose id and depth disagree fails the invariant every read of `id_` relies on. Leaving the depth unclamped would additionally let `getRawString` narrow it to a byte, turning depth 256 into a node claiming to be the root. `deserializeSHAMapNodeID` gets the same mask check `isPrefixOf` already performs, pulled into a shared `isPrefixOfAtDepth` helper, and the masking both it and `createID` perform is now a named `maskedToDepth` rather than a repeated bitwise-and. Tests cover the depth-sensitivity of `isPrefixOf`, the guards that must hold with asserts stripped, that `deserializeSHAMapNodeID` rejects an out-of-range depth, and the clamp itself under both build configurations (`EXPECT_DEATH` in a forked process when the assert is live, and the clamped result compared against depth 63 when it is not). The clamp test also gates on `ENABLE_VOIDSTAR`, not just `NDEBUG`: under Antithesis instrumentation `XRPL_ASSERT` routes to a handler that records the hit but never aborts, so a Debug build with voidstar enabled has `NDEBUG` undefined yet still hits the same non-fatal assert as a release build, and without this gate would send that configuration into the `EXPECT_DEATH` arm, where the forked child never dies and the test fails, breaking the CI job that runs this suite under `-Dvoidstar=ON`.
bthomee
force-pushed
the
bthomee/shamap-stack-03-reject-inner-node-leaf-depth
branch
from
August 4, 2026 00:49
da20f92 to
8af43fd
Compare
bthomee
force-pushed
the
bthomee/shamap-stack-04-clamp-selectbranch-depth
branch
from
August 4, 2026 00:49
7b7d835 to
a09fec0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 4/8 of a stack. Base: part 3 (
bthomee/shamap-stack-03-reject-inner-node-leaf-depth).selectBranchreads the key byte atdepth / 2, which is out of bounds for a32-byte key once the depth reaches 64. Every branch selection in the map
funnels through here, so this is the one place a stray depth can turn into a
bad read. Assert the precondition for callers, then clamp anyway: a wrong
answer for an input that should never occur is better than reading past the
buffer. Verified under ASan that the unclamped form reads one byte past a
32-byte allocation while the clamped form does not.
deserializeSHAMapNodeIDgets the same mask checkisPrefixOfalreadyperforms, pulled into a shared
isPrefixOfAtDepthhelper.Tests cover the depth-sensitivity of
isPrefixOf, the guards that must holdwith asserts stripped, that
deserializeSHAMapNodeIDrejects an out-of-rangedepth, and the clamp itself under both build configurations (
EXPECT_DEATHin a forked process when the assert is live, and the clamped result compared
against depth 63 when it is not). The clamp test also gates on
ENABLE_VOIDSTAR, not justNDEBUG: under Antithesis instrumentationXRPL_ASSERTroutes to a handler that records the hit but never aborts, so aDebug build with voidstar enabled has
NDEBUGundefined yet still hits thesame non-fatal assert as a release build, and without this gate would send
that configuration into the
EXPECT_DEATHarm, where the forked child neverdies and the test fails, breaking the CI job that runs this suite under
-Dvoidstar=ON.