Skip to content

Commit 105be6b

Browse files
authored
Fix stubbing bugs: FFI name resolution and generic parameter validation (#4565)
Fixes three stubbing bugs that block stabilization of the stubbing feature. Resolves #1953 Resolves #2686 Resolves #2673 Partial progress on #2007 ## Bug 1: Foreign function stubs fail name resolution (#2686, #2673) Functions declared in `extern "C" { fn foo(); }` blocks could not be stubbed because the name resolution algorithm (`resolve_relative`) only searched top-level module items. Foreign functions are children of `ForeignMod` HIR items, not top-level items themselves. Additionally, the stub validation in attributes.rs rejected foreign functions with "function does not have a body, but is not an extern function" — the check was intended to catch trait functions without default bodies, but it also caught `extern "C"` functions. Fix: - resolve.rs: When a name is not found as a direct module item, also search inside `ForeignMod` blocks by iterating their HIR foreign items. - attributes.rs: Exclude `is_foreign_item` from the no-body rejection. ## Bug 2: Generic parameter names must match (#1953) Stub validation compared MIR body types directly, which meant `bar<S>() -> S` was rejected as a stub for `foo<T>() -> T` because the generic parameter S produced a different Ty than T. The RFC explicitly states this should work. Fix: - stubbing/mod.rs: Build a substitution mapping the stub's generic parameters to the original's parameters by position, using `GenericArgs::identity_for_item` as the base to correctly handle parent type parameters (e.g., methods on `LocalType<T>`). Apply the substitution to the stub's types before comparing. ## Bug 3: Lifetime mismatch detection (partial, #2007) Type comparison now uses MIR body types (which have regions erased) combined with the generic renaming substitution. This means lifetime differences between the original and stub (e.g., `&'a self` vs `&char` in method-to-function stubs) no longer cause false rejections. A documentation note warns users that lifetime mismatches can still cause subtle verification failures. ## Tests New tests: - stub_foreign_function.rs — basic extern "C" function stubbing - stub_multiple_foreign_functions.rs — multiple foreign functions from one extern block - stub_generic_param_rename.rs — single generic param T → S - stub_generic_multi_param_rename.rs — multiple generic params A, B → X, Y Promoted from fixme: - fixme_issue_1953.rs → stub_generic_param_rename_simple.rs (was ignored, now passes) ## Documentation Updated docs/src/reference/experimental/stubbing.md: - New "Stubbing foreign functions" section with example - New "Stub compatibility and lifetime considerations" section warning about lifetime mismatches ## Call-outs - The generic param renaming uses `GenericArgs::identity_for_item` to get the full substitution (including parent type `params`), then overrides the function's own `params` by position. This correctly handles methods on generic types like `LocalType<T>::pub_fn`. - Region/lifetime comparison is intentionally relaxed (erased) — the old code also didn't compare lifetimes (MIR body types have regions erased). The difference is now documented. - The FFI fix only handles local extern blocks. Foreign functions from external crates (e.g., `libc::sysconf`) already worked via `resolve_in_foreign_module` when the crate was resolved; the issue was only with local extern "C" declarations. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
1 parent f2ac758 commit 105be6b

11 files changed

Lines changed: 260 additions & 15 deletions

File tree

docs/src/reference/experimental/stubbing.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,40 @@ VERIFICATION:- SUCCESSFUL
106106

107107
Kani shows that the assertion is successful, avoiding any issues that appear if we attempt to verify the code without stubbing.
108108

109+
## Stubbing foreign functions
110+
111+
Kani supports stubbing foreign functions declared in `extern` blocks. This is useful for
112+
replacing system calls, C library functions, or other FFI functions with verification-friendly
113+
implementations:
114+
115+
```rust
116+
extern "C" {
117+
fn my_c_function(input: u32) -> u32;
118+
}
119+
120+
fn my_c_function_stub(input: u32) -> u32 {
121+
input + 1
122+
}
123+
124+
#[kani::proof]
125+
#[kani::stub(my_c_function, my_c_function_stub)]
126+
fn check_with_ffi_stub() {
127+
let result = unsafe { my_c_function(42) };
128+
assert_eq!(result, 43);
129+
}
130+
```
131+
132+
## Stub compatibility and lifetime considerations
133+
134+
When validating stub compatibility, Kani compares parameter and return types after erasing
135+
lifetime information. This means that a stub with different lifetime annotations than the
136+
original will be accepted. For example, a stub returning `&'static i32` will be accepted as
137+
a replacement for a function returning `&'a i32`.
138+
139+
**Warning:** While Kani accepts such stubs, lifetime mismatches can cause subtle verification
140+
failures (e.g., "dereference failure: dead object"). If you encounter unexpected failures with
141+
stubs, check whether the lifetime annotations match.
142+
109143
## Limitations
110144

111145
In the following, we describe all the limitations of the stubbing feature.

kani-compiler/src/kani_middle/attributes.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -787,10 +787,14 @@ impl<'tcx> KaniAttributes<'tcx> {
787787
let replace_res = self.resolve_path(current_module, replace, attr.span()).map(|res| res.def());
788788

789789
if let Ok(original_res) = original_res && let Ok(replace_res) = replace_res {
790-
// Emit an error if either function is local, yet doesn't have a body.
791-
// This can happen if a user specifies a trait fn without a default body, e.g. B::bar, where B is a trait.
792-
let o_bad = original_res.krate().is_local && !original_res.has_body();
793-
let r_bad = replace_res.krate().is_local && !replace_res.has_body();
790+
// Emit an error if either the original or the replacement is local yet
791+
// doesn't have a body. This catches trait fns without a default body
792+
// (e.g., B::bar where B is a trait). Foreign functions (extern "C") are
793+
// expected to not have bodies, so we exclude them on both sides.
794+
let o_def_id = rustc_internal::internal(self.tcx, original_res.def_id());
795+
let r_def_id = rustc_internal::internal(self.tcx, replace_res.def_id());
796+
let o_bad = original_res.krate().is_local && !original_res.has_body() && !self.tcx.is_foreign_item(o_def_id);
797+
let r_bad = replace_res.krate().is_local && !replace_res.has_body() && !self.tcx.is_foreign_item(r_def_id);
794798

795799
if o_bad || r_bad {
796800
let mut err = self.tcx.dcx().struct_span_err(

kani-compiler/src/kani_middle/resolve.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ fn resolve_relative(tcx: TyCtxt, current_module: LocalModDefId, name: &str) -> R
454454
debug!(?name, ?current_module, "resolve_relative");
455455

456456
let mut glob_imports = vec![];
457+
let mut foreign_items = vec![];
458+
// Note: `find_map` short-circuits on `Some`, but the `ForeignMod` branch
459+
// always returns `None`, so foreign items are always collected even if
460+
// `find_map` returns early from a different branch. This is intentional:
461+
// direct module items take precedence over foreign items.
457462
let result = tcx.hir_module_free_items(current_module).find_map(|item_id| {
458463
let item = tcx.hir_item(item_id);
459464
if item.kind.ident().is_some_and(|ident| ident.as_str() == name) {
@@ -473,10 +478,31 @@ fn resolve_relative(tcx: TyCtxt, current_module: LocalModDefId, name: &str) -> R
473478
// since paths resolved via non-globs take precedence.
474479
glob_imports.extend(use_path.res.present_items());
475480
}
481+
// Collect foreign items declared inside `extern` blocks so we can
482+
// search them if the name is not found as a direct module item.
483+
// This handles `extern "C" { fn foo(); }` where `foo` is a child
484+
// of the ForeignMod, not a top-level module item. (fixes #2686, #2673)
485+
if let ItemKind::ForeignMod { items, .. } = item.kind {
486+
for foreign_item in items {
487+
let fi = tcx.hir_foreign_item(*foreign_item);
488+
if fi.ident.as_str() == name {
489+
foreign_items.push(fi.owner_id.def_id.to_def_id());
490+
}
491+
}
492+
}
476493
None
477494
}
478495
});
479-
result.map_or(RelativeResolution::Globs(glob_imports), RelativeResolution::Found)
496+
if let Some(def_id) = result {
497+
return RelativeResolution::Found(def_id);
498+
}
499+
// Take the first matching foreign item. If multiple `extern` blocks declare
500+
// functions with the same name, that is a user error (rustc would reject it
501+
// as a duplicate symbol), so we don't need ambiguity diagnostics here.
502+
if let Some(def_id) = foreign_items.into_iter().next() {
503+
return RelativeResolution::Found(def_id);
504+
}
505+
RelativeResolution::Globs(glob_imports)
480506
}
481507

482508
/// Resolves a path relative to a local or foreign module.

kani-compiler/src/kani_middle/stubbing/mod.rs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,21 +119,84 @@ pub fn check_compatibility(tcx: TyCtxt, old_def: FnDef, new_def: FnDef) -> Resul
119119
}
120120
// Check whether the types match. Index 0 refers to the returned value,
121121
// indices [1, `arg_count`] refer to the parameters.
122-
// TODO: We currently force generic parameters in the stub to have exactly
123-
// the same names as their counterparts in the original function/method;
124-
// instead, we should be checking for the equivalence of types up to the
125-
// renaming of generic parameters.
126-
// <https://github.com/model-checking/kani/issues/1953>
122+
// We compare types up to renaming of generic parameters (fixes #1953).
123+
// Build a substitution from new generic params to old generic params by position.
124+
// We need to handle both the function's own params AND parent type params.
125+
let old_generics = tcx.generics_of(old_def_id);
126+
let new_generics = tcx.generics_of(new_def_id);
127+
128+
let old_all_params: Vec<_> = old_generics.own_params.iter().collect();
129+
let new_all_params: Vec<_> = new_generics.own_params.iter().collect();
130+
131+
// Build identity substitution for the new function, then override with
132+
// old param names where positions match. Using identity_for_item ensures
133+
// parent type params (e.g., T in `impl<T> MyStruct<T>`) are included.
134+
let new_identity = ty::GenericArgs::identity_for_item(tcx, new_def_id);
135+
let mut rename_args: Vec<ty::GenericArg<'_>> = new_identity.iter().collect();
136+
137+
for (i, new_param) in new_all_params.iter().enumerate() {
138+
if i < old_all_params.len() {
139+
let old_param = old_all_params[i];
140+
let idx = new_param.index as usize;
141+
if idx < rename_args.len() {
142+
match (&old_param.kind, &new_param.kind) {
143+
(
144+
ty::GenericParamDefKind::Type { .. },
145+
ty::GenericParamDefKind::Type { .. },
146+
) => {
147+
rename_args[idx] = ty::GenericArg::from(ty::Ty::new_param(
148+
tcx,
149+
old_param.index,
150+
old_param.name,
151+
));
152+
}
153+
(ty::GenericParamDefKind::Lifetime, ty::GenericParamDefKind::Lifetime) => {
154+
rename_args[idx] = ty::GenericArg::from(ty::Region::new_early_param(
155+
tcx,
156+
ty::EarlyParamRegion { index: old_param.index, name: old_param.name },
157+
));
158+
}
159+
(
160+
ty::GenericParamDefKind::Const { .. },
161+
ty::GenericParamDefKind::Const { .. },
162+
) => {
163+
rename_args[idx] = ty::GenericArg::from(ty::Const::new_param(
164+
tcx,
165+
ty::ParamConst { index: old_param.index, name: old_param.name },
166+
));
167+
}
168+
_ => {} // Keep identity for mismatched kinds; type comparison will catch it
169+
}
170+
}
171+
}
172+
}
173+
174+
// Compare types from the MIR bodies with generic parameter renaming applied.
175+
// MIR body types already have regions erased, so lifetime differences
176+
// (e.g., &'a self vs &char) don't cause false mismatches.
177+
// The renaming substitution ensures different generic parameter names
178+
// (e.g., T vs S) don't cause false mismatches either (fixes #1953).
179+
// Note: Lifetime mismatches may still cause verification failures (#2007).
180+
let rename_args = tcx.mk_args(&rename_args);
181+
127182
let old_ret_ty = old_body.ret_local().ty;
128183
let new_ret_ty = new_body.ret_local().ty;
184+
let old_ret_internal = rustc_internal::internal(tcx, old_ret_ty);
185+
let new_ret_internal = rustc_internal::internal(tcx, new_ret_ty);
186+
let new_ret_renamed = EarlyBinder::bind(new_ret_internal).instantiate(tcx, rename_args);
187+
129188
let mut diff = vec![];
130-
if old_ret_ty != new_ret_ty {
189+
// Error messages show the user's original types (before renaming) for clarity.
190+
if old_ret_internal != new_ret_renamed {
131191
diff.push(format!("Expected return type `{old_ret_ty}`, but found `{new_ret_ty}`"));
132192
}
133193
for (i, (old_arg, new_arg)) in
134194
old_body.arg_locals().iter().zip(new_body.arg_locals().iter()).enumerate()
135195
{
136-
if old_arg.ty != new_arg.ty {
196+
let old_ty_internal = rustc_internal::internal(tcx, old_arg.ty);
197+
let new_ty_internal = rustc_internal::internal(tcx, new_arg.ty);
198+
let new_renamed = EarlyBinder::bind(new_ty_internal).instantiate(tcx, rename_args);
199+
if old_ty_internal != new_renamed {
137200
diff.push(format!(
138201
"Expected type `{}` for parameter {}, but found `{}`",
139202
old_arg.ty,
@@ -150,6 +213,9 @@ pub fn check_compatibility(tcx: TyCtxt, old_def: FnDef, new_def: FnDef) -> Resul
150213
diff.iter().join("\n - ")
151214
))
152215
} else {
216+
// Note: Lifetime mismatches between the original and stub are not currently
217+
// detected (#2007). Differing lifetimes (e.g., `-> &'static T` vs `-> &'a T`)
218+
// can cause subtle verification failures such as "dereference failure: dead object".
153219
Ok(())
154220
}
155221
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Cannot stub `original` by `wrong_return_type`
2+
Expected return type `T`, but found `bool`
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test that a stub with a genuinely wrong return type is still rejected
7+
//! after the generic parameter renaming logic.
8+
9+
fn original<T: Default>(_x: T) -> T {
10+
T::default()
11+
}
12+
13+
fn wrong_return_type<S: Default>(_x: S) -> bool {
14+
true
15+
}
16+
17+
#[kani::proof]
18+
#[kani::stub(original, wrong_return_type)]
19+
fn check_wrong_return_type() {
20+
let _result: u32 = original(42u32);
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test that foreign functions declared in `extern "C"` blocks can be stubbed.
7+
//! Regression test for https://github.com/model-checking/kani/issues/2686
8+
9+
extern "C" {
10+
fn foreign_fn() -> u32;
11+
}
12+
13+
fn foreign_fn_stub() -> u32 {
14+
42
15+
}
16+
17+
#[kani::proof]
18+
#[kani::stub(foreign_fn, foreign_fn_stub)]
19+
fn check_foreign_fn_stub() {
20+
let result = unsafe { foreign_fn() };
21+
assert_eq!(result, 42);
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test that generic parameter renaming works with multiple type parameters.
7+
//! Regression test for https://github.com/model-checking/kani/issues/1953
8+
9+
fn original<A, B>(_x: A, _y: B) -> bool {
10+
false
11+
}
12+
13+
fn stub<X, Y>(_x: X, _y: Y) -> bool {
14+
true
15+
}
16+
17+
#[kani::proof]
18+
#[kani::stub(original, stub)]
19+
fn check_multi_generic_rename() {
20+
assert!(original(42u32, "hello"));
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
//
4+
// kani-flags: -Z stubbing
5+
//
6+
//! Test that generic parameter names can differ between original and stub.
7+
//! Regression test for https://github.com/model-checking/kani/issues/1953
8+
9+
fn original<T: Default>(_x: T) -> T {
10+
T::default()
11+
}
12+
13+
fn stub_with_different_name<S: Default>(_x: S) -> S {
14+
S::default()
15+
}
16+
17+
#[kani::proof]
18+
#[kani::stub(original, stub_with_different_name)]
19+
fn check_generic_param_rename() {
20+
let result: u32 = original(42u32);
21+
assert_eq!(result, 0); // stub returns Default::default() which is 0
22+
}

tests/kani/Stubbing/fixme_issue_1953.rs renamed to tests/kani/Stubbing/stub_generic_param_rename_simple.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
//
44
// kani-flags: -Z stubbing --harness main
55
//
6-
// We currently require a stub and the original/function method to have the same
7-
// names for generic parameters; instead, we should allow for renaming.
8-
// See <https://github.com/model-checking/kani/issues/1953> for more information.
6+
// Regression test: generic parameter names can differ between original and stub.
7+
// Previously this was a fixme test for https://github.com/model-checking/kani/issues/1953
98

109
fn foo<T>(_x: T) -> bool {
1110
false

0 commit comments

Comments
 (0)