This is a very easy trap to fall into, but isn't true. This example adapted from rio
's issue tracker and is originally by @dtolnay. (rio
is unsound because it relies on the property that memory is either dropped or never reclaimed. But please don't dogpile on the project, it's a known issue and probably well enough disclaimed.)
To be completely fair here, if you're saying lifetime as in the uniquely owned memory behind an indirection (i.e. Box
), I don't have a counterexample, and I think you're correct in that extremely minimal understanding[1]. But if you're saying lifetime as in the captured 'a
lifetime, and borrowed pointers, well, counterexample.
use std::{
cell::Cell,
marker::{PhantomData, Send},
ptr,
sync::Arc,
thread,
};
#[derive(Debug)]
enum Buffer {
Inline([usize; 3]),
Outline(Vec<usize>),
}
impl Buffer {
fn as_mut(&mut self) -> &mut [usize] {
match self {
Buffer::Inline(buf) => buf,
Buffer::Outline(buf) => buf,
}
}
}
struct BufWriter<'a>(*mut [usize], PhantomData<&'a mut [usize]>);
impl Drop for BufWriter<'_> {
fn drop(&mut self) {
eprintln!("BufWriter may never be dropped");
std::process::abort();
}
}
struct TrustMe<T>(T);
unsafe impl<T> Send for TrustMe<T> {}
impl<'a> BufWriter<'a> {
fn new(buf: &'a mut [usize]) -> Self {
assert_eq!(buf.len(), 3);
let this = BufWriter(buf, PhantomData);
let ptr = TrustMe(this.0 as *mut [usize; 3]);
thread::spawn(move || loop {
// [INCORRECT] SAFETY: as this BufWriter can
// never be dropped, it must be valid to continue
// to write into the buffer it holds.
unsafe { ptr::write(ptr.0, [1, 1, 1]) };
});
this
}
}
struct Oops<T>(T, Cell<Option<Arc<Self>>>);
fn main() {
let mut buf = Buffer::Inline([0, 0, 0]);
let w = BufWriter::new(buf.as_mut());
let oops = Arc::new(Oops(w, Default::default()));
oops.1.set(Some(oops.clone()));
drop(oops);
buf = Buffer::Outline(vec![]);
thread::sleep_ms(100);
dbg!(buf);
}
It's obviously too late to change the fact that mem::forget
/ManuallyDrop
are safe. But even if I could go back in time and bring my current knowledge about Rust, I'd still argue on the side of making them safe. The benefit of ManuallyDrop
for drop ordering and writing unwind-correct code is huge, and the benefit of "stack forgetting" being impossible is miniscule if present at all.
[1]: In fact, I think you could potentially argue that this weakened form of "only behind an owned pointer" "leaked == forever alive" is true even with stack forget
, because the whole idea of forget
is that the destructor isn't run, so the out-of-line data isn't cleaned up. The only possible benefit remaining is the "stack part" remaining alive, which isn't even potentially useful, because you can move it around freely, so there's no way to know where it is.
This is a very long-winded way to say that no, while mem::forget
/ManuallyDrop
are in fact the only way to not run destructors for a value on the stack (beyond aborting the process), removing the ability to do so does, in fact, give you no extra guaranteed properties about any object. Borrowck considers leaked objects to no longer have lock on any captured 'lifetimes.