Skip to content

Add fail-closed reentrancy rely-guarantee gate#2032

Merged
Th0rgal merged 7 commits into
mainfrom
feat/reentrancy-rely-guarantee
Jun 18, 2026
Merged

Add fail-closed reentrancy rely-guarantee gate#2032
Th0rgal merged 7 commits into
mainfrom
feat/reentrancy-rely-guarantee

Conversation

@Th0rgal

@Th0rgal Th0rgal commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Single-function CEI checks (cei_safe, allow_post_interaction_writes) do not prevent cross-function reentrancy. A CEI-clean function that makes an external call leaves transiently-exploitable state live during its callback, which a reentrant sibling function can exploit — this is the Midnight take()/liquidate() callback bug, and it previously passed verification.

This PR adds a dedicated fail-closed pass, validateReentrancyDisposition (Compiler/CompilationModel/Validation.lean), that runs after call well-formedness and separate from the single-function CEI check:

  • Trigger: any function whose body opens a reentrancy window (an external call or a raw low-level call opcode) and that is not view/pure.
  • Sound accept-set: nonreentrant(<lock>) (runtime guard) or reentrancy_trusted (audited, author-asserted). CEI tags no longer satisfy the gate — CEISafety is demoted.
  • reentrancy_trusted is metadata-only (no codegen, no proof obligation), so retrofitted contracts' runtime Yul is byte-identical. Existing smoke contracts that open windows are retrofitted with the appropriate tag.

AUDIT.md and TRUST_ASSUMPTIONS.md are updated to record the new trust boundary, per the synchronization non-negotiable.

Validation

  • lake build — green (full, incl. proofs)
  • make check — green ("All checks passed")
  • forge build (difftest profile) — green, 81 files
  • Targeted suite — 38/38 (incl. full reentrancy property suite)
  • Differential (Counter / SimpleStorage / Owned, reduced counts) — 23/23
  • Full non-differential — 436/436 (one initial failure, LowLevelTryCatchSmoke, was a genuine catch by the new gate of a contract that only compiles via the CLI --module FFI path and slipped the build-time retrofit; fixed by tagging its 3 functions → 3/3)
  • Post-fix FFI-compiled smoke contracts (all 9) — 33/33

Test plan

  • CI lake build passes
  • CI make check passes
  • Foundry differential + property suites pass
  • Confirm a CEI-clean-but-window-opening function without a disposition is rejected (negative test: PropertyReentrancyDispositionDeclared)
  • Confirm retrofitted contracts' Yul is byte-identical (no codegen drift)

Note

High Risk
Changes compiler acceptance for every mutating function with external or low-level call windows and introduces an unverified reentrancy_trusted trust boundary; mistakes would silently weaken reentrancy policy across the contract corpus.

Overview
This PR closes the cross-function reentrancy gap where a CEI-clean function with an external call could still leave mid-update state exploitable by a sibling entrypoint (Midnight take/liquidate shape). Compilation now runs validateReentrancyDisposition after call well-formedness checks: any non-view/pure function whose body stmtOpensReentrancyWindow must declare nonreentrant(<lock>) or the new metadata-only reentrancy_trusted author assertion. cei_safe / allow_post_interaction_writes no longer satisfy this gate; CEISafety is documented as single-function ordering only. Window detection treats staticcall-summarised ECMs as non-opening; mutating calls, raw Yul calls, and other external forms still trigger the gate.

reentrancy_trusted threads through the macro into FunctionSpec.reentrancyTrusted (no codegen). Existing smoke and test specs that open windows are retrofitted with the tag so builds stay green while runtime Yul stays unchanged for those contracts.

Macro lowering adds ensureCallableAsInternalHelper: a lock-only nonreentrant entry cannot be called as an internal helper (the shadow path drops the transient guard); reentrancy_trusted on the callee is the accepted exemption. SecurityCombos.lean pins rejection/acceptance pairs with #guard_msgs.

Additively, Verity.Core.Invariant / Verity.Core.Reentrancy, Env.reenter, and Contracts/ReentrancyRelyGuarantee machine-check the buggy vs locked take story (+8 proven theorems, proof-only in CI). Audit/trust docs, verification counters, property manifests, and selector/property-test generators are updated for the new modifier and contracts.

Reviewed by Cursor Bugbot for commit 3945e77. Bugbot is set up for automated code reviews on this repo. Configure here.

Single-function CEI checks (cei_safe, allow_post_interaction_writes)
do not prevent cross-function reentrancy: a CEI-clean function that
makes an external call leaves transiently-exploitable state live during
its callback, exploitable by a reentrant sibling function. This is the
Midnight take()/liquidate() callback bug, which previously passed
verification.

Add a dedicated fail-closed pass, validateReentrancyDisposition, that
runs after call well-formedness and separate from the CEI check. Any
function that opens a reentrancy window (external call or raw low-level
call opcode) and is not view/pure is rejected unless it carries a sound
disposition: nonreentrant(<lock>) (runtime guard) or reentrancy_trusted
(audited author assertion). CEISafety is demoted: its tags no longer
satisfy the gate.

reentrancy_trusted is metadata-only (no codegen, no proof obligation),
so retrofitted contracts' runtime Yul is byte-identical. Existing smoke
contracts that open windows are retrofitted with the appropriate tag.

AUDIT.md and TRUST_ASSUMPTIONS.md updated to record the new trust
boundary per the synchronization non-negotiable.
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
verity Ready Ready Preview, Comment Jun 18, 2026 7:13am

Request Review

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor
\n### CI Failure Hints\n\nFailed jobs: `checks`\n\nCopy-paste local triage:\n```bash\nmake check\nlake build\nFOUNDRY_PROFILE=difftest forge test -vv\n```

…antee gate (#2032)

Annotate the unsafeYul raw-call AST fixture (reentrancyTrusted) and the
macro-DSL external-call smoke specs (CallWithValueSmoke.execute/executeBytes,
MacroExternal.storeEcho) with reentrancy_trusted so CompilationModelFeatureTest
compiles under the fail-closed gate. Codegen-preserving: reentrancy_trusted
asserts trust without injecting a runtime guard, so emitted Yul and trust
reports are unchanged.
Comment thread Compiler/CompilationModel/Validation.lean
…nerators

The macro property-test generator (generate_macro_property_tests.py) and the
selector verifier (check_selectors.py) matched `function <name> (...)` but not
the optional mutability modifiers that sit between `function` and the name
(Verity/Macro/Syntax.lean). A function annotated with one of these modifiers —
e.g. `function reentrancy_trusted hashLeaves (...)` introduced by this PR —
captured the modifier keyword as the name, failed at `(`, and was silently
dropped: the externally-dispatchable function got no property test and no
selector check, while CI stayed green (an empty test file still compiles).

Both regexes now consume any number of leading modifiers
(payable/view/pure/no_external_calls/allow_post_interaction_writes/cei_safe/
reentrancy_trusted/nonreentrant(<lock>)) before capturing the identifier.
`internal` is deliberately omitted: internal helpers carry no external
selector, so leaving them unmatched keeps them correctly excluded.

Regenerated artifacts/macro_property_tests via the CI wrapper; 27 suites
restored their previously-dropped functions (e.g. the 5 reentrancy_trusted
entrypoints of LinkedExternalDynamicArgSmoke). InternalHelperSmoke stays
unchanged. Fixes the Bugbot MEDIUM finding.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2bf5b52. Configure here.

Comment thread Verity/Macro/Translate.lean
…#2032)

The nonreentrant(<lock>) transient guard is synthesised only at the external
dispatch boundary (attachNonReentrantGuard), so it is absent on the lock-free
internal-helper shadow the macro emits for direct intra-contract calls. The
shadow's reentrancyTrusted flag is synthesised from the lock (not an author
assertion), so a public entry that internalCalls such a function would run the
guarded body without the lock, silently bypassing the only reentrancy
protection (Bugbot HIGH on this PR).

Split ensureSupportsInternalHelperSpec (type-support only) from new
ensureCallableAsInternalHelper, which additionally fail-closes any internal
call to a function with nonReentrantLock set and reentrancy_trusted unset.
reentrancy_trusted remains the sound exemption (caller-agnostic author
assertion). Declaration-site rejection of `internal nonreentrant(<lock>)`
(#1971) is preserved by leaving the declaration-validation path on the
type-only check.

Tests: NonreentrantCalledAsInternalHelperRejected (public nonreentrant entry
called as helper -> rejected) and NonreentrantTrustedInternalHelperAccepted
(same call accepted once callee adds reentrancy_trusted). AUDIT.md and
TRUST_ASSUMPTIONS.md document the internal-helper closure.
@Th0rgal Th0rgal merged commit 47eba4b into main Jun 18, 2026
22 checks passed
@Th0rgal Th0rgal deleted the feat/reentrancy-rely-guarantee branch June 18, 2026 09:09
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.

1 participant