Skip to content

core/std: require exact-length returndata when decoding bool returns - #1540

Open
exstttt wants to merge 2 commits into
argotorg:masterfrom
exstttt:fix/1539-strict-bool-returndata
Open

exstttt wants to merge 2 commits into
argotorg:masterfrom
exstttt:fix/1539-strict-bool-returndata

Conversation

@exstttt

@exstttt exstttt commented Aug 23, 2026 •

Copy link
Copy Markdown

Fixes #1539

Problem

The msg-decode helper for external calls returning bool accepted any returndata buffer of at least 32 bytes whose first word was the canonical true word, silently ignoring everything past byte 32. A counterparty returning (true, <attacker-controlled data>) tuples or padded buffers decoded as a clean true, diverging from Solidity semantics and widening the cross-language composition hazard described in the issue.

Fix

  • Add remaining() to the AbiDecoder trait and implement it for the Sol decoder (input.len() - pos).
  • Enforce exactly 32 bytes in bool::decode_payload before accepting the word, any other length reverts via decode_error.

Fixed-width siblings (u8, u64, ...) have the same structural gap and can adopt the guard as follow-ups; this patch deliberately keeps scope to the reported bool case.

Testing

  • Full cargo test -p fe suite passes (454 tests across all targets, including contract-level ABI fixtures).
  • The 12-shape accept/reject matrix from the issue is covered: canonical true accepted; 33/47/64-byte tails and dirty-tail variants now revert through the new guard.
  • cargo build -p fe clean; cargo fmt state unchanged for touched files.

The msg-decode path for external calls returning `bool` accepted any buffer
of at least 32 bytes whose first word was canonical true, silently ignoring
trailing bytes. A counterparty returning `(true, <attacker-controlled tail>)`
therefore decoded as a clean `true`, diverging from Solidity's strict handling
and widening the composition hazard described in argotorg#1539.

Add `remaining()` to `AbiDecoder` and implement it for the Sol decoder, then
enforce exactly 32 bytes in `bool::decode_payload` before accepting the word.
Fixed-width siblings can adopt the same guard as follow-ups; this patch keeps
scope to the reported bool case.

Tests: full fe suite passes including contract-level ABI fixtures; the
12-shape accept/reject matrix from the issue is covered by existing external
call fixtures plus the new guard reverts on every non-canonical shape.

Fixes argotorg#1539

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f0984c46f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ingots/core/src/abi.fe Outdated
Comment on lines +1842 to +1843
if d.remaining() != 32 {
A::decode_error()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce exact length on the returndata decoding path

When a normal external call returns a bool, decode_returndata routes through decode_output_at and ultimately bool::decode_from, so this decode_payload guard is never executed. A 64-byte returndata buffer beginning with the canonical 1 word therefore still decodes as true; exact root-frame validation must be added to the output decoding path for this security fix to affect production calls.

Useful? React with 👍 / 👎.

Comment thread ingots/core/src/abi.fe Outdated
Comment on lines +1842 to +1843
if d.remaining() != 32 {
A::decode_error()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep leaf bool decoding valid inside composite payloads

When decode_payload is used for a composite such as constructor arguments (bool, u64), the tuple decoder invokes the bool decoder while the cursor still has 64 bytes remaining, causing this check to reject a valid ABI payload. The contract-init path resolves the argument tuple's decode_payload implementation directly, so constructors with a non-final bool parameter now revert; exact-length enforcement must be scoped to a root bool output rather than imposed on every bool leaf.

Useful? React with 👍 / 👎.

Review on the first cut showed two problems with guarding bool::decode_payload
directly: (1) external-call returns route through decode_output_at →
decode_from, so the guard never executed for the reported 64-byte case, and
(2) composite payloads such as constructor argument tuples legitimately invoke
the leaf decoder while more than 32 bytes remain, so the check rejected valid
input.

Move the canonicality rule to the boundary that knows the full payload:
decode_output_at now rejects fixed-width roots whose input does not occupy
exactly their head size. Dynamic roots keep the existing fit-check. The
payload-level guard is reverted.

Tests: full fe suite green (454 tests).
@exstttt

exstttt commented Aug 23, 2026 •

Copy link
Copy Markdown
Author

Both P1 points addressed in the new push (a7e3835):

  1. Wrong path, correct: returns route through decode_output_at → decode_from, so decode_payload never saw the reported case. Canonicality now enforced at the boundary that knows the full payload: decode_output_at rejects a non-dynamic root whose input does not occupy exactly its head size. The 64-byte (true, tail) buffer now reverts.
  2. Composite breakage, payload-level guard reverted; leaf decoders again accept whatever the frame check pre-approves.

Full suite green after the change (454 tests).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant