Skip to content

feat(lint): detect unsafe ecrecover#15765

Open
mattsse wants to merge 5 commits into
foundry-rs:masterfrom
mattsse:mattsse/lint-ecrecover
Open

feat(lint): detect unsafe ecrecover#15765
mattsse wants to merge 5 commits into
foundry-rs:masterfrom
mattsse:mattsse/lint-ecrecover

Conversation

@mattsse

@mattsse mattsse commented Jul 15, 2026

Copy link
Copy Markdown
Member

This adds the medium-severity ecrecover lint from #14381. It reports direct uses of Solidity's builtin unless the signature's s value is proven to be in the canonical lower half of the secp256k1 order, with path-sensitive handling for guards, aliases, branches, loops, assignments, mutable calls, and Solidity constant expressions.

A locally stored recovery is deferred until its first observable use, so a dominating post-call low-s guard can validate the exact signature value before the signer affects behavior. Pending recoveries survive joins when any path remains unguarded, while pre-guard reads, state writes, reassigned signature values, and implicit named-return uses still report. The UI fixture also covers comparison polarity, control-flow exits, tuple and ternary assignments, loop-carried writes, mutable-state invalidation, builtin resolution, and safe counterexamples.

The companion Book documentation is in foundry-rs/book#1981, with the post-call guard follow-up in foundry-rs/book#1994. This PR was implemented with Codex assistance across code, tests, and lint documentation, then independently reviewed and corrected by a fresh Codex context.

mattsse added 2 commits July 15, 2026 15:42
Track canonical low-s checks through local control flow and aliases so direct ecrecover calls warn only when signature malleability is not ruled out.
Invalidate low-s facts across loop backedges and calls that may mutate state. Preserve simultaneous assignment semantics, wrapped constants, and control-flow joins so safe calls stay quiet without hiding reachable high-s calls.

@mablr mablr 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

mablr
mablr previously approved these changes Jul 15, 2026
Comment thread crates/lint/testdata/Ecrecover.sol
Defer diagnostics for ecrecover results stored locally until the result is read or a matching low-s guard validates the signature value. Preserve pending results across control-flow joins so partial guards remain unsafe.
Comment thread crates/lint/src/sol/med/ecrecover.rs
Comment thread crates/lint/docs/ecrecover.md Outdated
Follow only feasible constant branches and propagate selected exits so unreachable ecrecover calls are ignored. Document supported post-call low-s guards.
@mattsse
mattsse requested a review from mablr July 16, 2026 20:15
Comment thread crates/lint/src/sol/med/ecrecover.rs
Comment thread crates/lint/src/sol/med/ecrecover.rs Outdated
Prune infeasible expression arms when tracking calls and low-s facts. Carry deferred recoveries through tuple and nested local assignments while treating observable state writes as immediate uses.
@mattsse
mattsse requested a review from mablr July 16, 2026 23:13

@mablr mablr 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

return ControlFlow::Continue(());
}
StmtKind::Err(_) | StmtKind::AssemblyBlock(_) => {
self.state = FlowState::default();

@stevencartavia stevencartavia Jul 17, 2026

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.

Resetting FlowState here also drops deferred pending recoveries, allowing an assembly block to suppress an expected ecrecover warning. For example, address signer = ecrecover(...); assembly { pop(0) } return signer; currently produces no warning. I reproduced this with unrelated, empty, and memory-safe assembly blocks; moving the same assembly before ecrecover warns correctly. One option would be to treat AssemblyBlock as an opaque boundary and preserve or conservatively report unresolved pending recoveries instead of clearing them, while keeping overwritten recoveries clean.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants