Skip to content

Reject mutual recursion with #[kani::recursion]#4580

Merged
feliperodri merged 1 commit intomodel-checking:mainfrom
feliperodri:warn-mutual-recursion-contracts
Apr 22, 2026
Merged

Reject mutual recursion with #[kani::recursion]#4580
feliperodri merged 1 commit intomodel-checking:mainfrom
feliperodri:warn-mutual-recursion-contracts

Conversation

@feliperodri
Copy link
Copy Markdown
Contributor

@feliperodri feliperodri commented Apr 19, 2026

Emit a compilation error when a function with #[kani::recursion] is involved in mutual recursion. The per-function REENTRY mechanism only handles direct recursion soundly since mutual recursion silently produces unsound verification results.

Contributes to #3316, #3273.

Problem

The #[kani::recursion] attribute uses a per-function REENTRY flag to detect when a function calls itself, replacing the recursive call with the function's contract. For mutual recursion (f calls g, g calls f), the REENTRY flag for g is never set when verifying f's contract, so g's body executes fully instead of being replaced by its contract. Verification succeeds but the result is meaningless — a silent soundness gap.

Solution

check_mutual_recursion() in the contract transform pass scans the MIR body of functions in RecursiveCheck mode for calls to other functions that also have #[kani::recursion] and a contract. For each such callee, it checks if the callee's body calls back to the original function. If so, it emits a span_err (not a warning) because the result would be unsound.

This is an error rather than a warning because soundness violations should not be silently ignorable.

Limitations

  • Only detects one level of indirection (f→g→f), not deeper chains (f→g→h→f).
  • Reports only the first mutual-recursive callee per function.

Example error

error: `#[kani::recursion]` is used on `mutual_a`, which calls `mutual_b`
       that calls back to `mutual_a`. Mutual recursion is not supported by
       contract verification and may produce unsound results. Only direct
       recursion (a function calling itself) is handled correctly.
  --> tests/expected/function-contract/mutual_recursion_unsound.rs:12:30

Testing

  • mutual_recursion_unsound.rs — two mutually recursive functions (mutual_amutual_b) with contracts and #[kani::recursion], verifying the error is emitted.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@feliperodri feliperodri added this to the Contracts milestone Apr 19, 2026
@feliperodri feliperodri added [C] Feature / Enhancement A new feature request or enhancement to an existing feature. [F] Soundness Kani failed to detect an issue Z-Contracts Issue related to code contracts labels Apr 19, 2026
@github-actions github-actions Bot added Z-EndToEndBenchCI Tag a PR to run benchmark CI Z-CompilerBenchCI Tag a PR to run benchmark CI labels Apr 19, 2026
@feliperodri feliperodri force-pushed the warn-mutual-recursion-contracts branch 2 times, most recently from 1c1e669 to 5b1c860 Compare April 19, 2026 19:06
@feliperodri feliperodri marked this pull request as ready for review April 19, 2026 19:07
@feliperodri feliperodri requested a review from a team as a code owner April 19, 2026 19:07
@rajathmCMU
Copy link
Copy Markdown

Should this be a warn or an error?

@feliperodri feliperodri force-pushed the warn-mutual-recursion-contracts branch from 5b1c860 to 00c4ba2 Compare April 22, 2026 19:26
@feliperodri feliperodri changed the title Detect and warn about mutual recursion with #[kani::recursion] Reject mutual recursion with #[kani::recursion] Apr 22, 2026
@feliperodri feliperodri force-pushed the warn-mutual-recursion-contracts branch 3 times, most recently from 5cd7305 to 0208e77 Compare April 22, 2026 19:34
…recursion]

The per-function REENTRY mechanism used by #[kani::recursion] only handles
direct recursion (f calls f) soundly. For mutual recursion (f calls g,
g calls f), the REENTRY flag for g is never set, so g's body executes
fully instead of being replaced by its contract. This is a silent
soundness gap — no error or warning was emitted.

This change adds check_mutual_recursion() in the contract transform pass.
When a function with #[kani::recursion] is being processed in
RecursiveCheck mode, we scan its MIR body for calls to other functions
that also have contracts AND #[kani::recursion]. For each such callee,
we check if the callee's body calls back to the original function. If
so, we emit a span_warn pointing at the call site.

We require both has_contract() and has_recursion() on the callee because
if the callee has a contract but no #[kani::recursion], Kani replaces
the call with the contract abstraction — no mutual recursion occurs.

Limitations:
- Only detects one level of indirection (f->g->f), not deeper chains.
- Reports only the first mutual-recursive callee per function.

Includes a test case (mutual_recursion_unsound.rs) with two mutually
recursive functions that triggers the warning.
@feliperodri feliperodri force-pushed the warn-mutual-recursion-contracts branch from 0208e77 to e1ccef1 Compare April 22, 2026 19:36
@feliperodri feliperodri enabled auto-merge April 22, 2026 19:38
@feliperodri feliperodri added this pull request to the merge queue Apr 22, 2026
Merged via the queue into model-checking:main with commit 33bfc86 Apr 22, 2026
34 checks passed
@feliperodri feliperodri deleted the warn-mutual-recursion-contracts branch April 22, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[C] Feature / Enhancement A new feature request or enhancement to an existing feature. [F] Soundness Kani failed to detect an issue Z-CompilerBenchCI Tag a PR to run benchmark CI Z-Contracts Issue related to code contracts Z-EndToEndBenchCI Tag a PR to run benchmark CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants