Casting a ref to writable mem to &mut: UB or not?

use std::slice;
fn foo(b: &[i32]) {
    let x = unsafe {
        slice::from_raw_parts_mut(b.as_ptr() as *mut i32, 10)
    };
    x[5] = 10; // ub or not?
}

fn main() {
    let mut buf = [0; 10];
    foo(&buf);
    println!("buf[5] = {}", buf[5]);
}

I need to perform this hack because I need to hack some struct fields from another crates that are not pub. I know the underlying memory is writable. Is this behavior conformant or a UB?

This is UB because writing to shared memory (&T) is UB and the case that there is only one instance of the share, then you could call that a unique share (Not too sure how it's supposed to be taught/worded?) and you're at a &mut T. Another thing that makes this UB is just assuming the length of the new slice to be 10.

Please note that this is only UB if it's done wrong which safe rust disallows.

2 Likes

Also, if you're trying to write a struct field, the order of fields in a struct is not defined and can change across platforms and compiler versions, so you're probably not doing what you think you're doing. Consider patching the crate's code to do what you need.

2 Likes

This is what is known as instant UB. There are a few things classified as such. The reason is as @OptimisticPeach said. Rust assumes that all &mut _ are unique. But &_ means shared. This means that casting from &_ to &mut _ is invalid, because there is no way to guarentee that &T isn't being shared without external synchronization. Because of this, Rust assumes that anything behind a &T can't change

There is only 1 exception to this rule, UnsafeCell. An UnsafeCell<T> is a special type that can reach behind a &UnsafeCell<T> and give you have a *mut T which is valid to write, on the condition that the pointer is unique. UnsafeCell gives no way of checking if *mut T is in fact unique, so you must make sure of that yourself with external synchronization. Everything else that you see that reaches behind a &T to mutate T uses UnsafeCell<_> either directly or indirectly. For the simplest example, Cell<T>.

Cell<T> is defined as such. (some details that are not relevant have been removed, not all functions are shown)

use std::cell::UnsafeCell;

struct Cell<T> {
    value: UnsafeCell<T>
}

impl<T> Cell<T> {
    pub fn new(value: T) -> Cell<T> {
        Cell { value: UnsafeCell<T> }
    }
    
    pub fn replace(&self, value: T) -> T {
        let ptr = self.value.get();
        
        // this is safe because we don't hand out references to `T`
        // and `Cell<T>` is not `Sync`
        unsafe { std::mem::replace(&mut *ptr, value) }
    }
    
    pub fn swap(&self, other: &Self) {
        if !std::ptr::eq(self, other) {
            std::ptr::swap(self.value.get(), other.value.get())
        }
    }
        
        // this is safe because we don't hand out references to `T`
        // and `Cell<T>` is not `Sync`
        unsafe { std::mem::replace(&mut *ptr, value) }
    }
    
    // lots more convenience methods, like Cell::get or Cell::take
}

// This is safe because sending a `Cell<T>` to another thread doesn't invalidate anything,
// all of the unsafety is localized to just `swap` and `replace`, and it doesn't infect anywhere else
unsafe impl<T: Send> Send for Cell<T> {}

Note that this is the simplest usage of UnsafeCell that you can have. It works because Cell<T> can only be used in a single thread (can't be shared across multiple threads). This means that nothing can interrupt swap and replace while they are running, which means that we can unique control over that memory for the duration of the function.

Other notable types that use UnsafeCell in the standard library are RefCell, Mutex, RwLock, and atomics

5 Likes
  • Transmuting an & to &mut is UB
    • Transmuting an & to &mut is always UB
    • No you can't do it
    • No you're not special

But this isn't really limited to transmute -- it's just as bad through pointer casts.

8 Likes

To be absolutely clear, any way of going from &T to &mut T is immediately UB, unless there is a UnsafeCell in the process. Even with UnsafeCell you must uphold that &mut T is unique on your own, otherwise you will still cause UB.

4 Likes

And even if it's inside an UnsafeCell<T>, &T to &mut T is still UB because the &T exists so the uniqueness requirement of the &mut T in immediately violated.

(What one can do, if one is sufficiently careful, is get a &mut T to the inside of a &UnsafeCell<T>.)

4 Likes

For a more detailed reasoning behind all this (especially the UnsafeCell interaction), you may have a look at this blog post.

2 Likes

I'm actually not sure why transmute to &mut should be UB.
Logically speaking writing to such &mut should be UB rather than the transmute itself

Because the &T exists. Rust will immediately assume that the &mut produced by the transmute refers to something other than what the &T refers to. This can miscompile even if you never write to the &mut.

7 Likes

UB is "just" a contract violation, which happens as soon as the transmute takes place. Although most contract violations lead to bugs, some may sometimes not. Sure, by writing to a transmuted &mut you will surely have bugs, but "the writing" is not necessary. &mut _ has such strong semantics that the compiler can abuse them in other ways.

I have crafted a small example that transmutes a &mut _, but never uses that reference for writing, only for reading:

use ::std::{*, cell::RefCell};

#[allow(mutable_transmutes)]
unsafe fn alias_mut<Pointee> (p: &'_ Pointee) -> (&'_ Pointee, &'_ mut Pointee) // same input lifetime
{
    (p, mem::transmute(p))
}

type T<'a> = Option<&'a i32>;

fn main ()
{
    let x: RefCell<T> = RefCell::new(Some(&42));
    let (p1, p2) = unsafe { alias_mut(&x) };
    foo(p1, p2.get_mut())
}

fn foo (p1: &RefCell<T>, p2: &T)
{
    if let &Some(ref at_inner) = p2 {
        *p1.borrow_mut() = None;
        println!("{}", **at_inner);
    }
}

12 Likes

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