fix(abi): prevent decode amplification from aliased tail pointers - #221
Merged
kuny0707 merged 2 commits intoJul 29, 2026
Merged
Conversation
An array's element count and every element's tail pointer come from the response, and nothing required the tails to be distinct. The existing guards were per-element and each measured against the whole remaining input, so L elements could all name one tail and each pass its own check while allocating independently — a few hundred words of input decoding into tens of thousands of objects, scaling with the response. Count the decode against what the response carries, in the two units that can be inflated: payload bytes, and values against the input's word count. Both are needed — aliasing one large payload inflates bytes while the value count stays low, and aliasing empty containers does the reverse. A value owns a word when it is fixed-width or when it is reached through a tail pointer, so an aliased tail is paid for again by every element naming it; inline containers own nothing of their own, and counting them too would charge the same words twice and reject uint256[2][2]. Counts are per-thread, opened by a function result or an array decode and cleared on the way out, including when the decode throws.
Round the value cap up so truncated input hits the accurate bounds error, seed the array-entry budget from the region after offset, drop an unreachable Array exclusion, and keep error messages in one unit.
liuyifei001
approved these changes
Jul 28, 2026
317787106
approved these changes
Jul 29, 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.
What does this PR do?
TypeDecoder: count what a decode produces against what the response carries, andreject when either count is exceeded:
A value owns a word when it is fixed-width, or when it is reached through a tail
pointer, so an aliased tail is paid for again by every element naming it. Inline
containers own nothing, and length/offset words are structure rather than values.
DefaultFunctionReturnDecoder: open one count for the whole function result, sooutputs decoded one at a time cannot each claim the same tail.
Counts are per-thread and cleared on the way out, including when the decode throws.
No existing method signature or visibility is changed.
Why are these changes required?
An array's element count and every element's tail pointer are read straight from the
response, and nothing required those tails to be distinct. The two existing guards were
per-element and each measured against the whole remaining input, so L elements could
all name one tail — or overlapping tails — and each passed its own check while
allocating independently. A few hundred words of input then decode into tens of
thousands of objects, and the shape scales with the response, so a single hostile
constantResultcan exhaust the heap of any process embedding the SDK. Reachablewhenever an output is declared as an array of a dynamic element type —
string[],bytes[],uint256[][], or an array of dynamic structs.This PR has been tested by:
Follow up
Extra details