Diverging functions never return, so their local variables are never dropped. Would it be possible to set their lifetime to 'static in the compiler?
Currently the following error occurs:
fn foo() -> ! {
let x = 42;
bar(&x);
loop {}
}
fn bar(_: &'static u32) {}
error[E0597]: `x` does not live long enough
--> src/main.rs:7:9
|
7 | bar(&x);
| ----^^-
| | |
| | borrowed value does not live long enough
| argument requires that `x` is borrowed for `'static`
8 | loop {}
9 | }
| - `x` dropped here while still borrowed
The compiler checks that the function is really diverging, so it knows that line 9 is never reached and that x is never dropped. So why not treat x as 'static?
This feature would be useful for embedded/OSdev where some static structures are needed for hardware initialization. For example, on x86_64 an interrupt descriptor table with 'static lifetime needs to be created and loaded in the CPU before enabling interrupts. Currently, we use lazy_static to construct this table with a 'static lifetime, but it would be easier if we could just use a local variable of our diverging kernel_main function.
The current (somewhat proposed) model of stacked borrows has no concept of lifetimes, only usage timestamps. If you obey the usage rules it’s sound under that model even if you lie to the type system so it can’t protect you. Cc @RalfJung
(Disclaimer: this is by memory and not an authoritative source of what is or isn’t UB)
It grants an “arbitrary” shared reference through a closure that must diverge
since a diverging closure cannot use ! (it’s the never type instead of a diverging notation), I have used an empty enum for the same effect;
the lifetime is caller-chosen, hence the term “arbitrary”, but cannot of course “outlive the type”; i.e., for all types T of the local, the lifetime parameter 'a mut uphold that T : 'a (else &'a T doesn’t make sense);
an abort-on-drop bomb is used to prevent exploitation through stack unwinding; I have set up a scenario that would use-after-free otherwise (you may go an comment the let guard = ... line to see that for yourselves).
// #![feature(never_type)]
enum Diverging {}
use ::std::*;
struct AbortOnDrop;
impl Drop for AbortOnDrop { fn drop(&mut self) {
// Triggered the abort bomb!
process::abort();
}}
trait WithDiverging<'a> : Sized + 'a {
fn with_diverging (
self,
f: impl FnOnce(&'a Self) -> Diverging,
) -> !
{
#![allow(unused_variables)]
unsafe {
let guard = AbortOnDrop;
let diverged = f(mem::transmute(&self));
match diverged {
// !
}
// kabooms here if stack unwinds
// (*before* self is dropped)
}
}
}
impl<'a, T : Sized + 'a> WithDiverging<'a> for T {}
fn main ()
{
let _ = panic::catch_unwind(|| {
// Our local
let s = String::from("hi");
s.with_diverging(|at_s: &'static String| {
// can transmute to &'static since this closure diverges ...
assert_eq!(at_s, "hi");
thread::spawn(move || {
// &'static is given to another thread
// that constantly reads it
loop {
thread::sleep(time::Duration::from_millis(100));
dbg!(at_s);
}
});
thread::sleep(time::Duration::from_secs(1));
// ... and thanks to the abortbomb guard
panic!("Attempt at being evil");
})
});
// sleep to give time to the other thread to use at_s if unwind
thread::sleep(time::Duration::from_secs(3));
}
EDIT: the code can be changed into granting a unique reference, since it already takes ownership of T:
This was discussed as part of the NLL design. IIRC the core design input was that literally just loop{} essentially never happens and rust doesn’t have a panic-free effect, so nearly anything inside the loop would make it uncertainly-forever. And along with a desire to not have the borrowing rules differ for different panic implementations, that meant the decision was to just never allow this.
If you have a function you know really is forever, you can always just Box::leak something to get a &'static mut.
No, the NeverDrop value could be moved around, invalidating references. Or if you made it hold a borrowed reference in the first place, it could be leaked with mem::forget to avoid the guard.
Also, that get_mut allows mutable aliasing, because the disconnected lifetime doesn’t hold a borrow on the value anymore.
Since we'd need T : 'static to call .get(), how would that work?
It seems to me that with some pinning (and without .get_mut()!) that API might become sound, although it is very easy to miss an important detail.
Obviously if we are pinning, we may very possibly end up using Box, at which point Box::leak is a thousand times better. But I love these hypothetical challenges
I was trying to predict an alternate like struct NeverDrop<'a, T>(&'a T). This would solve the problem of the value moving since it's borrowed here. But since get's lifetime doesn't hold a borrow (same problem as get_mut), this guard can just be leaked or forgotten.
I'm not well versed in pinning, but yes, once you involve the heap, you might as well just Box::leak.
I agree this should be sound, I cannot see a problem with this.
However, we have to be a bit careful – not because of Stacked Borrows, but because if we generalize this idea to "replace some lifetime by 'static", we can make currently sound patterns of lifetime usage unsound. Namely, exploiting generative lifetimes relies on programs not being able to change invariant lifetimes to anything else, but if we argue that in a diverging function, we can have a lifetime 'a outlive 'static (and vice versa, because it’s 'static) then we can break some libraries.
Just a note: I did choose the most cautious approach for my API: taking ownership of a value of type T (which we could #[inline(always)] to hint at avoiding the copy), and then lending a &'a mut T for any 'a where T : 'a. I’m pretty sure this is not only currently sound, but sound even with regards to other patterns. Please correct me if I am wrong!
The elephant in the room here, to which @RalfJung’s post was directed (I think), is if instead of