The concrete question would be, if you have a variable on the stack of a function that never returns, is it safe to extend its lifetime to 'static using transmute?
fn main() -> ! {
let x = 42;
// Safety: This function never returns, therefore the reference is valid
// until the end of the program
let x = unsafe { make_static(&x) };
loop {
take_static_ref(x);
}
}
fn take_static_ref(x: &'static i32) {
println!("{x}");
}
unsafe fn make_static<T>(t: &T) -> &'static T {
core::mem::transmute(t)
}
It's not enough for the function to never return, it also need to never unwind (i.e. never panic), otherwise stack variables will be dropped during the unwind and the static reference will become invalid.
In my opinion your example is much better solved by leaking a Box.
Then I guess you won't call this function more than once. In that case have you considered using a static, potentially in combination with an OnceCell for initialization?
Why does interior mutability affect the soundness of the code? If there was unsoundness in the code with interior mutability, the code would be unsound with or without the transmute, wouldn't it? The transmute is just extending the lifetime of the variable, nothing else.
This is probably sound; I wonder if a crate for this exists:
fn with_static<T: 'static>(
mut x: T,
f: impl FnOnce(&'static mut T) -> std::convert::Infallible, // <- type `!` not allowed here on stable Rust
) -> ! {
let _guard = Guard;
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
// must not make it beyond this point!
std::process::abort();
// might need a no_std alternative if you don't have std
// e.g. double-panic should abort reliably, too
}
}
// sound, because `_guard` ensures this call can never be left, even with unwinding
let static_ref: &'static mut T = unsafe { &mut *(&mut x as *mut T) };
#[allow(unreachable_code)]
match f(static_ref) {}
}
Edit: Glancing at the mk_static API discussed in the article linked above, this seems different and avoids the soundness conflicts AFAICT, because we’re taking the value that’s supposed to be made to “live forever” by-value, not by-reference.
I think it might be possible to wrap everything in a struct and make it all static. I'll see if it is possible and I can get rid of those unsafe calls.
As he's doing embedded (no-std and even no alloc) work, there's a good chance he's also doing panic = 'abort' in his Cargo.toml and can therefore ignore unwinding concerns.