What is going on with an reference that looks like it has been dropped?

I am messing around with an idea and have ended up trying some unsafe stuff. I have found myself in a situation where I have a reference that, from my understanding, is not referencing anything.

In the test unbind_child_context_ref, the ContextRef is deref'ed, and the reference is stored in a variable.
The ContextRef internals are set to None and, in my mind, the ChildContext is dropped. But calling on the child_context_ref doesn't cause the test to panic.

What is actually going on here?


use std::cell::{Ref, RefCell, RefMut, UnsafeCell};
use std::collections::HashMap;
use std::mem;
use std::ops::Deref;
use std::rc::{Rc, Weak};

pub struct Context {
    children: RefCell<ContextChildren>
}

struct ContextChildren {
    child_context: InternalContextRef<ChildContext>,
    // ...
}

fn replace_context<T>(internal_context: &mut InternalContextRef<T>, ctx: T) -> ContextRef<T> {
    if internal_context.is_not_null() {
        // nullify the last context
        let _ = internal_context.release();
    }

    let int_ctx_ref = InternalContextRef::new(ctx);
    *internal_context = int_ctx_ref.clone();
    
    ContextRef {
        internal: int_ctx_ref
    }
}

impl Context {
    pub fn new() -> Self {
        Context {
            children: RefCell::new(ContextChildren::new())
        }
    }

    fn bind_child_context(&self, id: u32) -> ContextRef<ChildContext> {
        let mut children = self.children.borrow_mut();

        // if id != children.child_context

        replace_context(
            &mut children.child_context, 
            ChildContext::new(id)
        )
    }

    fn unbind_child_context(&self) {
        let children = self.children.borrow_mut();
        let _ = children.child_context.release();
    }
}

impl ContextChildren {
    fn new() -> Self {
        Self {
            child_context: InternalContextRef::<ChildContext>::null()
        }
    }
}

pub struct ContextRef<T> {
    internal: InternalContextRef<T>
}

impl<T> Deref for ContextRef<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.internal
    }
}

struct InternalContextRef<T> {
    ptr: Rc<UnsafeCell<Option<T>>>
}

impl <T> InternalContextRef<T> {
    fn null() -> Self {
        InternalContextRef { 
            ptr: Rc::new(UnsafeCell::new(None))
        }
    }

    fn new(data: T) -> Self {
        InternalContextRef { 
            ptr: Rc::new(UnsafeCell::new(Some(data)))
        }
    }

    fn is_not_null(&self) -> bool {
        let ptr_ref = unsafe { & *self.ptr.get() };
        ptr_ref.is_some()
    }

    fn is_null(&self) -> bool {
        let ptr_ref = unsafe { & *self.ptr.get() };
        ptr_ref.is_some()
    }

    fn try_context(&self) -> Option<&T> {
        let ptr_ref = unsafe { & *self.ptr.get() };
        ptr_ref.as_ref()
    }

    fn release(&self) -> Option<T> {
        let ptr_mut_ref = unsafe { &mut *self.ptr.get() };
        ptr_mut_ref.take()
    }

    fn replace(&self, data: T) -> Option<T> {
        let ptr_mut_ref = unsafe { &mut *self.ptr.get() };
        ptr_mut_ref.replace(data)
    }
}

impl<T> Deref for InternalContextRef<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        let ptr_ref = unsafe { & *self.ptr.get() };

        if let Some(ctx) = ptr_ref.as_ref() {
            return ctx;
        } else {
            panic!("context has been released");
        }
    }
}

impl<T> Clone for InternalContextRef<T> {
    fn clone(&self) -> Self {
        InternalContextRef {
            ptr: Rc::clone(&self.ptr)
        }
    }
}

struct ChildContext {
    id: u32,
}

impl ChildContext {
    fn new(id: u32) -> Self {
        Self {
            id
        }
    }

    fn id(&self) -> u32 {
        self.id
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn child_context_creation() {
        let context = Context::new();

        let child_context = context.bind_child_context(1);
        assert!(child_context.id() == 1);
    }

    #[test]
    #[should_panic]
    fn unbind_child_context() {
        let context = Context::new();

        let child_context = context.bind_child_context(1);
        context.unbind_child_context();

        child_context.id();
    }

    #[test]
    #[should_panic]
    fn unbind_child_context_ref() {
        let context = Context::new();

        let child_context = context.bind_child_context(1);
        let child_context_ref = child_context.deref();
        context.unbind_child_context();
        // the internal ChildContext should be dealloc
        // child_context_ref is a reference that still lives

        child_context_ref.id();
    }
}

There's no panic because the call to deref() happened before you destroyed the value. The code is still wrong, though. If you run it under miri, it will detect the undefined behavior.

3 Likes