id: union-02 original_id: P.UNS.UNI.02 level: P impact: CRITICAL
Do Not Use Union Variants Across Different Lifetimes
Summary
Do not write to one union field and read from another field that has a different lifetime or references data with a different lifetime.
Rationale
Union fields share the same memory. If one field stores a reference with lifetime 'a and you read it as a reference with lifetime 'b, you bypass lifetime checking and can create dangling references.
Bad Example
// DON'T: Extend lifetime through union
union LifetimeBypass<'a, 'b> {
short: &'a str,
long: &'b str,
}
fn bad_lifetime_extension<'a, 'b>(short: &'a str) -> &'b str {
let u = LifetimeBypass { short };
// BAD: Reading with different lifetime is UB
unsafe { u.long }
}
fn exploit() {
let long_ref: &'static str;
{
let temp = String::from("temporary");
// Extend local reference to 'static - dangling pointer!
long_ref = bad_lifetime_extension(&temp);
}
// temp is dropped, long_ref is dangling
println!("{}", long_ref); // UB: use after free
}
Good Example
// DO: Use same lifetime for all reference fields
union SafeUnion<'a> {
str_ref: &'a str,
bytes_ref: &'a [u8],
}
fn safe_conversion<'a>(s: &'a str) -> &'a [u8] {
let u = SafeUnion { str_ref: s };
// SAFETY: Both fields have same lifetime 'a
// AND str and [u8] have compatible representations
unsafe { u.bytes_ref }
}
// Better: Just use as_bytes()
fn better_conversion(s: &str) -> &[u8] {
s.as_bytes()
}
// DO: Use MaybeUninit for delayed initialization, not lifetime tricks
use std::mem::MaybeUninit;
fn delayed_init<T>(init: impl FnOnce() -> T) -> T {
let mut value: MaybeUninit<T> = MaybeUninit::uninit();
value.write(init());
unsafe { value.assume_init() }
}
Why This Is Dangerous
The Rust lifetime system prevents use-after-free by tracking how long references are valid. Unions can subvert this:
Memory: [pointer to "hello"]
Union as 'short: points to stack memory (valid during function)
Union as 'long: claims to point to valid memory forever
Reality: After function returns, pointer is dangling
Safe Union Patterns
// Pattern 1: All fields have same lifetime
union SameLifetime<'a, T, U> {
a: &'a T,
b: &'a U,
}
// Pattern 2: No references at all
#[repr(C)]
union NoRefs {
i: i32,
f: f32,
}
// Pattern 3: Use ManuallyDrop for owned values (careful with Drop!)
union OwnedUnion {
s: std::mem::ManuallyDrop<String>,
v: std::mem::ManuallyDrop<Vec<u8>>,
}
Checklist
- Do all reference fields have the same lifetime parameter?
- Am I trying to extend a lifetime through union? (If yes, stop!)
- For owned types, am I handling Drop correctly?
Related Rules
union-01: Avoid union except for C interopsafety-02: Verify safety invariants
你可能感兴趣的文章
Do Not Expose Raw Pointers in Public APIs
Unsafe Code Authors Must Verify Safety Invariants
Ensure Consistent Data Layout for Custom Types
Add Safety Section in Docs for Public Unsafe Functions
Use MaybeUninit
Ensure C-ABI Compatibility for Strings Between Rust and C
Do Not Dereference Pointers Cast to Misaligned Types
Use Dedicated Opaque Type Pointers Instead of c_void for C Opaque Types