Is this recast/transmute of references sound?

So basically, is it safe to transmute from one struct reference to another, as long as:

  • they have the same layout and field types.
  • the only difference is the mutability of field references in those structs.
  • source struct has fields that are unique references, while in dest struct they are shared.

Miri doesn't complain when running this:

#[cfg(test)]
mod transmute_tests {
  use std::ops::{Deref, DerefMut};
  #[repr(C)]
  pub struct RefGuard<'a> {
    /// shared ref to ctx
    pub ctx:          &'a Vec<i32>,
    other_fields: f32,
  }
  impl RefGuard<'_> {
    pub fn access(&self) -> &i32 { &self.ctx[0] }
  }
  impl Drop for RefGuard<'_> {
    fn drop(&mut self) {
      unreachable!("We never construct RefGuard in this example, just hand out references to it.")
    }
  }

  #[repr(C)]
  pub struct MutGuard<'a> {
    /// unique ref to ctx
    pub ctx:          &'a mut Vec<i32>,
    other_fields: f32,
  }
  impl<'a> Deref for MutGuard<'a> {
    type Target = RefGuard<'a>;
    fn deref(&self) -> &Self::Target {
      // SAFETY:
      // `MutGuard` and `RefGuard`  have the same layout and field types.
      // The only difference is that `MutGuard` has unique reference,
      // and `RefGuard` has shared.
      // Casting unique->shared with the same lifetime should be safe?
      unsafe { &*(self as *const MutGuard as *const RefGuard) }
    }
  }
  impl<'a> DerefMut for MutGuard<'a> {
    fn deref_mut(&mut self) -> &mut Self::Target {
      // SAFETY: Same as for `deref`
      unsafe { &mut *(self as *mut MutGuard as *mut RefGuard) }
    }
  }
  impl Drop for MutGuard<'_> {
    fn drop(&mut self) { test_access(self); }
  }
  #[test]
  fn test_transmute() {
    let mut ctx = vec![1; 1024];

    let mut mut_guard = MutGuard { ctx: &mut ctx, other_fields: 0.0 };
    test_access(&mut mut_guard)
  }
  fn test_access(mut_guard: &mut MutGuard) {
    mut_guard.access();
    mut_guard.ctx[0] = 2;
    mut_guard.access();
    mut_guard.ctx[0];
    mut_guard.ctx[0] = 3;

    mut_guard.deref().other_fields;
    mut_guard.other_fields = 1.0;
    mut_guard.deref().other_fields;

    let ref_guard = &mut_guard;
    ref_guard.access();
    mut_guard.ctx[0] = 4;
    let ref_guard_mut = mut_guard.deref_mut();
    ref_guard_mut.access();
    ref_guard_mut.other_fields = 2.0;
    mut_guard.other_fields;
    mut_guard.other_fields = 3.0;

    mut_guard.deref().other_fields;
    mut_guard.deref_mut().other_fields;
    mut_guard.deref_mut().other_fields = 4.0;
    mut_guard.other_fields;
  }
}

What is the public API you indent to expose?

I've edited the example.
Both RefGuard and MutGuard structs are public. As well as ctx field on both. (Also, there are various public functions beside access in impl RefGuard)

How would users of this API construct a MutGuard if other_fields is not public?

Through methods on another struct that owns ctx:

pub struct State {
  ctx: Vec<i32>,
}
impl State {
  pub fn get_ref_guard(&self) -> RefGuard { 
    RefGuard { ctx: &self.ctx, other_fields: 0.0 } 
  }

  pub fn get_mut_guard(&mut self) -> MutGuard { 
    MutGuard { ctx: &mut self.ctx, other_fields: 0.0 }
  }
}

That means State has to be in the same module as MutGuard/RefGuard (or a child module of it).

Correct

The DerefMut for MutGuard is unsound:

static READONLY: Vec<i32> = Vec::new();
let mut vec = Vec::new();
let mut guard = MutGuard {
    ctx: &mut vec,
    other_fields: 0.0,
};
let immut_guard = RefGuard {
    ctx: &READONLY,
    other_fields: 0.0,
};
let gref: &mut RefGuard<'_> = &mut *guard;
*gref = immut_guard;
// This modifies the static Vec!
guard.ctx.push(5);
println!("{READONLY:?}");

This playground segfaults, and shows UB under Miri.

Imagine &'a mut MutGuard<'b> as &'a mut &'b mut T, and &'a mut RefGuard<'b> as &'a mut &'b T. Having a free coercion from &mut &mut T to &mut &T would be disastrous, because you could write a &T and then use it as a &mut T when the memory isn't even writable.

3 Likes
  • MutGuard and RefGuard cannot be counstructed directly by users. They get them through get_mut_guard and get_ref_guard on State.
  • Also, ctx in not actually a Vec but another type from this module that cannot be constructed directly by users.

Can UB still be reproduced?

Challenge accepted.

//  vvvvvvvvv
let immutable_state = State::new();
dbg!(&immutable_state);
let ref_guard = immutable_state.get_ref_guard();
let mut mutable_state = State::new();
let mut mutable_guard = mutable_state.get_mut_guard();
let place: &mut RefGuard = &mut *mutable_guard;
*place = ref_guard;
mutable_guard.ctx.some_mutating_function();
// The immutable state has mutated
dbg!(&immutable_state);

This playground shows a specific example with the value as Vec, where let x = &EXPR, but x != EXPR. (Miri does flag UB, as expected)

1 Like

Even the Deref is unsound, for similar reasons as to why you can't get a &'long _ from a &'short &'long mut _.

(You should show your complete code so that we don't have to try to gradually discover your actual public API by echo location.)

4 Likes

Thanks so much for taking the time to investigate!
State is also not something user can ever construct.
User receives it as:

run_loop(|state:&mut State|{
});

run_loop never terminates, and would crash if called again.
(Yea, I'm sorry I should have written a better example in the first place.)

Another example of echo location... but my previous comment isn't effected by this one, you still have unsoundness. AFAICT so far.

3 Likes

Yea, you are definitely correct, my use-case is unsound.