Skip to content

Commit 00c4ba2

Browse files
committed
feat(contracts): Detect and warn about mutual recursion with #[kani::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.
1 parent d2c6dca commit 00c4ba2

3 files changed

Lines changed: 103 additions & 0 deletions

File tree

kani-compiler/src/kani_middle/transform/contracts.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ impl TransformPass for FunctionWithContractPass {
302302
match instance.ty().kind().rigid().unwrap() {
303303
RigidTy::FnDef(def, args) => {
304304
if let Some(mode) = self.contract_mode(tcx, *def) {
305+
if mode == ContractMode::RecursiveCheck {
306+
check_mutual_recursion(tcx, *def, &body);
307+
}
305308
self.mark_unused(tcx, *def, &body, mode);
306309
let new_body = self.set_mode(tcx, body, mode);
307310
(true, new_body)
@@ -552,3 +555,76 @@ fn find_closure(tcx: TyCtxt, fn_def: FnDef, body: &Body, name: &str) -> ClosureD
552555
unreachable!()
553556
})
554557
}
558+
559+
/// Check if a function with `#[kani::recursion]` is involved in mutual recursion.
560+
///
561+
/// Scans the function's MIR body for calls to other functions that also have
562+
/// `#[kani::recursion]`. For each such callee, checks if the callee's body calls
563+
/// back to the original function (one level of indirection). If so, emits a
564+
/// warning because the per-function REENTRY mechanism only handles direct
565+
/// recursion soundly.
566+
///
567+
/// We require both `has_contract()` and `has_recursion()` on the callee because
568+
/// if the callee has a contract but no `#[kani::recursion]`, Kani replaces the
569+
/// callee with its contract abstraction — no mutual recursion occurs.
570+
///
571+
/// Limitations:
572+
/// - Only detects one level of indirection (f→g→f), not deeper chains (f→g→h→f).
573+
/// - Reports only the first mutual-recursive callee per function.
574+
fn check_mutual_recursion(tcx: TyCtxt, fn_def: FnDef, body: &Body) {
575+
let fn_name = tcx.item_name(rustc_internal::internal(tcx, fn_def.def_id()));
576+
let locals = body.locals().to_vec();
577+
578+
for bb in body.blocks.iter() {
579+
let TerminatorKind::Call { func, .. } = &bb.terminator.kind else { continue };
580+
let Ok(func_ty) = func.ty(&locals) else { continue };
581+
let TyKind::RigidTy(RigidTy::FnDef(callee_def, callee_args)) = func_ty.kind() else {
582+
continue;
583+
};
584+
585+
// Skip direct recursion (that's handled correctly by REENTRY).
586+
if callee_def.def_id() == fn_def.def_id() {
587+
continue;
588+
}
589+
590+
// Only warn when the callee also uses #[kani::recursion] with a contract.
591+
// If the callee has a contract but no #[kani::recursion], Kani replaces
592+
// the call with the contract abstraction, so no mutual recursion occurs.
593+
let callee_attrs = KaniAttributes::for_def_id(tcx, callee_def.def_id());
594+
if !callee_attrs.has_contract() || !callee_attrs.has_recursion() {
595+
continue;
596+
}
597+
598+
// Check if the callee calls back to us (one level deep).
599+
let Ok(callee_instance) = Instance::resolve(callee_def, &callee_args) else { continue };
600+
let Some(callee_body) = callee_instance.body() else { continue };
601+
let callee_locals = callee_body.locals().to_vec();
602+
603+
for callee_bb in callee_body.blocks.iter() {
604+
let TerminatorKind::Call { func: callee_func, .. } = &callee_bb.terminator.kind else {
605+
continue;
606+
};
607+
let Ok(callee_func_ty) = callee_func.ty(&callee_locals) else { continue };
608+
let TyKind::RigidTy(RigidTy::FnDef(transitive_def, _)) = callee_func_ty.kind() else {
609+
continue;
610+
};
611+
612+
if transitive_def.def_id() == fn_def.def_id() {
613+
let callee_name = tcx.item_name(rustc_internal::internal(tcx, callee_def.def_id()));
614+
let span = rustc_internal::internal(tcx, bb.terminator.span);
615+
tcx.dcx().span_err(
616+
span,
617+
format!(
618+
"`#[kani::recursion]` is used on `{fn_name}`, which calls \
619+
`{callee_name}` that calls back to `{fn_name}`. \
620+
Mutual recursion is not supported by contract verification \
621+
and may produce unsound results. Only direct recursion \
622+
(a function calling itself) is handled correctly."
623+
),
624+
);
625+
// LIMITATION: Only report the first mutual-recursive callee.
626+
return;
627+
}
628+
}
629+
}
630+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`#[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.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
// kani-flags: -Zfunction-contracts
4+
5+
//! Test that Kani detects mutual recursion in functions with contracts and
6+
//! emits a warning about unsoundness.
7+
8+
#[kani::requires(x < 100)]
9+
#[kani::ensures(|&result| result <= x)]
10+
#[kani::recursion]
11+
fn mutual_a(x: u32) -> u32 {
12+
if x == 0 { 0 } else { mutual_b(x - 1) }
13+
}
14+
15+
#[kani::requires(x < 100)]
16+
#[kani::ensures(|&result| result <= x)]
17+
#[kani::recursion]
18+
fn mutual_b(x: u32) -> u32 {
19+
if x == 0 { 0 } else { mutual_a(x - 1) }
20+
}
21+
22+
#[kani::proof_for_contract(mutual_a)]
23+
fn check_mutual_a() {
24+
let x: u32 = kani::any();
25+
mutual_a(x);
26+
}

0 commit comments

Comments
 (0)