Skip to content

Fix stub_verified infinite recursion when Arbitrary calls the stubbed function#4571

Draft
feliperodri wants to merge 1 commit intomodel-checking:mainfrom
feliperodri:fix-stub-verified-arbitrary
Draft

Fix stub_verified infinite recursion when Arbitrary calls the stubbed function#4571
feliperodri wants to merge 1 commit intomodel-checking:mainfrom
feliperodri:fix-stub-verified-arbitrary

Conversation

@feliperodri
Copy link
Copy Markdown
Contributor

@feliperodri feliperodri commented Apr 5, 2026

Problem

When a type's kani::Arbitrary implementation calls a function that is the target of #[kani::stub_verified], verification hits infinite recursion. The contract replacement (ContractMode::Replace) applies globally, including inside Arbitrary::any(), so test input generation itself invokes the contract abstraction instead of the real function.

impl kani::Arbitrary for Wrapper {
    fn any() -> Self {
        Wrapper::new(kani::any())  // new() calls normalize()
    }
}

#[kani::proof]
#[kani::stub_verified(Wrapper::normalize)]  // replaces ALL calls to normalize
fn check() {
    let w: Wrapper = kani::any();  // Arbitrary → new() → normalize() → STUBBED → recursion
}

Solution

kani::any() now tracks nesting depth via a global counter (ARBITRARY_NESTING_DEPTH). The contract REPLACE match arm checks this counter — when inside an Arbitrary context (depth > 0), it executes the original function body instead of the contract replacement.

Implementation details

  • RAII guard (ArbitraryContextGuard): Increments the counter on creation, decrements on drop. Ensures correctness even if T::any() panics during concrete playback.
  • All paths covered: kani::any(), any_where(), write_any_slim(), and write_any_slice() all go through the guard.
  • Wrapping arithmetic: Avoids CBMC overflow checks on the counter.
  • Contract bootstrap change: The REPLACE arm in bootstrap.rs checks in_arbitrary_context() and falls back to #block (the original function body) when true.

Soundness

Excluding Arbitrary from stub replacement is sound: the real function produces a subset of valid values; the stub produces a superset. Using the real function gives tighter (more precise) input generation, not less sound verification.

Testing

  • stub_verified_arbitrary_fix.rs — Regression test: Wrapper::Arbitrary calls normalize() (the stubbed function). Previously caused infinite recursion, now works.
  • stub_verified_safe_arbitrary.rs — Derived Arbitrary (doesn't call stubbed function) works with stub_verified.
  • stub_verified_arbitrary_workaround.rs — Documents the standalone proof pattern as an alternative.

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 5, 2026
@feliperodri feliperodri added the Z-Contracts Issue related to code contracts label Apr 5, 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 5, 2026
@feliperodri feliperodri marked this pull request as ready for review April 5, 2026 18:56
@feliperodri feliperodri requested a review from a team as a code owner April 5, 2026 18:56
@feliperodri feliperodri marked this pull request as draft April 5, 2026 21:46
@feliperodri feliperodri self-assigned this Apr 19, 2026
@feliperodri feliperodri force-pushed the fix-stub-verified-arbitrary branch 4 times, most recently from 70828a2 to 7812142 Compare April 19, 2026 19:44
@feliperodri feliperodri marked this pull request as ready for review April 19, 2026 19:46
…ry 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
@feliperodri feliperodri force-pushed the fix-stub-verified-arbitrary branch from 7812142 to afdd4ab Compare April 19, 2026 22:38
@feliperodri feliperodri marked this pull request as draft April 19, 2026 23:52
@feliperodri
Copy link
Copy Markdown
Contributor Author

I attempted to implement a runtime fix (nesting depth counter) but it conflicts with CBMC's DFCC assigns checking: writes to global mutable state inside contract-checked scopes trigger assigns violations. CBMC provides no mechanism to exempt infrastructure writes from DFCC tracking. So back to the drawing board...

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

Labels

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.

2 participants