Fix stub_verified infinite recursion when Arbitrary calls the stubbed function#4571
Draft
feliperodri wants to merge 1 commit intomodel-checking:mainfrom
Draft
Fix stub_verified infinite recursion when Arbitrary calls the stubbed function#4571feliperodri wants to merge 1 commit intomodel-checking:mainfrom
stub_verified infinite recursion when Arbitrary calls the stubbed function#4571feliperodri wants to merge 1 commit intomodel-checking:mainfrom
Conversation
Ari4ka
approved these changes
Apr 8, 2026
70828a2 to
7812142
Compare
…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
7812142 to
afdd4ab
Compare
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... |
Ari4ka
approved these changes
Apr 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a type's
kani::Arbitraryimplementation calls a function that is the target of#[kani::stub_verified], verification hits infinite recursion. The contract replacement (ContractMode::Replace) applies globally, including insideArbitrary::any(), so test input generation itself invokes the contract abstraction instead of the real function.Solution
kani::any()now tracks nesting depth via a global counter (ARBITRARY_NESTING_DEPTH). The contractREPLACEmatch arm checks this counter — when inside an Arbitrary context (depth > 0), it executes the original function body instead of the contract replacement.Implementation details
ArbitraryContextGuard): Increments the counter on creation, decrements on drop. Ensures correctness even ifT::any()panics during concrete playback.kani::any(),any_where(),write_any_slim(), andwrite_any_slice()all go through the guard.REPLACEarm inbootstrap.rschecksin_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::Arbitrarycallsnormalize()(the stubbed function). Previously caused infinite recursion, now works.stub_verified_safe_arbitrary.rs— DerivedArbitrary(doesn't call stubbed function) works withstub_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.