Skip to content

fix(symbolic): require ecrecover scalars in [1, n) - #18347

Open
figtracer wants to merge 3 commits into
mablr/evm2-migrationfrom
fig/symbolic-ecrecover-zero-rs
Open

figtracer wants to merge 3 commits into
mablr/evm2-migrationfrom
fig/symbolic-ecrecover-zero-rs

Conversation

@figtracer

Copy link
Copy Markdown
Member

Why

Symbolic ecrecover treated signatures with r = 0 or s = 0 as recoverable, so true properties reported a counterexample that did not replay. Found by symdiff (Tempo).

[FAIL: incomplete symbolic execution (Error): symbolic counterexample did not replay] check_zero_r(bytes32)

Change

Symbolic ecrecover validity also requires r and s nonzero and below the secp256k1 order. Case folded into symbolic_ecrecover_return_data_conformance.

Downstream

Base mablr/evm2-migration. No public behavior change outside the buggy case.

Symbolic ecrecover checked only `v`, so a signature with `r = 0` or `s = 0` counted as recoverable and a true property reported a counterexample that did not replay. Require `r` and `s` in `[1, n)` as the precompile does; outside that range it returns no data.

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please address the ecrecover counterexample replay regression below.

));
valid.push(scalar.nonzero_bool(cx));
}
let valid_v = SymBoolExpr::and(cx, valid);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please preserve replayable malformed-signature witnesses when adding this predicate. The existing ecrecover_basic_modeling test (assert(ecrecover(h, v, r, s) != address(0)) with unconstrained inputs) now reports incomplete symbolic execution (Error): symbolic counterexample did not replay instead of the expected replay-confirmed assertion failure; the head CI job reproduces this on all three attempts. There are trivial concrete counterexamples (for example all-zero inputs), so this loses an existing counterexample-finding capability and breaks the CLI suite. The scalar bounds are correct, but witness selection/replay needs to continue finding a concrete invalid signature rather than stopping at the abstract recovery mismatch.

With `r` and `s` range-checked, the solver picked well-formed inputs whose modeled recovery failed through the opaque term, and concrete ecrecover recovered an address, so the counterexample did not replay. Fork a symbolic ECRECOVER call: the malformed path returns no data and continues first, so its counterexamples replay exactly; the well-formed path keeps the opaque recovery model.
@figtracer
figtracer requested a review from mattsse November 10, 2026 00:58

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The previous replay finding is fixed. One redirected-call regression remains below.

}
state.constraints =
if malformed_sat { malformed_constraints } else { well_formed_constraints };
let outcome = execute_symbolic_precompile(&mut self.cx, to, input, input_len, spec_id)?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use the resolved code_address for precompile execution here and in the both-feasible branch. The caller dispatches this helper using code_address, but passes the original to; these differ after vm.mockFunction. For example, mock address(4) to address(1) for abi.encode(bytes32(0), uint256(27), bytes32(0), bytes32(0)), then staticcall address(4): concrete execution and the previous symbolic implementation return empty ecrecover output, whereas this call executes identity and returns all 128 input bytes. Redirecting an ordinary contract instead produces unsupported precompile. Keep the original recipient for call effects, but execute the resolved ecrecover precompile.

Run the resolved precompile address after mockFunction redirects a call. Keep the original recipient for call effects. Concrete fuzz and symbolic CLI tests agree on the empty ecrecover result.
@figtracer
figtracer requested a review from mattsse November 10, 2026 03:42

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

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

Development

Successfully merging this pull request may close these issues.

2 participants