Is writing through the pointer returned from `slice.as_ptr()` UB?

I happened upon something similar to this snippet in the wild, and it looked like UB to me, but I was surprised to discover it seems to pass MIRI (at least on the playground).

fn main() {
    let mut v = vec![3u8; 5];
    let slice = &mut v[..];
    let ptr = slice.as_ptr() as *mut u8;
    unsafe { *ptr = 7; }
    println!("{:?}", v);
}

I was under the impression that since slice.as_ptr() takes a shared reference, mutating through the pointer returned from it was unsound. (I understand passing MIRI doesn't mean that UB is absent, but this seemed like the kind of case that MIRI would pick up on and I am curious why it passes.)

I found this topic, which seems related, but it's mostly talking about a clippy lint, not the exact mechanics involved.

Does anyone know why this passes MIRI or can anyone describe why this is or is not UB?

MIRI throws error with proper flags.

$ MIRIFLAGS="-Zmiri-track-raw-pointers" cargo +nightly miri run

error: Undefined Behavior: no item granting write access to tag <3462> at alloc1568 found in borrow stack.
 --> src/main.rs:5:14
  |
5 |     unsafe { *ptr = 7; }
  |              ^^^^^^^^ no item granting write access to tag <3462> at alloc1568 found in borrow stack.
  |
  = help: this indicates a potential bug in the program: it performed an invalid operation, but the 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
2 Likes

Right, I totally forgot about that flag.

fwiw, your intuition was correct here.

That code is UB because as_ptr() takes an immutable reference and returns a const pointer, we then cast that to a mutable pointer, and mutate the thing behind it. The only sound way to mutate something when you start with an &T reference is if you go through an UnsafeCell.

1 Like