Hi Rustaceans!
I was seeing Jon Gjengset's Crust of Rust on Smart Pointers and Interior Mutability, on the Cell topic, when someone asked what would happen with a wrong implementation. He then intentionally let his Cell::get()
method retrieve a reference to its content and implemented a test that held that reference and tried to use it after it was dropped. The objective was to see a SEGFAULT or whatever the effect of trying to use a dangling pointer is. But, to everyone's bewilderment including Jon's, the code worked!
Here is the video at the exact time, but I've improved the test code here and discovered a few things myself:
- the Drop is indeed being called on the first content
- I intentionally set a second String bigger than the first one so it would be required to allocate more space for it (he says in the video the deallocator happened not to have deallocated the first one yet, but for me, it doesn't make sense)
- bizarrely, the reference returned by
get()
after the secondset()
is the same as the previous one
Well, thinking deeper about it, since get()
returned a reference with the exact same address as before (the value
field of its struct, I suppose) and its inner content has surely changed, does this mean that the stack part of the String (pointer to heap, length, and capacity) was reused into the same UnsafeCell
field space? It is repr(transparent)
after all...
(Awesome! I came to this conclusion while trying to conclude this post )
So, does this mean Jon would not be able to demonstrate it breaking, at all?
EDIT: Humm, if that doesn't really break this Cell
, why std's Cell
can't return a reference to its content?
Thanks.
use std::cell::UnsafeCell;
pub struct Cell<T> {
value: UnsafeCell<T>,
}
impl<T> Cell<T> {
pub fn new(value: T) -> Self {
Cell {
value: UnsafeCell::new(value),
}
}
pub fn set(&self, value: T) {
unsafe { *self.value.get() = value };
}
// this is WRONG, intentionally.
pub fn get(&self) -> &T {
unsafe { &*self.value.get() }
}
}
#[test]
fn why_this_works() {
#[derive(Debug)]
struct DropString(String);
impl Drop for DropString {
fn drop(&mut self) {
println!(">> drop: {}", self.0);
}
}
let x = Cell::new(DropString(String::from("first")));
let s = x.get();
println!("addr of s : {s:p}");
x.set(DropString(String::from("oops, a new value!")));
println!("addr of get(): {:p}", x.get());
println!("print {s:?}");
}
It prints:
cargo test -- --nocapture
running 1 test
addr of s : 0x16d6b6598
>> drop: first
addr of get(): 0x16d6b6598
print DropString("oops, a new value!")
>> drop: oops, a new value!
test why_this_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s