From 653ee2251090a02786040fa2b9c8e49802a8543d Mon Sep 17 00:00:00 2001 From: Popy21 Date: Fri, 21 Aug 2026 16:19:46 +0200 Subject: [PATCH] LibBytes: bound `s` in `dynamicStructInCalldata` `dynamicStructInCalldata` reads the relative offset `s` from calldata and computes `result.length := sub(a.length, s)` without bounding `s` against `a.length`. When `s > a.length` the subtraction underflows and the helper returns a slice whose offset is past the end of `a` and whose length is ~2**256. The existing `shr(64, s)` term does not catch it: a value slightly larger than `a.length` is far below 2**64. The two sibling helpers in the same file already reject the same input -- `staticStructInCalldata` via `gt(offset, l)` and `bytesInCalldata` via `gt(add(s, result.length), l)`. This adds the matching bound. Adds a regression test that fails without the change ("next call did not revert as expected") and passes with it. Full test/LibBytes.t.sol suite: 24/24 passing. Co-Authored-By: Claude Opus 5 (1M context) --- src/utils/LibBytes.sol | 6 +++++- test/LibBytes.t.sol | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/utils/LibBytes.sol b/src/utils/LibBytes.sol index 06d082b23..c9ea07c38 100644 --- a/src/utils/LibBytes.sol +++ b/src/utils/LibBytes.sol @@ -816,7 +816,11 @@ library LibBytes { let s := calldataload(add(a.offset, offset)) // Relative offset of `result` from `a.offset`. result.offset := add(a.offset, s) result.length := sub(a.length, s) - if or(shr(64, or(s, or(l, a.offset))), gt(offset, l)) { revert(l, 0x00) } + // `gt(s, a.length)` is required: without it `sub(a.length, s)` underflows and + // `result.length` becomes ~2**256, yielding a slice that points past `a`. + // The sibling helpers already bound `s` -- `bytesInCalldata` via + // `gt(add(s, result.length), l)`, `staticStructInCalldata` via `gt(offset, l)`. + if or(shr(64, or(s, or(l, a.offset))), or(gt(offset, l), gt(s, a.length))) { revert(l, 0x00) } } } diff --git a/test/LibBytes.t.sol b/test/LibBytes.t.sol index ca94902f3..c7b3b4474 100644 --- a/test/LibBytes.t.sol +++ b/test/LibBytes.t.sol @@ -414,4 +414,25 @@ contract LibBytesTest is SoladyTest { require(keccak256(expectedChildren[i]) == keccak256(children[i])); } } + + function testDynamicStructInCalldataRejectsOutOfBoundsOffset() public { + // `s` (the relative offset read from calldata) greater than `a.length` used to make + // `sub(a.length, s)` underflow, returning a slice with a ~2**256 length pointing past `a`. + bytes memory encoded = abi.encodePacked(uint256(0x1000)); + vm.expectRevert(); + this.dynamicStructInCalldataAt(encoded, 0x00); + } + + function dynamicStructInCalldataAt(bytes calldata a, uint256 offset) + public + pure + returns (uint256 o, uint256 l) + { + bytes calldata p = LibBytes.dynamicStructInCalldata(a, offset); + assembly { + o := p.offset + l := p.length + } + } + }