Skip to content

fix(abi): prevent decode amplification from aliased tail pointers - #221

Merged
kuny0707 merged 2 commits into
tronprotocol:release_1.0.0from
0xbigapple:fix/abi-decode-value-cap
Jul 29, 2026
Merged

fix(abi): prevent decode amplification from aliased tail pointers#221
kuny0707 merged 2 commits into
tronprotocol:release_1.0.0from
0xbigapple:fix/abi-decode-value-cap

Conversation

@0xbigapple

Copy link
Copy Markdown
Collaborator

What does this PR do?

  • TypeDecoder: count what a decode produces against what the response carries, and
    reject when either count is exceeded:

    • dynamic payload bytes → against the input's byte length
    • decoded values → against the input's word count

    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, so
    outputs 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
constantResult can exhaust the heap of any process embedding the SDK. Reachable
whenever 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:

  • Unit Tests

Follow up

Extra details

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.
@0xbigapple 0xbigapple changed the title fix(abi): count what one response can decode into fix(abi): fix(abi): prevent decode amplification from aliased tail pointers Jul 28, 2026
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.
@0xbigapple 0xbigapple changed the title fix(abi): fix(abi): prevent decode amplification from aliased tail pointers fix(abi): prevent decode amplification from aliased tail pointers Jul 28, 2026
@0xbigapple
0xbigapple requested a review from 317787106 July 29, 2026 02:23
@kuny0707
kuny0707 merged commit 7d66c24 into tronprotocol:release_1.0.0 Jul 29, 2026
3 checks passed
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.

4 participants