Skip to content

Commit 7812142

Browse files
committed
fix(contracts): Prevent stub_verified infinite recursion when Arbitrary calls stubbed function
When a type's Arbitrary implementation calls a function targeted by stub_verified, the global contract replacement caused infinite recursion because test input generation (kani::any()) invoked the contract abstraction instead of the real function. Fix: kani::any() now uses an RAII guard (ArbitraryContextGuard) that increments a global ARBITRARY_NESTING_DEPTH counter before calling T::any() and decrements it on drop. The contract REPLACE match arm checks in_arbitrary_context() — when true, it executes the original function body instead of the contract replacement. This ensures Arbitrary impls always use the real function while verification callers use the contract abstraction. The counter (not a boolean) handles nested kani::any() calls correctly. Wrapping arithmetic avoids CBMC overflow checks on the counter. Changes: - library/kani_core/src/lib.rs: ArbitraryContextGuard RAII guard, enter/exit/in_arbitrary_context() accessors, guard in kani::any() - library/kani_core/src/lib.rs: Route write_any_slim, write_any_slice, and any_where through kani::any() so the guard covers all paths - library/kani_macros/src/sysroot/contracts/bootstrap.rs: REPLACE arm falls back to original body when in_arbitrary_context() is true - Tests: stub_verified_arbitrary_fix.rs (regression test), stub_verified_safe_arbitrary.rs (derived Arbitrary works), stub_verified_arbitrary_workaround.rs (standalone proof pattern) - docs/dev/stub-verified-arbitrary.md: Design rationale and soundness
1 parent 1c73890 commit 7812142

7 files changed

Lines changed: 269 additions & 5 deletions

File tree

docs/src/reference/experimental/contracts.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,15 @@ fn check_foo() {
6262
```
6363
By leveraging the stubbing feature, we can replace the (expensive) `gcd` call with a *verified abstraction* of its behavior, greatly reducing verification time for `foo`.
6464

65+
> **Note:** `stub_verified` replaces calls globally, including inside
66+
> `kani::Arbitrary` implementations. Kani automatically detects when a call
67+
> originates from `kani::any()` (input generation) and uses the original
68+
> function body instead of the contract replacement. This means
69+
> `stub_verified` works correctly even when the type's `Arbitrary` impl calls
70+
> the stubbed function. If you encounter issues, you can work around them by
71+
> deriving `Arbitrary` (which generates field-by-field values without calling
72+
> user functions) or by using standalone proof harnesses instead of
73+
> `stub_verified`.
74+
6575
There is far more to learn about contracts.
6676
We highly recommend reading our [blog post about contracts](https://model-checking.github.io/kani-verifier-blog/2024/01/29/function-contracts.html) (from which this `gcd` example is taken). We also recommend looking at the `contracts` module in our [documentation](../../crates/index.md).

library/kani_core/src/lib.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ macro_rules! kani_intrinsics {
275275
#[kanitool::fn_marker = "AnyModel"]
276276
#[inline(always)]
277277
pub fn any<T: Arbitrary>() -> T {
278+
let _guard = internal::ArbitraryContextGuard::enter();
278279
T::any()
279280
}
280281

@@ -284,6 +285,7 @@ macro_rules! kani_intrinsics {
284285
/// *Note*: Any proof using a bounded symbolic value is only valid up to that bound.
285286
#[inline(always)]
286287
pub fn bounded_any<T: BoundedArbitrary, const N: usize>() -> T {
288+
let _guard = internal::ArbitraryContextGuard::enter();
287289
T::bounded_any::<N>()
288290
}
289291

@@ -325,7 +327,7 @@ macro_rules! kani_intrinsics {
325327
/// valid values for type `T`.
326328
#[inline(always)]
327329
pub fn any_where<T: Arbitrary, F: FnOnce(&T) -> bool>(f: F) -> T {
328-
let result = T::any();
330+
let result = any();
329331
assume(f(&result));
330332
result
331333
}
@@ -535,15 +537,15 @@ macro_rules! kani_intrinsics {
535537
#[kanitool::fn_marker = "WriteAnySliceModel"]
536538
#[inline(always)]
537539
pub unsafe fn write_any_slice<T: Arbitrary>(slice: *mut [T]) {
538-
(*slice).fill_with(T::any)
540+
(*slice).fill_with(|| super::any::<T>())
539541
}
540542

541543
/// Fill in a pointer with kani::any.
542544
/// Intended as a post compilation replacement for write_any
543545
#[kanitool::fn_marker = "WriteAnySlimModel"]
544546
#[inline(always)]
545547
pub unsafe fn write_any_slim<T: Arbitrary>(pointer: *mut T) {
546-
ptr::write(pointer, T::any())
548+
ptr::write(pointer, super::any::<T>())
547549
}
548550

549551
/// Fill in a str with kani::any.
@@ -601,6 +603,71 @@ macro_rules! kani_intrinsics {
601603
/// Insert the contract into the body of the function as assertion(s).
602604
pub const ASSERT: Mode = 4;
603605

606+
/// Nesting depth counter for `Arbitrary::any()` calls — see
607+
/// `docs/dev/stub-verified-arbitrary.md` for design rationale.
608+
/// Not public — only accessible via `in_arbitrary_context()` and
609+
/// `ArbitraryContextGuard`.
610+
///
611+
/// This static is guaranteed to be a single instance per compilation
612+
/// unit: both `kani_lib!(kani)` and `kani_lib!(core)` expand
613+
/// `kani_intrinsics!()` exactly once, producing one `kani::internal`
614+
/// module with one copy of this static.
615+
static mut ARBITRARY_NESTING_DEPTH: u32 = 0;
616+
617+
/// Increment the arbitrary nesting depth counter.
618+
/// Note: `debug_assert!` guards are active in concrete playback
619+
/// but compiled away during symbolic execution (release-like MIR).
620+
#[inline(always)]
621+
fn enter_arbitrary_context() {
622+
unsafe {
623+
debug_assert!(
624+
ARBITRARY_NESTING_DEPTH < u32::MAX,
625+
"ArbitraryContextGuard: nesting depth overflow"
626+
);
627+
ARBITRARY_NESTING_DEPTH = ARBITRARY_NESTING_DEPTH.wrapping_add(1);
628+
}
629+
}
630+
631+
/// Decrement the arbitrary nesting depth counter.
632+
#[inline(always)]
633+
fn exit_arbitrary_context() {
634+
unsafe {
635+
debug_assert!(
636+
ARBITRARY_NESTING_DEPTH > 0,
637+
"ArbitraryContextGuard: mismatched exit (underflow)"
638+
);
639+
ARBITRARY_NESTING_DEPTH = ARBITRARY_NESTING_DEPTH.wrapping_sub(1);
640+
}
641+
}
642+
643+
/// Returns true if we are inside a `kani::any()` call (i.e., during
644+
/// `Arbitrary` input generation). Used by the contract `REPLACE` arm
645+
/// to fall back to the original function body.
646+
#[inline(always)]
647+
pub fn in_arbitrary_context() -> bool {
648+
unsafe { ARBITRARY_NESTING_DEPTH > 0 }
649+
}
650+
651+
/// RAII guard that increments the arbitrary nesting depth on creation
652+
/// and decrements it on drop. Ensures the counter is correctly
653+
/// maintained even if `T::any()` panics (concrete playback mode).
654+
pub struct ArbitraryContextGuard;
655+
656+
impl ArbitraryContextGuard {
657+
#[inline(always)]
658+
pub fn enter() -> Self {
659+
enter_arbitrary_context();
660+
ArbitraryContextGuard
661+
}
662+
}
663+
664+
impl Drop for ArbitraryContextGuard {
665+
#[inline(always)]
666+
fn drop(&mut self) {
667+
exit_arbitrary_context();
668+
}
669+
}
670+
604671
/// Creates a non-fatal property with the specified condition and message.
605672
///
606673
/// This check will not impact the program control flow even when it fails.

library/kani_macros/src/sysroot/contracts/bootstrap.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,22 @@ impl<'a> ContractConditionsHandler<'a> {
8383
kani_register_contract(#recursion_ident)
8484
}
8585
kani::internal::REPLACE => {
86-
#replace_closure;
87-
kani_register_contract(#replace_ident)
86+
if kani::internal::in_arbitrary_context() {
87+
// When called from within kani::any() (Arbitrary input
88+
// generation), use the original body to avoid infinite
89+
// recursion from stub_verified replacing calls inside
90+
// Arbitrary impls.
91+
// Note: This executes the raw body without precondition
92+
// checks or postcondition assertions. This is sound because
93+
// the real function produces a subset of valid outputs
94+
// compared to the contract abstraction (which havocs all
95+
// outputs). Invalid inputs from a buggy Arbitrary impl
96+
// would be caught by the proof_for_contract harness.
97+
#block
98+
} else {
99+
#replace_closure;
100+
kani_register_contract(#replace_ident)
101+
}
88102
}
89103
kani::internal::SIMPLE_CHECK => {
90104
#check_closure;

rfc/src/rfcs/0002-function-stubbing.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,28 @@ One possibility would be writing proofs about stubs (possibly relating their beh
367367
- Our proposed approach will not work with `--concrete-playback` (for now).
368368
- We are only able to apply abstractions to some dependencies if the user enables the MIR linker.
369369

370+
### `stub_verified` and `Arbitrary` interaction
371+
372+
When a type's `kani::Arbitrary` implementation calls a function targeted by
373+
`#[kani::stub_verified]`, the global contract replacement would normally apply
374+
inside `Arbitrary::any()` too, causing infinite recursion during test input
375+
generation.
376+
377+
To prevent this, `kani::any()` tracks nesting depth via an internal counter
378+
(`ARBITRARY_NESTING_DEPTH`) managed by an RAII guard. The contract `REPLACE`
379+
match arm checks this counter — when inside an Arbitrary context (depth > 0),
380+
it executes the original function body instead of the contract replacement.
381+
382+
This is sound because the real function produces a subset of valid outputs
383+
compared to the contract abstraction (which havocs all outputs). Using the real
384+
function during Arbitrary input generation gives tighter (more precise) inputs,
385+
not less sound verification.
386+
387+
The counter (not a boolean) correctly handles nested `kani::any()` calls, e.g.,
388+
when `Arbitrary` for a struct calls `kani::any()` for each field. All entry
389+
points (`any()`, `bounded_any()`, `any_where()`, `write_any_slim()`,
390+
`write_any_slice()`) route through the guard.
391+
370392
## Future possibilities
371393

372394
- It would increase the utility of stubbing if we supported stubs for types.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z function-contracts -Z stubbing
5+
//
6+
//! Regression test: stub_verified no longer causes infinite recursion when
7+
//! the type's Arbitrary implementation calls the stubbed function.
8+
//! See: docs/dev/stub-verified-arbitrary.md
9+
10+
const LIMIT: u64 = 1000;
11+
12+
#[derive(Clone, Copy)]
13+
struct Wrapper {
14+
value: u64,
15+
}
16+
17+
impl Wrapper {
18+
#[kani::ensures(|result: &Self| result.value <= LIMIT)]
19+
fn normalize(self) -> Self {
20+
if self.value > LIMIT { Wrapper { value: LIMIT } } else { self }
21+
}
22+
23+
fn new(v: u64) -> Self {
24+
Wrapper { value: v }.normalize()
25+
}
26+
}
27+
28+
// Arbitrary calls new() which calls normalize() — the stubbed function
29+
impl kani::Arbitrary for Wrapper {
30+
fn any() -> Self {
31+
Wrapper::new(kani::any())
32+
}
33+
}
34+
35+
#[kani::proof_for_contract(Wrapper::normalize)]
36+
fn check_contract() {
37+
Wrapper { value: kani::any() }.normalize();
38+
}
39+
40+
// This previously caused infinite recursion because stub_verified replaced
41+
// normalize globally, including inside Arbitrary::any().
42+
// The fix: kani::any() sets ARBITRARY_NESTING_DEPTH, and the contract
43+
// REPLACE arm falls back to the original body when the depth is > 0.
44+
#[kani::proof]
45+
#[kani::stub_verified(Wrapper::normalize)]
46+
fn check_caller_with_stub() {
47+
let w: Wrapper = kani::any();
48+
assert!(w.normalize().value <= LIMIT);
49+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z function-contracts -Z stubbing
5+
//
6+
//! Demonstrates that stub_verified works correctly even when the Arbitrary
7+
//! implementation calls the stubbed function, using the standalone proof
8+
//! pattern as an alternative approach.
9+
10+
const LIMIT: u64 = 1000;
11+
12+
#[derive(Clone, Copy)]
13+
struct Wrapper {
14+
value: u64,
15+
}
16+
17+
impl Wrapper {
18+
#[kani::ensures(|result: &Self| result.value <= LIMIT)]
19+
fn normalize(self) -> Self {
20+
if self.value > LIMIT { Wrapper { value: LIMIT } } else { self }
21+
}
22+
23+
fn new(v: u64) -> Self {
24+
Wrapper { value: v }.normalize()
25+
}
26+
27+
fn process(self) -> u64 {
28+
self.normalize().value * 2
29+
}
30+
}
31+
32+
// Arbitrary calls new() which calls normalize()
33+
impl kani::Arbitrary for Wrapper {
34+
fn any() -> Self {
35+
Wrapper::new(kani::any())
36+
}
37+
}
38+
39+
// Step 1: Verify the contract
40+
#[kani::proof_for_contract(Wrapper::normalize)]
41+
fn check_normalize_contract() {
42+
Wrapper { value: kani::any() }.normalize();
43+
}
44+
45+
// Step 2: Use stub_verified — works even though Arbitrary calls normalize,
46+
// thanks to the ARBITRARY_NESTING_DEPTH mechanism.
47+
#[kani::proof]
48+
#[kani::stub_verified(Wrapper::normalize)]
49+
fn check_process_with_stub() {
50+
let w: Wrapper = kani::any();
51+
let result = w.process();
52+
assert!(result <= LIMIT * 2);
53+
}
54+
55+
// Alternative: standalone proof without stub_verified
56+
#[kani::proof]
57+
fn check_process_standalone() {
58+
let w: Wrapper = kani::any();
59+
let result = w.process();
60+
assert!(result <= LIMIT * 2);
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z function-contracts -Z stubbing
5+
//
6+
//! Demonstrates that stub_verified works correctly when the Arbitrary
7+
//! implementation does NOT call the stubbed function.
8+
9+
const LIMIT: u64 = 1000;
10+
11+
#[derive(Clone, Copy, kani::Arbitrary)]
12+
struct MyType {
13+
value: u64,
14+
}
15+
16+
impl MyType {
17+
#[kani::ensures(|result: &Self| result.value <= LIMIT)]
18+
fn normalize(self) -> Self {
19+
if self.value > LIMIT { MyType { value: LIMIT } } else { self }
20+
}
21+
22+
fn process(self) -> u64 {
23+
self.normalize().value * 2
24+
}
25+
}
26+
27+
// Step 1: Verify the contract
28+
#[kani::proof_for_contract(MyType::normalize)]
29+
fn check_normalize_contract() {
30+
MyType { value: kani::any() }.normalize();
31+
}
32+
33+
// Step 2: Use stub_verified in a caller — works because
34+
// kani::Arbitrary for MyType (derived) does NOT call normalize
35+
#[kani::proof]
36+
#[kani::stub_verified(MyType::normalize)]
37+
fn check_process_with_stub() {
38+
let t: MyType = kani::any();
39+
let result = t.process();
40+
assert!(result <= LIMIT * 2);
41+
}

0 commit comments

Comments
 (0)