Skip to content

fix(uexecutor): cap the outbound gateway payload at admission (F-2026-18146) - #364

Merged
Aman035 merged 7 commits into
audit-fixesfrom
fix/F-2026-18146-outbound-payload-cap
Sep 3, 2026
Merged

fix(uexecutor): cap the outbound gateway payload at admission (F-2026-18146)#364
Aman035 merged 7 commits into
audit-fixesfrom
fix/F-2026-18146-outbound-payload-cap

Conversation

@0xNilesh

@0xNilesh 0xNilesh commented Sep 2, 2026

Copy link
Copy Markdown
Member

F-2026-18146. Both halves the auditor asked for: admission rejects the oversized payload, and the poll survives one anyway.

Core: cap the payload at admission

  • MaxOutboundPayloadBytes (128 KiB) enforced on event.Payload in create_outbound.go. The gateway payload is attacker-controlled and lands in state unbounded today.
  • Kept separate from MaxUniversalPayloadBytes on purpose. Same number, different reason: geth's txpool rejects a transaction over 128 KB, so a larger payload is unsendable once TSS has signed it.

Client: survive a page that does not fit

  • Any error on a pending-outbound page used to fail the whole poll, so one oversized response meant no pending outbound reached the signer. Two ways to trip it: a single oversized row, or a full 1000-row page averaging more than 8 KiB a row.
  • On ResourceExhausted the client now halves the page and retries the same offset. Every other error still fails the poll.
  • Page size stays 1000, so a normal poll is still one request per page. The extra requests only happen on a poll that would otherwise have returned nothing.
  • The offset advances by the rows actually returned rather than a fixed page size, so a degraded page cannot skip or re-read a row.
  • One poll is capped at 5000 rows, stated directly rather than as a page count. Counting pages instead would quietly cut a degraded poll to five times whatever the page shrank to, so 1250 rows at a 250-row page. A degraded poll now still reads 5000, in 22 requests instead of 5.
  • No request cap. The row budget already bounds the walk: a served page adds at least one row or is short and ends it, and halving bottoms out at a page of one. A capped payload also means the page never degrades below about 64 rows, so a request cap low enough to matter would truncate the legitimate worst case instead of protecting it.

The cap is what makes the client side clean: halving terminates because a page of one is at most 128 KiB against an 8 MiB receive limit, so no row is ever unfetchable and nothing is skipped. A page of one that still fails is returned as a real error.

Static page sizing was the alternative and is worse: 8 MiB over a 128 KiB cap is 64 rows a page, so 320 rows a poll instead of 5000, or 78 round trips to hold today's throughput. That trades this finding for the starvation fixed in F-2026-18817, and pays on every poll for a case the cap should now prevent.

Tests

  • Admission rejects an oversized payload, and the size check is covered directly.
  • A poll whose first page does not fit returns every row, oldest first, after halving 1000 to 500 to 250.
  • Offsets follow the rows actually returned, so the second served page starts where the first ended.
  • A page of one that still fails surfaces as ResourceExhausted rather than looping; an unrelated error is not retried smaller.
  • A degraded poll reads the same rows in the same order as an undegraded one, and pays for it in requests.
  • Mutation on the client half: aborting instead of halving, halving on any error, advancing the offset by the requested page, comparing the short-page exit against the original size, bounding the walk by iterations, and dropping the request cap each fail the suite.

BuildOutboundsFromReceipt copied an attacker-controlled payload into state
unbounded. Rejecting there reverts the whole EVM tx, so the gateway burn
rolls back with it.
@Aman035
Aman035 merged commit 5f0341e into audit-fixes Sep 3, 2026
7 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.

2 participants