Skip to content

Fix #73: automatic compile-time lifetime safety via proc macro#119

Merged
mazong1123 merged 11 commits intomainfrom
fix/issue-73-lifetime-soundness
Mar 16, 2026
Merged

Fix #73: automatic compile-time lifetime safety via proc macro#119
mazong1123 merged 11 commits intomainfrom
fix/issue-73-lifetime-soundness

Conversation

@mazong1123
Copy link
Collaborator

@mazong1123 mazong1123 commented Mar 15, 2026

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 str to be silently coerced to fn(&str) -> &str. When func! and fake! use the wrong type, the fake can return a dangling reference — undefined behavior with no compiler warning.

How it works

The func! macro's simplified fn syntax now delegates to a proc macro (injectorpp-macros) that inspects the return type:

  • Bare reference returns (e.g., -> &str, -> &[u8]): an invariance-based check is generated that detects if the function's actual return lifetime differs from the specified type
  • Explicit lifetime returns (e.g., -> &'static str, -> &'_ str): no check needed — the user has stated their intent
  • Non-reference returns (e.g., -> i32, -> Option<&str>): no check needed

Example: issue #73 is now caught at compile time

fn foo(_s: &str) -> &'static str { "hello" }

// This now produces a compile error instead of silent UB:
let f = func!(fn (foo)(&str) -> &str);  // ERROR: mismatched types
// Fix: specify the correct lifetime
let f = func!(fn (foo)(&str) -> &'static str);  // OK

For linked-lifetime functions

Functions that genuinely return references linked to input lifetimes can add an explicit annotation:

fn baz(s: &str) -> &str { s }
let f = func!(fn (baz)(&str) -> &'_ str);  // OK

Architecture

  • injectorpp-macros/: new proc macro crate with func_checked
  • func!: Case 1 (generic) and Case 2 (explicit type) remain as declarative macros; simplified fn syntax delegates to the proc macro via catch-all arm
  • Same user interface: func! syntax is completely unchanged — existing code compiles without modification

Testing

131 tests pass (100 unit/integration + 31 doctests). Clippy clean.

10 new lifetime safety tests (tests/lifetime_safety.rs):

Test Type Scenario
static_str_coerced_to_bare_ref compile-fail &'static str coerced to &str return
static_slice_coerced_to_bare_ref compile-fail &'static [u8] coerced to &[u8] return
func_info_prefix_lifetime_mismatch compile-fail Same issue with func_info: prefix syntax
fake_returns_dangling_reference compile-fail Full func! + fake! + will_execute pattern from issue #73
pass_explicit_static_lifetime pass -> &'static str is accepted
pass_explicit_elided_lifetime pass -> &'_ str is accepted
pass_non_reference_return pass -> i32 is accepted
pass_wrapped_reference_return pass -> Option<&str> is accepted
pass_unit_return pass No return type is accepted
pass_case2_explicit_type pass Case 2 explicit type syntax is accepted

Compile-fail tests use direct rustc invocation (exit code only, no stderr matching) to avoid fragility across Rust versions.

Closes #73

…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
@mazong1123 mazong1123 changed the title Add TypeId-based type checking and verify_func! macro for lifetime safety Fix #73: automatic compile-time lifetime safety via proc macro Mar 15, 2026
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
@mazong1123 mazong1123 merged commit 6f4b5aa into main Mar 16, 2026
17 checks passed
@mazong1123 mazong1123 deleted the fix/issue-73-lifetime-soundness branch March 16, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undefined behavior due to differing lifetimes

1 participant