fix: F-2026-18798 | [Dual Defense] UExecutor ValidateBasic Parses Unbounded Decimals Before Ante - #335
Open
0xNilesh wants to merge 1 commit into
Open
fix: F-2026-18798 | [Dual Defense] UExecutor ValidateBasic Parses Unbounded Decimals Before Ante#3350xNilesh wants to merge 1 commit into
0xNilesh wants to merge 1 commit into
Conversation
Length-cap before parse, then BitLen<=256, via a shared helper applied to the six UniversalPayload numeric fields, Inbound.Amount and OutboundTx.Amount.
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.
F-2026-18798 — UExecutor
ValidateBasicParses Unbounded Decimals Before AnteUniversalPayload.ValidateBasicvalidated six numeric string fields with only parse-success plus non-negativity:The comment promised
uint256; the check delivered "parses as a non-negative integer". That gap is two distinct defects.Defect 1 — unmetered DoS
big.Intdecimal parsing is superlinear in digit count. Measured:This is reached from
MsgExecutePayload.ValidateBasic, which is a gasless message type, andBaseApp.runTxrunsvalidateBasicTxMsgsbefore the ante handler. So the work is free and unmetered to the submitter — no gas is charged, and no fee is deducted, before the parse happens.The cost is also per field: there are six numeric fields on a single
UniversalPayload, so one message multiplies the figure above by six. (decode_payload.gore-parses all six downstream, paying it a second time.) Hacken estimated ~0.4s at 900k digits; we measured 3.353s.Defect 2 — silent truncation (not in the original finding)
All six fields are declared
uint256in the contract (src/libraries/Types.sol). We probed go-ethereum's ABI encoder on the pinned dependency:2^256 - 1(max uint256)2^256(max + 1)The encoder truncates mod 2^256 without erroring. An over-range
valuewas therefore not rejected anywhere — the UEA would execute a different amount than the user signed over. That is a correctness bug, not only a DoS.The fix
A shared helper,
types.ValidateUint256String, with three checks in this order:SetString. This is the whole point of the ordering: it turns rejecting a 900k-digit field from an O(n²) parse into an O(1) length comparison. 80 = the 78 digits of max uint256 plus slack for a zero-padded client value.BitLen() <= 256— authoritative range check; closes the truncation gap.Hacken's recommendation 1 (a length cap) is not sufficient on its own. 78 nines is only 78 characters — comfortably inside any 78- or 80-char cap — but has BitLen 260 and still overflows
uint256.BitLen()is the load-bearing check; the length cap is the cheap pre-filter that keeps the expensive parse off the unmetered path. Neither replaces the other, which is why both are present and why the order is fixed.We did not implement Hacken's recommendation 3 (reject in ante with gas charging). On gasless messages, charging gas is not the defence — Hacken themselves prefer failing cheaply in
ValidateBasic. Same reasoning as F-2026-18821 / F-2026-18816.Sites covered
UniversalPayloadfields:value,gas_limit,max_fee_per_gas,max_priority_fee_per_gas,nonce,deadlineInbound.Amount(ValidateForExecution)OutboundTx.AmountThe two
Amountsites keep their existing extra semantics —OutboundTxstill requires strictly positive,Inboundstill enforces its zero-amount-by-tx-type rule. Bounds were added; nothing was relaxed.Out of scope
MigrationPayload.ValidateBasichas the identical defect, and is deliberately untouched here. It is deprecated on both sides — the contract marks it "(Deprecated) Legacy migration payload", and the latest UEA has nomigrateUEAat all. It is being handled by the separateMsgMigrateUEAremoval, so changing it here would only create a conflict.Tests
x/uexecutor/types/uint256_test.go, covering both defects at all three call sites:Amountsites. The timing assertion is deliberately made first, because it is the only assertion that fails if the length cap is removed (BitLenwould still reject the value — just after paying for the parse).2^256 - 1(BitLen 256) is accepted;2^256(BitLen 257) and 78 nines (BitLen 260) are rejected — with an explicit assertion that 78 nines fits inside the length cap, so onlyBitLencan catch it.Both halves were mutation-verified: removing the length cap fails the timing assertions (6.3s vs the 1s budget), and removing the
BitLen()check fails the2^256/ 78-nines cases at all three sites.Full suite green:
./x/... ./app/... ./test/integration/...— 20 packages ok, 0 failures.