claytonwramsey / dumpster

Cycle-tracking garbage collector library for Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 of Drop, 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 of Collectable.

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 during Drop.
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.