MIRI error, with `Drop`

I am not good at English. Sorry if there are any funny expressions.


My Rust code contains functions miri_ok and miri_ng. The only difference between two is the method of instance dropping. But, MIRI only outputs an error for the latter.

The reason for this is probably because on_drop is being called while the drop function's exit point still protects self (though I'm not fully understanding MIRI's Stacked Borrows model paper, so I'm not entirely sure...).

Putting the reasons aside, what troubles me is that drop causes an error in the Stacked Borrows model (Tree Borrows model does not cause error). There's no way we can stop API users from using drop, right? But since it's possible with an implicit drop, I wondered there might be some good approach.

Code

#![allow(unused)]

#[test]
fn miri_ok() {
    let mut var = 0;
    let mut twp = TwoWayPtr::new(&mut var);
    twp.on_drop();
}

#[test]
fn miri_ng() {
    let mut var = 0;
    let mut twp = TwoWayPtr::new(&mut var);
    drop(twp);
}

struct TwoWayPtr<'a> {
    val_ptr: *mut i32,
    val_ref: Option<&'a i32>,
}

impl<'a> TwoWayPtr<'a> {
    fn new(val: &'a mut i32) -> Self {
        let val_ptr = val as *mut _;
        let val_ref = Some(unsafe { &*val_ptr });
        Self { val_ptr, val_ref }
    }

    fn on_drop(&mut self) {
        self.val_ref.take(); // For multiple alias avoiding.
        let _ = unsafe { &mut *self.val_ptr };
    }
}

impl Drop for TwoWayPtr<'_> {
    fn drop(&mut self) {
        self.on_drop();
    }
}

Error excerpt

test miri_ng ... error: Undefined Behavior: not granting access to tag <146804> because that would remove [SharedReadOnly for <146817>] which is strongly protected
  --> src\lib.rs:40:26
   |
40 |         let _ = unsafe { &mut *self.val_ptr };
   |                          ^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <146804> was created by a SharedReadWrite retag at offsets [0x0..0x4]
  --> src\lib.rs:33:23
   |
33 |         let val_ptr = val as *mut _;
   |                       ^^^
help: <146817> is this argument
  --> src\lib.rs:23:5
   |
23 |     drop(twp);
   |     ^^^^^^^^^
   = note: BACKTRACE (of the first span) on thread `miri_ng`:
   = note: inside `TwoWayPtr::<'_>::on_drop` at src\lib.rs:40:26: 40:44

Note

val_ptr and val_ref are used for simplicity in explanation. In the actual code, they are a pointer to the map and a mutable reference to its iterator. This was done because I wanted to access the map after using the iterator.

It does if you write through the reference in on_drop. (Presumably you want to, hence &mut?)

     fn on_drop(&mut self) {
         self.val_ref.take();
-        let _ = unsafe { &mut *self.val_ptr };
+        let hm = unsafe { &mut *self.val_ptr };
+        *hm = 0;
     }

I think what's going on is that lifetimes passed to a function must be valid for the entirety of the function call. When you pass a TwoWayPtr<'a> to some function (in this case, std::mem::drop, which is a normal function, not magic like Drop::drop), Miri interprets that as the contained shared borrow needing to be valid through the call completion, including the destructor call.

If you delete the Drop implementation, this still gives the same error.

fn miri_ng() {
    let mut var = 0;
    let mut twp = TwoWayPtr::new(&mut var);
    by_value(twp);
}

fn by_value(mut twp: TwoWayPtr<'_>) {
    twp.on_drop();
}

This doesn't help:

fn by_value(mut twp: TwoWayPtr<'_>) {
    twp.val_ref.take(); // <-- new
    twp.on_drop();
}

Because the reference exists upon entry to the function and it's "rights" extend through the call to on_drop.[1]

On the other hand, this does remove the error (on both stacked and tree borrows).

fn miri_ng() {
    let mut var = 0;
    let mut twp = TwoWayPtr::new(&mut var);
    twp.val_ref.take(); // <-- new
    by_value(twp);
}

I suggest just avoiding references for so long as you're in raw pointer land, to the extent possible. They have too many guarantees.

struct TwoWayPtr<'a> {
    val_ptr: *mut i32,
    val_ref: *const i32,
    _borrow: PhantomData<&'a mut i32>,
}

  1. Similarly I don't think your take in on_drop is effective. â†Šī¸Ž

1 Like

Oh, I understand. I should have made both of them pointers.
That was completely blind to me. Very helpful. Thank you very much.

This is irrelevant to your main problem, but to answer this question: the only way to prevent a user from dropping a value is to not let them have ownership of the value. That is, if you wish type Foo to not be dropped outside of your control, you must only give out &Foo — not Foo and not &mut Foo.

1 Like

Thank you for the precise explanation (I often forget that the reference target of &mut can be dropped by the replace function.)