Hi,
I did some experiments for learning lifetimes, raw pointers and unsafe.
All experiments use a simple struct:
#[derive(Debug)]
struct D(i32);
impl Drop for D {
fn drop(&mut self) {
println!("D is dead (value was {})", self.0);
}
}
The first experiment creates a simple lifetime issue with reference, which is rejected by rustc (which makes perfectly sense to me):
// This creates and uses a dangling reference
// Rejected as expected
pub fn dangling_reference() {
let value = D(42);
let ref_to_val = &value;
drop(value);
println!("Result: {:?}", *ref_to_val);
}
Now I'm doing the same with raw pointers, which seems to work (debug+release) and miri does not complain:
// This creates a raw pointer to an object,
// drops the object but still dereferences the ptr
pub fn should_be_dangling_ptr() {
let value = D(42);
let ptr_to_val = ref_to_ptr(&value);
drop(value);
println!("Result: {:?}", unsafe { (*ptr_to_val).0 });
}
Execution:
$ cargo +nightly miri run
D is dead (value was 42)
Result: 42
Similar lifetime issue, but from another function -- this is reported as incorrect by miri:
// Return a dangling ptr
fn ret_dropped() -> *const D {
let d = D(31);
&d
}
pub fn dangling_ptr() {
let ptr_to_val = ret_dropped();
println!("Result: {:?}", unsafe { (*ptr_to_val).0 });
}
I've seen this test case print an incorrect "result value" in release mode (or a similar one; cannot reproduce it anymore).
What I don't understand is why case 2 is not reported by miri and always seemed to be working correctly: This should be easier to detect than case 3. Is this a limitation of miri or is there something going on which I'm not aware of (like a temporary lifetime extension which is only used with raw ptrs)?
In the case of 3 the stack slot for d is deallocated when returning from ret_dropped and as such the pointer is truly dangling. In the case of 2 the stack slot for value is not deallocated, instead the drop(value) call copies the value into the stack frame of the drop() call, but leaves the original value in place. Borrowck doesn't allow accessing it, but with unsafe code you can still access it.
In the case of 2 if I enable optimizations, I do see a StorageDead for value right after drop(value), so accessing it should be UB, but it may be the case that miri doesn't see the StorageDead due to running without optimizations and us removing StorageDead when optimizations are disabled to reduce compile times.
Edit: Miri enables flags to keep the StorageDead, but I was wrong about the location of the StorageDead for value. It happens at the end of the function.
Thank you very much for the thorough answer, @bjorn3! If I understand it correctly, miri doesn't detect this issue (case 2; without optimization) because the StorageDead flag gets removed to reduce compile times. This is unfortunate as it seemed to be relatively easy to detect. Is there a possibility to disable the compile-time optimization (or other settings to detect it)?
I was wrong about the StorageDead getting removed with miri. Miri sets a flag to keep them. The reason miri doesn't detect case 2 is because the StorageDead coincides with the end of the scope in which value exists, which is after the access.