From 9e8282d0e9e5155893877ad63e57533a2653d60b Mon Sep 17 00:00:00 2001 From: tanglearncode Date: Mon, 14 Sep 2026 00:28:48 +0800 Subject: [PATCH] Check borrowed inputs on the declared signature Since nightly-2026-08-22, mock! and replace! no longer compile for safe signatures with borrowed inputs, such as mock!(session, env::var_os::<&str>, fn(&str) -> Option). rustc reports "higher-ranked lifetime error" from the probe traits in macros/src/signature.rs. cargo-bisect-rustc points at rust-lang/rust#160619, which enables the next-generation trait solver by default on nightly; stable 1.98 and beta 1.99 still compile the code, and -Znext-solver=coherence restores it on nightly. The probes asked whether the source function accepts any lifetime for an input, and relied on that answer to pick a trait impl: if it did, the declared signature had to accept any lifetime too, and otherwise a fallback impl accepted a static-only source. The next solver ignores lifetimes when it picks an impl, so it chose the strict impl for every source. Proving that impl then fails for static-only sources, and for generic sources such as var_os::<&str>, whose input lifetime is an inference variable that cannot satisfy a higher-ranked bound. The probes now inspect the declared function pointer instead. The impl is chosen only by whether an input is a shared or mutable reference, and its method then requires that input to accept any lifetime. No lifetime decides which impl applies, so both solvers treat the code the same way, and the declared pointer has no inference variables to trip over. This is stricter than before: a safe signature can no longer declare a 'static borrowed input, even for a function that only accepts 'static borrows. Such functions are declared with an unsafe fn signature, which is checked only as a function pointer, as extern and unsafe signatures already are. The static_length test now uses that form, and the mock! and replace! docs say so. Declaring 'static for a source that accepts any lifetime stays rejected, including through a type alias and for mutable borrows. --- macros/src/signature.rs | 65 +++++++++++++++++++++++++---------------- src/lib.rs | 11 +++++-- tests/expectations.rs | 4 ++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/macros/src/signature.rs b/macros/src/signature.rs index d6e87ae..ffe531a 100644 --- a/macros/src/signature.rs +++ b/macros/src/signature.rs @@ -16,9 +16,18 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok .map(|i| format_ident!("__arg{i}", span = Span::mixed_site())) .collect(); let target_name = format_ident!("__target", span = Span::mixed_site()); + // A borrow of this local cannot last for 'static. `&()` would be promoted to a + // 'static borrow and accept every signature. + let local = format_ident!( + "borrowed_inputs_must_accept_any_lifetime", + span = Span::mixed_site() + ); let binder = &signature.lifetimes; let mut probes = TokenStream::new(); - // Probe borrowed inputs without rejecting static-only functions. + // Every borrowed input of the declared signature must accept a borrow of a local + // value, so a rule never treats a short borrow as a longer one. The probe impl is + // chosen by the input's type alone and the local borrow is checked afterwards, so + // no lifetime decides which impl applies and every trait solver agrees. for index in 0..args.len() { for mutable in [false, true] { let name = format_ident!("__Input{index}{}", usize::from(mutable)); @@ -30,36 +39,41 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok .filter_map(|(i, ty)| (i != index).then_some(ty)) .collect(); let mutability = mutable.then(|| quote!(mut)); - let probe_args: Vec<_> = types - .iter() - .enumerate() - .map(|(i, ty)| { - if i == index { - quote!(&'__probe #mutability __Pointee) - } else { - quote!(#ty) - } - }) - .collect(); + let borrowed = |lifetime: TokenStream| -> Vec { + types + .iter() + .enumerate() + .map(|(i, ty)| { + if i == index { + quote!(&#lifetime #mutability __Pointee) + } else { + quote!(#ty) + } + }) + .collect() + }; + let shape_args = borrowed(quote!('__shape)); + let local_args = borrowed(quote!('__local)); probes.extend(quote! { - trait #name<__Pointee: ?Sized, #(#other),*> { - fn #method<__Target>(&self, target: __Target) - where __Target: for<'__probe> __Signature<(#(#probe_args,)*)>; + trait #name<'__shape, __Pointee: ?Sized, #(#other),*> { + fn #method<'__local, __Target>(&self, target: __Target, local: &'__local ()) + where __Pointee: '__local, __Target: __Signature<(#(#local_args,)*)>; } - impl<__Source, __Pointee: ?Sized, #(#other),*> #name<__Pointee, #(#other),*> - for __SourceValue<__Source> - where __Source: for<'__probe> __Signature<(#(#probe_args,)*)> { - fn #method<__Target>(&self, _: __Target) - where __Target: for<'__probe> __Signature<(#(#probe_args,)*)> {} + impl<'__shape, __Shape, __Pointee: ?Sized + '__shape, #(#other),*> #name<'__shape, __Pointee, #(#other),*> + for __Declared<__Shape> + where __Shape: __Signature<(#(#shape_args,)*)> { + fn #method<'__local, __Target>(&self, _: __Target, _: &'__local ()) + where __Pointee: '__local, __Target: __Signature<(#(#local_args,)*)> {} } - trait #fallback { fn #method<__Target>(&self, target: __Target); } - impl<__Source> #fallback for &__SourceValue<__Source> { - fn #method<__Target>(&self, _: __Target) {} + trait #fallback { fn #method<__Target>(&self, target: __Target, local: &()); } + impl<__Shape> #fallback for &__Declared<__Shape> { + fn #method<__Target>(&self, _: __Target, _: &()) {} } - (&__SourceValue(#source)).#method(#target_name); + (&__Declared(#target_name)).#method(#target_name, &#local); }); } } + let local_value = (!args.is_empty()).then(|| quote!(let #local = ();)); quote! { { trait __Signature<__Args> { type Output; } @@ -67,7 +81,7 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok where __Function: ::std::ops::FnOnce(#(#types),*) -> __Output { type Output = __Output; } - struct __SourceValue<__Source>(__Source); + struct __Declared<__Shape>(__Shape); // Mutable references keep the input lifetimes distinct. fn __same_output<__Source, __Target, #(#types),*>(_: __Source, _: __Target, _: (#(&mut #types,)*)) where __Source: __Signature<(#(#types,)*)>, @@ -75,6 +89,7 @@ pub(crate) fn check(source: &Expr, signature: &TypeBareFn, target: &Expr) -> Tok #[allow(clippy::type_complexity)] let _: &mut dyn #binder ::std::ops::FnMut(#(#args),*) = &mut |#(mut #names),*| { let #target_name: #signature = #target; + #local_value #probes __same_output(#source, #target_name, (#(&mut #names,)*)); }; diff --git a/src/lib.rs b/src/lib.rs index 1299c02..6d75ed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -406,6 +406,10 @@ impl Drop for Session { /// be `Send + 'static`. Follow the same runtime safety rules as [`replace!`]. /// Installation panics if the function cannot be patched. /// +/// Borrowed inputs in a safe signature must accept any lifetime. For a function +/// that only accepts `'static` borrows, declare an `unsafe fn` signature; it is +/// checked only as a function pointer, so confirm the lifetimes yourself. +/// /// The source signature must match: /// ```compile_fail /// let mut session = shimforge::Session::new_global(); @@ -526,9 +530,10 @@ fn finish(result: Result<(), Error>, fatal: fn() -> !) { /// ``` /// /// No `unsafe` block is needed. Follow the crate's safety rules. Lifetime checks -/// are best effort; do not narrow lifetimes to force a type match. Closures -/// without captures are accepted. Installation panics if the function cannot be -/// patched. +/// are best effort; do not narrow lifetimes to force a type match. Borrowed inputs +/// in a safe signature must accept any lifetime; use an `unsafe fn` signature for a +/// function that only accepts `'static` borrows. Closures without captures are +/// accepted. Installation panics if the function cannot be patched. /// /// Incompatible signatures are rejected: /// ```compile_fail diff --git a/tests/expectations.rs b/tests/expectations.rs index cef7379..c9171a2 100644 --- a/tests/expectations.rs +++ b/tests/expectations.rs @@ -763,7 +763,9 @@ fn independent_input_lifetimes_keep_their_output_link() { fn genuine_static_inputs_and_results_stay_supported() { let _serial = serial_test(); let mut session = Session::new_global(); - let length = mock!(session, static_length, fn(&'static str) -> usize); + // Safe signatures require borrowed inputs that accept any lifetime, so a function + // that only takes 'static borrows is declared unsafe. + let length = mock!(session, static_length, unsafe fn(&'static str) -> usize); let seen = Arc::new(Mutex::new(None)); let captured = seen.clone(); length.expect().once().returning(move |value| {