Repository navigation
bugc: revert on out-of-bounds indexing - #344
Merged
Merged
Conversation
Contributor
|
gnidan
force-pushed
the
bugc-mem-element-write
branch
from
October 6, 2026 14:23
5cb4b63 to
45910e8
Compare
An `assert` instruction takes a condition and a panic code. Code generation jumps over a revert when the condition holds; otherwise the code reverts with Solidity's `Panic(uint256)` error data, and the REVERT carries a `revert` context with the panic code and a pointer to that data. Dead code elimination keeps asserts, and constant folding removes an assert whose condition is a true constant.
An array literal's value had a scalar type, so code generation took its length as the scalar's size: [1, 2, 3].length was 32.
As in Solidity, an array or bytes index and a slice bound must be an unsigned integer. Before, a signed index compiled, and since #339 its comparisons are signed, so -1 would pass a bounds check.
Indexing an array or bytes in memory, or a fixed-size array in storage, now reverts with Panic(0x32) when the index is not less than the length, as Solidity does. This holds for reads and writes, at every level of a nested array. A slice x[a:b] reverts unless a <= b <= x.length. The check compares as unsigned words whatever the index's type, so a negative value cast to an index is a huge index and reverts. Before, an index past the end read or wrote whatever memory followed: writing m[0][1] on [[1], [2]] overwrote m[1]'s length word. The locals soundness test that sliced msg.data[0:4] runs with no calldata, so it slices msg.data[0:0] now.
This was referenced Oct 7, 2026
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.
An index out of bounds now reverts, as in Solidity. Before,
a[i]past the end read or wrote whatever memory followed: withlet m: array<array<uint256>> = [[1], [2]], the writem[0][1] = 99overwrote the length word ofm[1].What is checked, for reads and writes, at O0 to O3:
bytesin memory, andmsg.datathrough its slices;b[s:e]reverts unlesss <= e <= b.length, somsg.data[0:4]reverts on shorter calldata;array<T, N>), againstN.A dynamic array in storage is not checked. BUG has no
push, so writing past the end is how such an array grows today (thenested-arraysexample writesmatrix[2][5]on an empty array). Checking those needspushfirst.The revert data is Solidity's
Panic(uint256)with code0x32(selector0x4e487b71), so tools that decode Solidity panics decode these too.Four commits do the work:
assert <condition>, panic <code>, reverts withPanic(code)unless the condition holds. Code generation emits it inline: the condition, aJUMPIover the revert, thenmstore(0, selector) mstore(0x20, code) revert(0x1c, 0x24), then aJUMPDEST. The jump target is a label inside the block, patched like a block jump, so the CFG and the optimizer passes do not change. TheREVERThas the access'scoderange and arevertcontext withpanic: 50and a pointer to the revert data in memory. Dead code elimination keeps asserts; constant folding removes an assert whose condition is a true constant, sof[2]on anarray<uint256, 3>in storage costs nothing.[1, 2, 3].lengthwas 32 (on main too), and so a check on[1, 2, 3][i]would compare against 32. It is now a memory reference.a[i as uint256]). Since bugc: use signed opcodes for signed operands #339 a comparison with a signed operand is signed, so -1 would pass a check.irgen/generate/memory.tscheck every element and byte access, so every memory element and byte access gets them; the slice path and the storage chain (for fixed-size arrays) call the same check. The check compares as unsigned words whatever the operands' types, so a negative value cast to an index reverts.The new
bounds.test.tsruns each case at levels 0 to 3: the first and last index, an index at the length, from calldata, huge, and wrapped below zero, an index after the variable takes a shorter array, a loop that runs exactly to the length and one that runs one past it, nested arrays (the case above), a parameter,bytes, slices, fixed storage arrays, the revert data, theREVERT's context, an array literal indexed past its length, and a negative value cast to an index. That includesf[0 - 1]on a fixed storage array, where the optimizer folds the index and then the check; that is sound now that folding wraps modulo 2^256 (#340). The locals soundness test for abytesslice tookmsg.data[0:4]with no calldata, which now reverts, so it slicesmsg.data[0:0].Code size, deployed bytecode summed over the examples that compile (token-registry fails at O2 and O3 before and after):
Only examples that index or slice change;
arrays.buggrows the most (1076 to 1492 bytes at O3). Each check costs about 22 bytes plus loading the length and comparing. Sharing one revert sequence per function would save about 17 bytes per check after the first, if that matters.