diff --git a/Cargo.toml b/Cargo.toml index cec1ebc..f5c69bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ description = "Injectorpp is a powerful tool designed to facilitate the writing [dependencies] libc = "0.2" +injectorpp-macros = { path = "injectorpp-macros" } [target.'cfg(target_os = "macos")'.dependencies] mach2 = "0.5" diff --git a/injectorpp-macros/Cargo.toml b/injectorpp-macros/Cargo.toml new file mode 100644 index 0000000..0caaa33 --- /dev/null +++ b/injectorpp-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "injectorpp-macros" +version = "0.5.0" +authors = ["Jingyu Ma "] +license = "MIT" +repository = "https://github.com/microsoft/injectorppforrust" +edition = "2021" +description = "Proc macros for injectorpp - compile-time lifetime safety checks" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" diff --git a/injectorpp-macros/src/lib.rs b/injectorpp-macros/src/lib.rs new file mode 100644 index 0000000..89bc02c --- /dev/null +++ b/injectorpp-macros/src/lib.rs @@ -0,0 +1,177 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Token, Type, parenthesized, punctuated::Punctuated}; + +/// Parsed input for the simplified `fn` syntax of `func!`. +struct FuncInput { + func_expr: Expr, + arg_types: Vec, + return_type: Option, + is_unsafe: bool, + extern_abi: Option, +} + +impl Parse for FuncInput { + fn parse(input: ParseStream) -> syn::Result { + // Strip optional "func_info:" prefix + if input.peek(syn::Ident) { + let fork = input.fork(); + let ident: syn::Ident = fork.parse()?; + if ident == "func_info" && fork.peek(Token![:]) { + // Consume "func_info:" + let _: syn::Ident = input.parse()?; + let _: Token![:] = input.parse()?; + } + } + + // Parse optional "unsafe" + let is_unsafe = input.peek(Token![unsafe]); + if is_unsafe { + let _: Token![unsafe] = input.parse()?; + // Skip optional empty braces "{}" used in some macro arms + if input.peek(syn::token::Brace) { + let content; + syn::braced!(content in input); + let _ = content; + } + } + + // Parse optional extern "ABI" + let extern_abi = if input.peek(Token![extern]) { + let _: Token![extern] = input.parse()?; + let abi: syn::LitStr = input.parse()?; + Some(abi.value()) + } else { + None + }; + + // Parse "fn" + let _: Token![fn] = input.parse()?; + + // Parse "( func_expr )" + let func_content; + parenthesized!(func_content in input); + let func_expr: Expr = func_content.parse()?; + + // Parse "( arg_types )" + let types_content; + parenthesized!(types_content in input); + let arg_types: Punctuated = + Punctuated::parse_terminated(&types_content)?; + + // Parse optional "-> return_type" + let return_type = if input.peek(Token![->]) { + let _: Token![->] = input.parse()?; + Some(input.parse::()?) + } else { + None + }; + + Ok(FuncInput { + func_expr, + arg_types: arg_types.into_iter().collect(), + return_type, + is_unsafe, + extern_abi, + }) + } +} + +/// Check if a type is a bare reference (& without explicit lifetime). +fn is_bare_reference(ty: &Type) -> bool { + match ty { + Type::Reference(ref_type) => ref_type.lifetime.is_none(), + _ => false, + } +} + +/// Proc macro that implements the simplified `fn` syntax for `func!` with +/// automatic compile-time lifetime safety checks. +/// +/// When the return type is a bare reference (e.g., `&str`, `&[u8]`), this macro +/// generates an invariance-based check that detects lifetime mismatches at compile +/// time. This catches the issue #73 pattern where `fn(&str) -> &'static str` is +/// incorrectly specified as `fn(&str) -> &str`. +/// +/// For functions with genuinely linked lifetimes (e.g., `fn(&str) -> &str` where +/// the output lifetime matches the input), use an explicit lifetime annotation: +/// `func!(fn (f)(&str) -> &'_ str)`. +#[proc_macro] +pub fn func_checked(input: TokenStream) -> TokenStream { + let parsed = match syn::parse::(input) { + Ok(parsed) => parsed, + Err(err) => return err.to_compile_error().into(), + }; + + let func_expr = &parsed.func_expr; + let arg_types = &parsed.arg_types; + + // Build the function pointer type + let fn_type = match (&parsed.return_type, parsed.is_unsafe, &parsed.extern_abi) { + (Some(ret), false, None) => quote! { fn(#(#arg_types),*) -> #ret }, + (None, false, None) => quote! { fn(#(#arg_types),*) }, + (Some(ret), true, None) => quote! { unsafe fn(#(#arg_types),*) -> #ret }, + (None, true, None) => quote! { unsafe fn(#(#arg_types),*) -> () }, + (Some(ret), true, Some(abi)) => { + let abi_lit = syn::LitStr::new(abi, proc_macro2::Span::call_site()); + quote! { unsafe extern #abi_lit fn(#(#arg_types),*) -> #ret } + } + (None, true, Some(abi)) => { + let abi_lit = syn::LitStr::new(abi, proc_macro2::Span::call_site()); + quote! { unsafe extern #abi_lit fn(#(#arg_types),*) -> () } + } + (Some(ret), false, Some(abi)) => { + let abi_lit = syn::LitStr::new(abi, proc_macro2::Span::call_site()); + quote! { extern #abi_lit fn(#(#arg_types),*) -> #ret } + } + (None, false, Some(abi)) => { + let abi_lit = syn::LitStr::new(abi, proc_macro2::Span::call_site()); + quote! { extern #abi_lit fn(#(#arg_types),*) } + } + }; + + // Generate the lifetime invariance check for bare reference returns. + // This only applies to non-unsafe functions (unsafe/extern functions + // typically don't have Rust lifetime semantics). + let lifetime_check = if !parsed.is_unsafe { + if let Some(ref ret) = parsed.return_type { + if is_bare_reference(ret) { + quote! { + { + fn __injpp_check_ret<__R>( + _f: fn(#(#arg_types),*) -> __R, + ) -> fn(#(#arg_types),*) -> __R { + _f + } + fn __injpp_eq<__T>(_: &mut __T, _: &mut __T) {} + let mut __a = __injpp_check_ret(#func_expr); + let mut __b: fn(#(#arg_types),*) -> #ret = #func_expr; + __injpp_eq(&mut __a, &mut __b); + } + } + } else { + quote! {} + } + } else { + quote! {} + } + } else { + quote! {} + }; + + let output = quote! { + { + #lifetime_check + { + let fn_val: #fn_type = #func_expr; + let ptr = fn_val as *const (); + let sig = std::any::type_name_of_val(&fn_val); + let type_id = std::any::TypeId::of::<#fn_type>(); + unsafe { FuncPtr::new_with_type_id(ptr, sig, type_id) } + } + } + }; + + output.into() +} diff --git a/src/interface/func_ptr.rs b/src/interface/func_ptr.rs index 9ff9df9..d30e0e7 100644 --- a/src/interface/func_ptr.rs +++ b/src/interface/func_ptr.rs @@ -1,4 +1,5 @@ use crate::injector_core::common::FuncPtrInternal; +use std::any::TypeId; use std::ptr::NonNull; /// A safe wrapper around a raw function pointer. @@ -17,6 +18,7 @@ pub struct FuncPtr { /// This is a wrapper around a non-null pointer to ensure safety. pub(super) func_ptr_internal: FuncPtrInternal, pub(super) signature: &'static str, + pub(super) type_id: Option, } impl FuncPtr { @@ -35,6 +37,27 @@ impl FuncPtr { Self { func_ptr_internal: FuncPtrInternal::new(nn), signature, + type_id: None, + } + } + + /// Creates a new `FuncPtr` from a raw pointer with type identity information. + /// + /// # Safety + /// + /// The caller must ensure that the pointer is valid and points to a function. + pub unsafe fn new_with_type_id( + ptr: *const (), + signature: &'static str, + type_id: TypeId, + ) -> Self { + let p = ptr as *mut (); + let nn = NonNull::new(p).expect("Pointer must not be null"); + + Self { + func_ptr_internal: FuncPtrInternal::new(nn), + signature, + type_id: Some(type_id), } } } diff --git a/src/interface/injector.rs b/src/interface/injector.rs index afaef76..a1b656d 100644 --- a/src/interface/injector.rs +++ b/src/interface/injector.rs @@ -3,6 +3,7 @@ use crate::injector_core::common::*; use crate::injector_core::internal::*; pub use crate::interface::func_ptr::FuncPtr; pub use crate::interface::macros::__assert_future_output; +pub use crate::interface::macros::__type_id_of_val; pub use crate::interface::verifier::CallCountVerifier; use std::future::Future; @@ -166,6 +167,7 @@ impl InjectorPP { lib: self, when, expected_signature: func.signature, + expected_type_id: func.type_id, } } @@ -211,6 +213,7 @@ impl InjectorPP { lib: self, when, expected_signature: "", + expected_type_id: None, } } @@ -254,15 +257,16 @@ impl InjectorPP { F: Future, { let poll_fn: fn(Pin<&mut F>, &mut Context<'_>) -> Poll = ::poll; - let when = WhenCalled::new( - crate::func!(poll_fn, fn(Pin<&mut F>, &mut Context<'_>) -> Poll).func_ptr_internal, - ); + let when = WhenCalled::new(unsafe { + FuncPtr::new(poll_fn as *const (), std::any::type_name_of_val(&poll_fn)) + }.func_ptr_internal); let signature = fake_pair.1; WhenCalledBuilderAsync { lib: self, when, expected_signature: signature, + expected_type_id: None, } } @@ -313,14 +317,15 @@ impl InjectorPP { F: Future, { let poll_fn: fn(Pin<&mut F>, &mut Context<'_>) -> Poll = ::poll; - let when = WhenCalled::new( - crate::func!(poll_fn, fn(Pin<&mut F>, &mut Context<'_>) -> Poll).func_ptr_internal, - ); + let when = WhenCalled::new(unsafe { + FuncPtr::new(poll_fn as *const (), std::any::type_name_of_val(&poll_fn)) + }.func_ptr_internal); WhenCalledBuilderAsync { lib: self, when, expected_signature: "", + expected_type_id: None, } } } @@ -356,6 +361,7 @@ pub struct WhenCalledBuilder<'a> { lib: &'a mut InjectorPP, when: WhenCalled, expected_signature: &'static str, + expected_type_id: Option, } impl WhenCalledBuilder<'_> { @@ -403,11 +409,24 @@ impl WhenCalledBuilder<'_> { /// assert!(Path::new("/nonexistent").exists()); /// ``` pub fn will_execute_raw(self, target: FuncPtr) { - if normalize_signature(target.signature) != normalize_signature(self.expected_signature) { - panic!( - "Signature mismatch: expected {:?} but got {:?}", - self.expected_signature, target.signature - ); + match (self.expected_type_id, target.type_id) { + (Some(expected), Some(actual)) if expected != actual => { + panic!( + "Signature mismatch: expected {:?} but got {:?}", + self.expected_signature, target.signature + ); + } + (None, _) | (_, None) => { + if normalize_signature(target.signature) + != normalize_signature(self.expected_signature) + { + panic!( + "Signature mismatch: expected {:?} but got {:?}", + self.expected_signature, target.signature + ); + } + } + _ => {} } #[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "arm"))] @@ -577,6 +596,7 @@ pub struct WhenCalledBuilderAsync<'a> { lib: &'a mut InjectorPP, when: WhenCalled, expected_signature: &'static str, + expected_type_id: Option, } impl WhenCalledBuilderAsync<'_> { @@ -605,11 +625,24 @@ impl WhenCalledBuilderAsync<'_> { /// } /// ``` pub fn will_return_async(self, target: FuncPtr) { - if normalize_signature(target.signature) != normalize_signature(self.expected_signature) { - panic!( - "Signature mismatch: expected {:?} but got {:?}", - self.expected_signature, target.signature - ); + match (self.expected_type_id, target.type_id) { + (Some(expected), Some(actual)) if expected != actual => { + panic!( + "Signature mismatch: expected {:?} but got {:?}", + self.expected_signature, target.signature + ); + } + (None, _) | (_, None) => { + if normalize_signature(target.signature) + != normalize_signature(self.expected_signature) + { + panic!( + "Signature mismatch: expected {:?} but got {:?}", + self.expected_signature, target.signature + ); + } + } + _ => {} } #[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "arm"))] diff --git a/src/interface/macros.rs b/src/interface/macros.rs index 53b4337..b4ba1e5 100644 --- a/src/interface/macros.rs +++ b/src/interface/macros.rs @@ -3,6 +3,18 @@ /// This macro handles both generic and non-generic functions: /// - For generic functions, provide the function name and type parameters separately: `func!(function_name, fn(Type1, Type2))` /// - For non-generic functions, simply provide the function: `func!(function_name, fn())` +/// +/// # Lifetime Safety +/// +/// When using the simplified `fn` syntax with a bare reference return type (e.g., `-> &str`), +/// `func!` automatically applies a compile-time check that catches lifetime mismatches. +/// This prevents undefined behavior from coercing `fn(&str) -> &'static str` into +/// `fn(&str) -> &str` (see GitHub issue #73). +/// +/// If your function genuinely returns a reference with a lifetime linked to its input +/// (e.g., `fn(&str) -> &str`), add an explicit lifetime annotation to bypass the check: +/// - `func!(fn (my_fn)(&str) -> &'_ str)` — explicitly linked to input lifetime +/// - `func!(fn (my_fn)(&str) -> &'static str)` — explicitly static #[macro_export] macro_rules! func { // Case 1: Generic function — provide function name and types separately @@ -10,97 +22,34 @@ macro_rules! func { let fn_val:$fn_type = $f::<$($gen),*>; let ptr = fn_val as *const (); let sig = std::any::type_name_of_val(&fn_val); + let type_id = std::any::TypeId::of::<$fn_type>(); - unsafe { FuncPtr::new(ptr, sig) } + unsafe { FuncPtr::new_with_type_id(ptr, sig, type_id) } }}; - // Case 2: Non-generic function + // Case 2: Non-generic function with explicit type ($f:expr, $fn_type:ty) => {{ let fn_val:$fn_type = $f; let ptr = fn_val as *const (); let sig = std::any::type_name_of_val(&fn_val); + let type_id = std::any::TypeId::of::<$fn_type>(); - unsafe { FuncPtr::new(ptr, sig) } - }}; - - // Simplified fn with return - (func_info: fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, fn($($arg_ty),*) -> $ret) - }}; - - (fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, fn($($arg_ty),*) -> $ret) - }}; - - // Simplified fn with unit return - (func_info: fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, fn($($arg_ty),*)) - }}; - - (fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, fn($($arg_ty),*)) - }}; - - // Simplified unsafe fn with return - (func_info: unsafe fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, unsafe fn($($arg_ty),*) -> $ret) - }}; - - (unsafe{} fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, unsafe fn($($arg_ty),*) -> $ret) - }}; - - // Simplified unsafe fn with unit return - (func_info: unsafe fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, unsafe fn($($arg_ty),*) -> ()) - }}; - - (unsafe{} fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, unsafe fn($($arg_ty),*) -> ()) - }}; - - // Simplified unsafe extern "C" fn with return - (func_info: unsafe extern "C" fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, unsafe extern "C" fn($($arg_ty),*) -> $ret) - }}; - - (unsafe{} extern "C" fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, unsafe extern "C" fn($($arg_ty),*) -> $ret) - }}; - - // Simplified unsafe extern "C" fn with unit return - (func_info: unsafe extern "C" fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, unsafe extern "C" fn($($arg_ty),*) -> ()) - }}; - - (unsafe{} extern "C" fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, unsafe extern "C" fn($($arg_ty),*) -> ()) + unsafe { FuncPtr::new_with_type_id(ptr, sig, type_id) } }}; - // Simplified unsafe extern "system" fn with return - (func_info: unsafe extern "system" fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, unsafe extern "system" fn($($arg_ty),*) -> $ret) - }}; - - (unsafe{} extern "system" fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ - $crate::func!($f, unsafe extern "system" fn($($arg_ty),*) -> $ret) - }}; - - // Simplified unsafe extern "system" fn with unit return - (func_info: unsafe extern "system" fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, unsafe extern "system" fn($($arg_ty),*) -> ()) - }}; - - (unsafe{} extern "system" fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ - $crate::func!($f, unsafe extern "system" fn($($arg_ty),*) -> ()) + // All simplified fn syntax patterns: delegate to proc macro for + // automatic lifetime safety checking. + ($($tt:tt)*) => {{ + $crate::__func_checked!($($tt)*) }}; } /// Converts a function to a `FuncPtr`. /// /// This macro handles both generic and non-generic functions: -/// - For generic functions, provide the function name and type parameters separately: `func!(function_name::)` -/// - For non-generic functions, simply provide the function: `func!(function_name)` +/// - Generic function: `func_unchecked!(function_name::)` +/// - Non-generic function: `func_unchecked!(function_name)` +/// - Simplified fn syntax: `func_unchecked!(fn (function_name)(ArgType) -> RetType)` /// /// # Safety /// @@ -126,6 +75,16 @@ macro_rules! func_unchecked { FuncPtr::new(ptr, "") }}; + + // Case 3: Simplified fn syntax with return — skips lifetime check + (fn ( $f:expr ) ( $($arg_ty:ty),* ) -> $ret:ty) => {{ + $crate::func!($f, fn($($arg_ty),*) -> $ret) + }}; + + // Case 4: Simplified fn syntax without return — skips lifetime check + (fn ( $f:expr ) ( $($arg_ty:ty),* )) => {{ + $crate::func!($f, fn($($arg_ty),*)) + }}; } /// Converts a closure to a `FuncPtr`. @@ -142,8 +101,9 @@ macro_rules! closure { ($closure:expr, $fn_type:ty) => {{ let fn_val: $fn_type = $closure; let sig = std::any::type_name_of_val(&fn_val); + let type_id = std::any::TypeId::of::<$fn_type>(); - unsafe { FuncPtr::new(fn_val as *const (), sig) } + unsafe { FuncPtr::new_with_type_id(fn_val as *const (), sig, type_id) } }}; } @@ -179,6 +139,12 @@ where { } +/// Helper to extract TypeId from a value's type. Used internally by macros. +#[doc(hidden)] +pub fn __type_id_of_val(_: &T) -> std::any::TypeId { + std::any::TypeId::of::() +} + /// Ensure the async function can be correctly used in injectorpp. #[macro_export] macro_rules! async_func { @@ -1365,3 +1331,57 @@ macro_rules! fake { (unsafe { FuncPtr::new(raw_ptr, std::any::type_name_of_val(&f)) }, verifier) }}; } + +/// Compile-time check that the specified function signature matches the function's actual type. +/// +/// This macro catches lifetime mismatches that `func!` cannot detect on its own due to Rust's +/// implicit function pointer subtyping. It uses type invariance (`&mut T`) to prevent coercion +/// and will produce a compile error if the lifetimes don't match. +/// +/// # When to use +/// +/// Use this macro when faking functions that return references, especially `&'static` references. +/// It prevents the common mistake of eliding `'static` (e.g., writing `&str` instead of +/// `&'static str`), which can cause undefined behavior (see issue #73). +/// +/// # Limitations +/// +/// This macro only works for functions whose return type is **independent** of input lifetimes. +/// It cannot be used with functions like `fn(&str) -> &str` where the output borrows from +/// the input — use `func!` directly for those. +/// +/// # Example +/// +/// ```rust,compile_fail +/// fn foo(_s: &str) -> &'static str { "abc" } +/// +/// // This correctly fails to compile — the return type should be &'static str, not &str +/// injectorpp::verify_func!(fn (foo)(&str) -> &str); +/// ``` +/// +/// ```rust +/// fn foo(_s: &str) -> &'static str { "abc" } +/// +/// // This compiles — the return type correctly matches +/// injectorpp::verify_func!(fn (foo)(&str) -> &'static str); +/// ``` +#[macro_export] +macro_rules! verify_func { + (fn ($func:expr)($($param:ty),*) -> $ret:ty) => {{ + #[allow(non_snake_case)] + fn __injectorpp_infer_ret<__InjectorppRet>( + _f: fn($($param),*) -> __InjectorppRet, + ) -> fn($($param),*) -> __InjectorppRet { + _f + } + #[allow(non_snake_case)] + fn __injectorpp_must_match<__InjectorppT>( + _a: &mut __InjectorppT, + _b: &mut __InjectorppT, + ) { + } + let mut __injectorpp_natural = __injectorpp_infer_ret($func); + let mut __injectorpp_user: fn($($param),*) -> $ret = $func; + __injectorpp_must_match(&mut __injectorpp_natural, &mut __injectorpp_user); + }}; +} diff --git a/src/lib.rs b/src/lib.rs index 50e2d38..bb6e13f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -555,3 +555,6 @@ //! ``` mod injector_core; pub mod interface; + +#[doc(hidden)] +pub use injectorpp_macros::func_checked as __func_checked; diff --git a/tests/compile_fail/fake_returns_dangling_reference.rs b/tests/compile_fail/fake_returns_dangling_reference.rs new file mode 100644 index 0000000..3fadc7c --- /dev/null +++ b/tests/compile_fail/fake_returns_dangling_reference.rs @@ -0,0 +1,27 @@ +/// Exact reproduction from issue #73. +/// The user creates a fake that returns a borrowed `s` as if it were &'static str, +/// causing use-after-free. +use injectorpp::interface::injector::*; + +#[inline(never)] +fn foo(_s: &str) -> &'static str { + "abc" +} + +fn main() { + let mut injector = InjectorPP::new(); + + injector + .when_called(injectorpp::func!(fn (foo)(&str) -> &str)) + .will_execute(injectorpp::fake!( + func_type: fn(s: &str) -> &str, + returns: s + )); + + let s = { + let s = String::from("foo"); + foo(&s) + }; + + println!("{s}"); +} diff --git a/tests/compile_fail/func_info_prefix_lifetime_mismatch.rs b/tests/compile_fail/func_info_prefix_lifetime_mismatch.rs new file mode 100644 index 0000000..769dc05 --- /dev/null +++ b/tests/compile_fail/func_info_prefix_lifetime_mismatch.rs @@ -0,0 +1,10 @@ +/// Lifetime check also applies when using the func_info: prefix syntax. +use injectorpp::interface::injector::*; + +fn foo(_s: &str) -> &'static str { + "hello" +} + +fn main() { + let _f = injectorpp::func!(func_info: fn (foo)(&str) -> &str); +} diff --git a/tests/compile_fail/static_slice_coerced_to_bare_ref.rs b/tests/compile_fail/static_slice_coerced_to_bare_ref.rs new file mode 100644 index 0000000..91ae42c --- /dev/null +++ b/tests/compile_fail/static_slice_coerced_to_bare_ref.rs @@ -0,0 +1,11 @@ +/// Bare &[u8] return when function actually returns &'static [u8]. +/// Same class of bug as &str but with a slice type. +use injectorpp::interface::injector::*; + +fn get_bytes(_x: i32) -> &'static [u8] { + b"hello" +} + +fn main() { + let _f = injectorpp::func!(fn (get_bytes)(i32) -> &[u8]); +} diff --git a/tests/compile_fail/static_str_coerced_to_bare_ref.rs b/tests/compile_fail/static_str_coerced_to_bare_ref.rs new file mode 100644 index 0000000..4750f64 --- /dev/null +++ b/tests/compile_fail/static_str_coerced_to_bare_ref.rs @@ -0,0 +1,11 @@ +/// Bare &str return when function actually returns &'static str. +/// This is the exact pattern from issue #73 that causes use-after-free. +use injectorpp::interface::injector::*; + +fn foo(_s: &str) -> &'static str { + "hello" +} + +fn main() { + let _f = injectorpp::func!(fn (foo)(&str) -> &str); +} diff --git a/tests/lifetime_safety.rs b/tests/lifetime_safety.rs new file mode 100644 index 0000000..0d64a43 --- /dev/null +++ b/tests/lifetime_safety.rs @@ -0,0 +1,138 @@ +// Compile-time lifetime safety tests for the func! macro. +// +// These tests verify that: +// - Mismatched lifetimes in bare reference returns are rejected at compile time +// - Correct usage patterns (explicit lifetimes, non-reference returns) continue to work + +use injectorpp::interface::injector::*; + +// ====================================================================== +// Compile-fail tests: these must NOT compile +// Uses rustc directly to avoid fragile stderr snapshot matching. +// ====================================================================== + +/// Helper: tries to compile a source file and returns whether it succeeded. +/// Returns None if build artifacts can't be found (e.g. cross-compilation). +fn try_compile(source_path: &str) -> Option { + let rlib = find_file(&["target/debug/deps", "target/debug"], "libinjectorpp", ".rlib")?; + let ext = if cfg!(windows) { ".dll" } else if cfg!(target_os = "macos") { ".dylib" } else { ".so" }; + let proc_dylib = find_file(&["target/debug/deps", "target/debug"], "injectorpp_macros", ext)?; + + let output = std::process::Command::new("rustc") + .args([ + "--edition", "2021", + "--crate-type", "bin", + "-L", "target/debug/deps", + "--extern", &format!("injectorpp={}", rlib), + "--extern", &format!("injectorpp_macros={}", proc_dylib), + "-o", if cfg!(windows) { "NUL" } else { "/dev/null" }, + source_path, + ]) + .output() + .expect("failed to invoke rustc"); + Some(output.status.success()) +} + +fn find_file(dirs: &[&str], prefix: &str, suffix: &str) -> Option { + for dir in dirs { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.filter_map(|e| e.ok()) { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with(prefix) && name.ends_with(suffix) { + return Some(entry.path().to_string_lossy().to_string()); + } + } + } + } + None +} + +#[test] +fn static_str_coerced_to_bare_ref_must_not_compile() { + match try_compile("tests/compile_fail/static_str_coerced_to_bare_ref.rs") { + Some(compiled) => assert!(!compiled, "expected compile error: &'static str coerced to bare &str should be rejected"), + None => eprintln!("skipped: build artifacts not found"), + } +} + +#[test] +fn static_slice_coerced_to_bare_ref_must_not_compile() { + match try_compile("tests/compile_fail/static_slice_coerced_to_bare_ref.rs") { + Some(compiled) => assert!(!compiled, "expected compile error: &'static [u8] coerced to bare &[u8] should be rejected"), + None => eprintln!("skipped: build artifacts not found"), + } +} + +#[test] +fn func_info_prefix_lifetime_mismatch_must_not_compile() { + match try_compile("tests/compile_fail/func_info_prefix_lifetime_mismatch.rs") { + Some(compiled) => assert!(!compiled, "expected compile error: lifetime mismatch with func_info: prefix should be rejected"), + None => eprintln!("skipped: build artifacts not found"), + } +} + +#[test] +fn fake_returns_dangling_reference_must_not_compile() { + match try_compile("tests/compile_fail/fake_returns_dangling_reference.rs") { + Some(compiled) => assert!(!compiled, "expected compile error: fake returning dangling reference via lifetime coercion should be rejected"), + None => eprintln!("skipped: build artifacts not found"), + } +} + +// ====================================================================== +// Pass tests: correct usage patterns that must continue to work +// ====================================================================== + +fn returns_static_str(_s: &str) -> &'static str { + "hello" +} + +fn linked_lifetime_str(s: &str) -> &str { + s +} + +fn returns_option_ref(s: &str) -> Option<&str> { + Some(s) +} + +fn returns_i32(x: i32) -> i32 { + x + 1 +} + +fn no_return() {} + +/// Explicit &'static str — correct, no check needed. +#[test] +fn pass_explicit_static_lifetime() { + let _f = injectorpp::func!(fn (returns_static_str)(&str) -> &'static str); +} + +/// Explicit &'_ str — user acknowledges linked lifetime, no check. +#[test] +fn pass_explicit_elided_lifetime() { + let _f = injectorpp::func!(fn (linked_lifetime_str)(&str) -> &'_ str); +} + +/// Option<&str> is not a bare reference — no check applied. +#[test] +fn pass_wrapped_reference_return() { + let _f = injectorpp::func!(fn (returns_option_ref)(&str) -> Option<&str>); +} + +/// Non-reference return — no check applied. +#[test] +fn pass_non_reference_return() { + let _f = injectorpp::func!(fn (returns_i32)(i32) -> i32); +} + +/// Unit return — no check applied. +#[test] +fn pass_unit_return() { + let _f = injectorpp::func!(fn (no_return)()); +} + +/// Case 2 (explicit type) — not subject to the proc macro check. +#[test] +fn pass_case2_explicit_type() { + let _f = injectorpp::func!(returns_i32, fn(i32) -> i32); +}