Unsoundness when dropping cycles with custom `Drop` implementation
wishawa opened this issue · comments
In this code...
#[test]
fn use_after_drop() {
#[derive(Collectable)]
struct Demo {
// for debug printing
id: i32,
// number of times `drop` was called on this struct
dropped: i32,
// for making cycles
other: RefCell<Option<Gc<Demo>>>
}
impl Drop for Demo {
fn drop(&mut self) {
if let Some(other) = self.other.borrow().as_ref() {
if other.dropped > 0 {
// if `other` had been dropped, we shouldn't be able to access it
panic!("accessing gc object with id {} after it was dropped!!", other.id);
}
}
self.dropped += 1;
}
}
// make a cycle
let gc1 = Gc::new(Demo {
id: 1,
dropped: 0,
other: Default::default()
});
let gc2 = Gc::new(Demo {
id: 2,
dropped: 0,
other: RefCell::new(Some(gc1.clone()))
});
*gc1.other.borrow_mut() = Some(gc2.clone());
}
the drop
call for the second Demo
object is able to access the first Demo
object after it was dropped.
Suggested fix: make #[derive(Collectible)]
generate a Drop
impl so the user can't supply one. This is not a big functionality hit because people can always write something like this
let my_gced_object = MyStruct {
gc1: Gc::new(...),
gc2: Gc::new(...),
actual_data: MyStructInner {
...
}
}
impl Drop for MyStructInner {
...
}
My original idea for handling this was covered in a remark in Collectable
- in essence, buyer beware - with Collectable
as an unsafe trait. I think that the approach of forcing a Drop
implementation on all Collectable
types is far more elegant, though. Will do.
@wishawa By the way, do you know any elegant way to extract a TokenStream
for the "default" implementation of Drop
, or do I have to go do that myself?
A possible wrinkle: in your example solution, MyStructInner
would have to be either !Collectable
or have a manual implementation of Collectable
. An alternate solution would be to make Gc
operations panic if dereferenced during Drop
.
@wishawa By the way, do you know any elegant way to extract a
TokenStream
for the "default" implementation ofDrop
, or do I have to go do that myself?
The "default" implementation of drop is just
impl Drop for Type {
fn drop(&mut self) {}
}
right?
But if you meant the drop glue, I have no idea. I don't know if it even exists as Rust code (as opposed to being some intermediate representation thing).
A possible wrinkle: in your example solution,
MyStructInner
would have to be either!Collectable
or have a manual implementation ofCollectable
.
True. I didn't think of that. One fix would be to provide a mostly transparent ManuallyCollectable
type analogous to std::mem::ManuallyDrop
.
An alternate solution would be to make
Gc
operations panic if dereferenced duringDrop
.
This might be the cleanest way to go about it.
An issue is that with careful use of Pin
, it is possible to safely make self-referential types that hold references derefed before the drop. I don't think there is a general fix for this. The good thing is this is almost impossible to unintentionally run in to.
I've now made it so that Gc::deref
will panic if dereferenced against a value which is currently being dropped.