Skip to content

Assigning Some instead of calling replace triggers Miri under some unsafe conditions #2722

@angelorendina

Description

@angelorendina

This is hopefully a small enough repro snippet:

#[test]
fn family() {
    struct Parent {
        value: u8,
        child: Option<Box<Self>>,
    }

    impl Parent {
        fn declare_child(&mut self, value: u8) {
            self.child.replace(Box::new(Self { value, child: None }));
        }

        fn declare_child_too(&mut self, value: u8) {
            self.child = Some(Box::new(Self { value, child: None }));
        }
    }

    // create the first ancestor
    let mut progenitor = Parent {
        value: 0,
        child: None,
    };

    // we record the lineage as a stack of raw pointers to each generation
    // we cannot have &mut as there would be aliasing/memory overlap
    // (e.g. both `x` and its child `y` could write into `x.child.value` <-> `y.value`)
    let mut past_parents = vec![];
    // to make this safe to use, we keep around one exclusive &mut reference
    // through which we perform all read and writes
    let mut parent_today = &mut progenitor;

    for i in 1..4 {
        // today's parent is becoming tomorrow's grandparent, so:
        // record it in the lineage (as a pointer)
        past_parents.push(parent_today as *mut Parent);
        // generate the new child
        parent_today.child = Some(Box::new(Parent { value: i, child: None }));
        // and make that the new parent for tomorrow
        parent_today = parent_today.child.as_mut().unwrap();
    }

    // now we can use the stack to navigate up the lineage
    while let Some(ptr) = past_parents.pop() {
        // SAFETY:
        // - ptr is valid (obtained directly from a valid &mut)
        // - aliasing is respected (there are no other references to the data, just this one &mut)
        parent_today = unsafe { ptr.as_mut().unwrap() };
    }

    // we should have reached the ancestor
    assert_eq!(parent_today.value, 0);
}

I am experimenting with unsafe, so apologies if the code/comments above are wrong or misleading.

Running cargo miri test, possible UB is detected because

help: <192863> was created by a SharedReadWrite retag at offsets [0x0..0x10]
   past_parents.push(parent_today as *mut Parent);
                     ^^^^^^^^^^^^
help: <192863> was later invalidated at offsets [0x0..0x8] by a write access
   parent_today.child = Some(Box::new(Parent { val...
   ^^^^^^^^^^^^^^^^^^
  1. I am not really sure I understand the problem here. If I rewrite that assignment as parent_today.child.replace(Box::new(Self { value, child: None })); then Miri is happy and reports no issue. Could I get some insight on this?
  2. If I declare helper methods
impl Parent {
    fn declare_child(&mut self, value: u8) {
        self.child.replace(Box::new(Self { value, child: None }));
    }

    fn declare_child_too(&mut self, value: u8) {
        self.child = Some(Box::new(Self { value, child: None }));
    }
}

which just wrap the two different behaviours of the previous point, then Miri reports no issue when using either (in place of the incriminated assignment). In particular, I would expect declare_child_too to trigger the same error reported originally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions