Slice::as_ptr and UnsafeCell relation

slice::as_ptr's description says:

The caller must also ensure that the memory the pointer (non-transitively) points to is never written to (except inside an UnsafeCell) using this pointer or any pointer derived from it. If you need to mutate the contents of the slice, use as_mut_ptr.

But when we use a slice with UnsafeCell, we don't ever use const pointer (which we obtain from slice::as_ptr), but mut pointer. So why is UnsafeCell mentioned in context of const slice pointer?

Well… if you have a slice: &[UnsafeCell<T>], it’s fine to get a *const UnsafeCell<T> from slice.as_ptr(), then offset that in case you want to access something other than the first element, then use UnsafeCell::raw_get to get a *mut T to the contained data, and finally mutate it.

Assuming you make sure that the mutation is data-race free (and doesn’t create aliased &mut T’s or aliased mutable references to parts of the T), your offsets are in-bounds, and you don’t keep around any pointer after the original slice is freed, or the original &[UnsafeCell<T>] reference is dead (which may be a lot earlier).

The documentation of as_ptr just tries to be clear on that the rules don’t rule out such a thing. Of course, as the user of unsafe API, you’re always free not to do everything you’re theoretically allowed to do, and thus it’s totally fine if you truly treat the data behind the pointer returned by slice::as_ptr as immutable. Though if user-callbacks are involved, and you – say – pass a &T reference (of appropriate lifetime) to an element of a &[T] slice back to user-code, then that callback might possibly mutate the contents of T, anyways, in case T happens to be instantiated as something like T == Cell<u8>.

For example if you call .iter() on a slice, the standard library implementation uses .as_ptr() to create a *const T based on which the iterator’s ptr and end fields are populated. If a user calls for i in slice.iter() { i.set(42) } on a slice: &[Cell<i32] then they have effectively mutated data through a pointer obtained via slice::as_ptr(), however the data that was modified was wrapped inside of an UnsafeCell (contained in Cell), and the UnsafeCell API was properly used to obtained the mutable access (again, indirectly, through the Cell API), so there isn’t any problem.

4 Likes

Thank you for the explanation, but I'm not sure why using UnsafeCell is helpful here at all.
From what I get, I can mutate the T data obtained from UnsafeCell received from hypothetical &[UnsafeCell<T>]. It's clear, but as I understand, I can only mutate it making sure that all of the safety rules are preserved. So in that case, why can't I just mutate T from &[T] or &[Vec<T>] if I make sure the safety rules are preserved? What's the advantage or using UnsafeCell here?

There are no safety rules you can follow in order to mutate a T from a &[T] access, if UnsafeCell isn't involved. Trying to do will always and unconditionally result in undefined behavior. E.g. to name some concrete types, if you try to mutate a u8 through a &[u8] reference, there was no UnsafeCell involved in the &[u8], so it's undefined behavior, no matter how you did it. Note that "no matter how" really means "not matter how". For example turning &[u8] into &[UnsafeCell<u8>] and then mutating it is still not allowed.

1 Like

UnsafeCell is a special marker that tells the compiler that stuff behind &UnsafeCell<T> may be mutated and this is allowed. No other type should be mutated behind a shared reference.

1 Like

There's nothing specific to slice here. This is just a reminder of the rule that you are not allowed to write to a location obtained through a shared reference, and UnsafeCell is always the only exception to that. It's useful to have this reminder since someone might do something like

let mut a = [1, 2, 3];
let b: &mut [u8] = &mut a;
let p = b.as_ptr();

Even though there is no visible shared reference, calling as_ptr converts &mut to &.

There isn't any difference in writability of *const compared to *mut. For example, if you obtain a *const from &mut, you can soundly convert it to *mut and write to it. The lack of writability of as_ptr comes from the shared reference. This would also be true if the conversion happened inside the function.

3 Likes

Be careful of the phrasing. I'm not sure of the behavior of casting &mut T to *const T directly, oft the top of my head, but I would be caucious (and/or consult miri). Turning &mut T into *mut T, then into *const T is fine to retain the permission to mutate, and turning &mut T into &T, then into *const T certainly isn't.

I was thinking of something like this:

fn change(r: &mut u8) -> *mut u8 {
    let c = r as *const u8;
    c as *mut u8
}

This would be equivalent to doing r as *mut u8, right?

Well… asking miri is easy enough

fn change(r: &mut u8) -> *mut u8 {
    let c = r as *const u8;
    c as *mut u8
}

fn main() {
    let mut x = 0;
    let p = change(&mut x);
    unsafe {
        *p += 1;
    }
}

answer: not okay

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `/playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/playground`
error: Undefined Behavior: attempting a write access using <1681> at alloc853[0x0], but that tag only grants SharedReadOnly permission for this location
  --> src/main.rs:10:9
   |
10 |         *p += 1;
   |         ^^^^^^^
   |         |
   |         attempting a write access using <1681> at alloc853[0x0], but that tag only grants SharedReadOnly permission for this location
   |         this error occurs as part of an access at alloc853[0x0..0x1]
   |
   = 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: <1681> was created by a SharedReadOnly retag at offsets [0x0..0x1]
  --> src/main.rs:2:13
   |
2  |     let c = r as *const u8;
   |             ^
   = note: BACKTRACE (of the first span):
   = note: inside `main` at src/main.rs:10:9: 10:16

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to previous error


Writing let c = r as *mut u8 as *const u8; instead appears to make a significant difference.

No, because the r as *const u8 syntax actually expands to this:

  1. Convert the &mut u8 to &u8.
  2. Convert the &u8 to a *const u8.

However, if you wrote

fn change(r: &mut u8) -> *mut u8 {
    let c = r as *mut u8 as *const u8;
    c as *mut u8
}

Then it's perfectly fine, as this is the same as:

  1. Convert the &mut u8 to *mut u8.
  2. Convert the *mut u8 to a *const u8.

The permissions of a raw pointer is determined by the kind of reference used when creating the raw pointer. Using an immutable reference to create the raw pointer gives one without permission to write.

1 Like

That's weird. Why does it do that?

I didn't even know you could cast a &mut to *const directly. I somehow always thought reference-to-pointer conversion/coercion was required to preserve mutability. Certainly, it's way better to do that and be explicit even if the above "works", because this UB is subtle to the point of being scary. Is there a Clippy lint for this perchance? (On my phone so inconvenient to search for it.)

1 Like

Sort of. Clippy is able to see that the mutability is not being used.

warning: this argument is a mutable reference, but not used mutably
 --> src/lib.rs:3:18
  |
3 | pub fn change(r: &mut u8) -> *mut u8 {
  |                  ^^^^^^^ help: consider changing to: `&u8`
  |

Which won't happen with as *mut. But this is easy to make not appear, like if you mutate the value first and then convert it.

P.S. also as_conversions, but that'll warn on every as so it's not that useful

Also there's borrow_as_ptr against &mut as *mut and & as *const casts, which would suggest usage of addr_of[_mut]. Haven't tested if it applies to &mut as *const, too, but I'd be very surprised if it didn't.

Nevermind, that's against expressions of the form &EXPR as *const Z and &mut EXPR as *mut T, i. e. expressions that create the borrow at the same time.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.