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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions