Unsafe question

Let's say i have a struct with two fields:

struct Foo {
    a: i32,
    b: bool,
}

Now, assume ptr is a *const Foo (due to variance) that points to a valid Foo. Would this be safe?

use std::ptr::NonNull;

unsafe fn f(ptr: NonNull<Foo>) {
    // modify the b field
    ptr.as_mut().b = true;
}

unsafe {
    let foo = &*ptr;
    f(NonNull::from(foo));  // (1)
    let res = &foo.a;  // (2)
    // use res
}

If not, would it make a difference if (1) and (2) are swapped?
And would it be safe if I passed NonNull::new_unchecked(ptr as *mut Foo) to f instead?

You cannot write to a pointer derived from a &T unless you pass through a UnsafeCell. (which you are not doing in this case). You can do a simple check of your code using miri (this also works on playground under the tools dropdown). miri is not perfect, so it won't catch all your mistakes, but if it complains then you are almost certainly making a mistake.

Does that still hold when the &T comes from a raw pointer?

Yes, even if it came from a raw pointer. References in Rust have quite a few invariants attached to them, so don't treat them like pointers. Another example is: &mut T is an exclusive reference that may not be aliased by any unrelated pointer.

See the rust nomicon for more info about unsafe, and stacked borrows for the closest thing Rust has to an aliasing model.

Unspecified Behavior (UB) is like Russian roulette; each time you recompile (because you made a change, or a library changed, or there is a new release of the compiler) you are spinning the barrel and pulling the trigger again. You might be lucky in the short term, but your luck won't last.

Note that the following might (potentially, keep reading) be fine:

unsafe {
    let foo = &mut *(ptr as *mut _);
    f(NonNull::from(foo));  // (1)
    let res = &foo.a;  // (2)
    // use res
}

Raw pointers have no aliasing rules, and raw pointer mutability is more like a suggestion than a rule, per se (they're much like C pointers in that respect). It's fine to cast a *const T to a *mut T temporarily and mutate the T through that. There are still a lot of ways this can go wrong, though; off the top of my head:

  1. You have to get the *const T initially in a way that allows you to mutate through it. In practice this means you have to get it by casting a *mut T that you got by taking a &mut T.
  2. Even though you're using raw pointers, you still have to honor the terms of all the borrow expressions in your program. So assuming you do what I just suggested and get the *const T by casting a &mut T, you can't have any other references to the same T that weren't derived from that &mut borrow until you are done using it. Swapping lines (1) and (2) also technically breaks this rule because it creates a mutable reference that aliases a shared reference. Even using raw pointers, you're still not allowed to violate the rules of references.
  3. You mentioned it's *const T and not *mut T because of variance. Variance is another way this can go wrong. If T is something like Foo<'long> and variance shrinks it down to Foo<'short>, a function similar to f could theoretically stuff a &'short into a slot labeled &'long, which would lead to unsoundness if the reference is then followed outside its proper lifetime. I don't think this can happen with the given f because b is just a bool, so you might be in the clear. It depends on how close this is to your real code.
1 Like

Basically if you want a raw pointer you can write through, you need to create it from a &mut reference.

Ok, so i assume this

unsafe {
    let foo = &*ptr;
    f(NonNull::new_unchecked(ptr as *mut Foo));  // (1)
    let res = &foo.a;  // (2)
    // use res
}

would be UB as well?

Using Box::into_raw works too, right?

Yes

Yes, you can also write to a raw pointer obtained from an UnsafeCell.

1 Like

I wasn't 100% sure about this, because there could be an argument that the borrow &*ptr doesn't get "used" until it's safe to do so, but Miri doesn't like it (go to Tools -> Miri):

error: Miri evaluation error: trying to reborrow for SharedReadOnly, but parent tag <1563> does not have an appropriate item in the borrow stack
  --> src/main.rs:16:15
   |
16 |     let res = &foo.a;  // (2)
   |               ^^^^^^ trying to reborrow for SharedReadOnly, but parent tag <1563> does not have an appropriate item in the borrow stack
   |
note: inside call to `bar` at src/main.rs:23:9
  --> src/main.rs:23:9
   |
23 |         bar(&mut obj as *mut _);
   |         ^^^^^^^^^^^^^^^^^^^^^^^
   = note: inside call to `main` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:67:34:

I take this to mean that creating the &mut inside f forcibly ends the earlier, shared borrow, after which it's invalid to try to use foo again. (The version I posted earlier, with a small tweak, does run without errors.)

You can use Miri to check your assumptions about the borrowing model, but be careful -- testing your code in Miri is not a sure way to guarantee it will never have undefined behavior.

Yes, even if you avoid the UB of mutating through a (pointer that originated from a) & reference, you still cannot swap the lines, otherwise it would be a violation of Stacked Borrows; see this post for an explanation.

There are some recent discussions on the Internals forum that are relevant here:

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.