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...
^^^^^^^^^^^^^^^^^^
- 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?
- 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.
This is hopefully a small enough repro snippet:
I am experimenting with unsafe, so apologies if the code/comments above are wrong or misleading.
Running
cargo miri test, possible UB is detected becauseparent_today.child.replace(Box::new(Self { value, child: None }));then Miri is happy and reports no issue. Could I get some insight on this?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_tooto trigger the same error reported originally.