Fix #73: automatic compile-time lifetime safety via proc macro#119
Merged
mazong1123 merged 11 commits intomainfrom Mar 16, 2026
Merged
Fix #73: automatic compile-time lifetime safety via proc macro#119mazong1123 merged 11 commits intomainfrom
mazong1123 merged 11 commits intomainfrom
Conversation
…fety Address issue #73: undefined behavior from differing lifetimes in func!/fake! macros due to implicit function pointer subtyping in Rust. Changes: - Add TypeId field to FuncPtr for precise type identity checking - Update func!, fake!, and closure! macros to capture TypeId - Replace string-based comparison with TypeId comparison in will_execute_raw and will_return_async (falls back to string comparison for unchecked paths) - Add verify_func! macro that catches lifetime mismatches at compile time using type invariance (works for functions with return types independent of input lifetimes) - Add documentation warning about lifetime safety requirements Relates to #73
When func! is used with the simplified fn syntax and a bare reference return type (e.g., -> &str), an invariance check now detects lifetime mismatches at compile time. This catches the exact issue #73 pattern where fn(&str) -> &'static str is incorrectly coerced to fn(&str) -> &str. Escape hatches for functions with genuinely linked lifetimes: - Use explicit lifetime: func!(fn (f)(&str) -> &'_ str) - Use func_unchecked!(fn (f)(&str) -> &str) Also removes TypeId capture from fake! macro since the generated function's TypeId inherently differs from the original (different lifetime annotations). Signature-based comparison is sufficient for fake! validation.
Remove the invariance-based compile-time check from func! macro arms. The check was a breaking change for functions with linked-lifetime bare reference returns (e.g., fn(&str) -> &str). Keep the non-breaking improvements: - TypeId-based type checking in func! (for will_execute_raw matching) - No TypeId in fake! (avoids false mismatches with different lifetimes) - verify_func! macro for opt-in compile-time lifetime validation - func_unchecked! simplified fn syntax support - Documentation warnings about lifetime safety
Introduce injectorpp-macros proc macro crate that powers the func! macro's simplified fn syntax. The proc macro automatically detects bare reference return types (e.g., -> &str) and generates an invariance-based check that catches lifetime mismatches at compile time. This automatically prevents the issue #73 UB pattern where fn(&str) -> &'static str is incorrectly specified as fn(&str) -> &str. The func! macro syntax is completely unchanged. For functions with genuinely linked lifetimes, users can add an explicit annotation: func!(fn (f)(&str) -> &'_ str) Architecture: - injectorpp-macros: proc macro crate with func_checked - func! declarative macro delegates simplified fn patterns to proc macro - Case 1 (generic) and Case 2 (explicit type) remain as declarative
Add trybuild-based tests that verify the proc macro correctly rejects lifetime-mismatched func! calls at compile time: - fn(&str) -> &str when function returns &'static str - fn(i32) -> &[u8] when function returns &'static [u8] - func_info: prefix variant with same mismatch All three cases produce compile errors instead of silent UB.
Replace issue73-prefixed tests with descriptive scenario names: - static_str_coerced_to_bare_ref: the exact issue #73 UB pattern - static_slice_coerced_to_bare_ref: same bug with &[u8] - func_info_prefix_lifetime_mismatch: check applies with prefix Add pass tests verifying correct patterns still compile: - explicit &'static str, explicit &'_ str, Option<&str>, non-reference return, unit return, Case 2 explicit type
Remove trybuild dependency and fragile .stderr snapshot files. Compile-fail tests now invoke rustc directly and assert non-zero exit code, making them resilient to compiler error message changes across Rust versions.
Adds a test that reproduces the complete code from issue #73: func! + fake! + use-after-free pattern. Verifies the proc macro rejects it at compile time.
- find_file() now searches both target/debug/deps and target/debug - try_compile() returns Option to gracefully skip on cross-compilation - Changed doc comments to regular comments to fix empty_line_after_doc_comments
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.
Summary
Fixes #73 by automatically detecting lifetime mismatches in
func!at compile time using a proc macro — no user code changes required.Problem
Rust's function pointer subtyping allows
fn(&str) -> &'static strto be silently coerced tofn(&str) -> &str. Whenfunc!andfake!use the wrong type, the fake can return a dangling reference — undefined behavior with no compiler warning.How it works
The
func!macro's simplifiedfnsyntax now delegates to a proc macro (injectorpp-macros) that inspects the return type:-> &str,-> &[u8]): an invariance-based check is generated that detects if the function's actual return lifetime differs from the specified type-> &'static str,-> &'_ str): no check needed — the user has stated their intent-> i32,-> Option<&str>): no check neededExample: issue #73 is now caught at compile time
For linked-lifetime functions
Functions that genuinely return references linked to input lifetimes can add an explicit annotation:
Architecture
func_checkedfunc!syntax is completely unchanged — existing code compiles without modificationTesting
131 tests pass (100 unit/integration + 31 doctests). Clippy clean.
10 new lifetime safety tests (
tests/lifetime_safety.rs):static_str_coerced_to_bare_ref&'static strcoerced to&strreturnstatic_slice_coerced_to_bare_ref&'static [u8]coerced to&[u8]returnfunc_info_prefix_lifetime_mismatchfunc_info:prefix syntaxfake_returns_dangling_referencefunc!+fake!+will_executepattern from issue #73pass_explicit_static_lifetime-> &'static stris acceptedpass_explicit_elided_lifetime-> &'_ stris acceptedpass_non_reference_return-> i32is acceptedpass_wrapped_reference_return-> Option<&str>is acceptedpass_unit_returnpass_case2_explicit_typeCompile-fail tests use direct
rustcinvocation (exit code only, no stderr matching) to avoid fragility across Rust versions.Closes #73