A Type Safety Hole in Unsafe Rust

Florian Weimer

There seems to be a widespread belief among Rust programmers that bypassing the borrow checker does not completely compromise type safety. This article attempts to show the converse, that lifetime checks are absolutely essential for type safety, even if no dangling pointers are involved.

Unsafe Rust offers a variant of the as operator which can convert between two arbitrary raw pointer types, something which is not supported by the safe language (which heavily restricts operations on raw pointers). However, we are not going to use this extension, but try to write a magic conversion function from scratch, just using aliasing.

Our example follows A Hole in Ada Type Safety. The key ingredient there was a mutable variant type which has aliasable components (similar to a union in C). We mirror that in Rust with the following type definitions:

struct Uncopyable<T> {
  value : T
}

enum Magic<A, B> {
  A (Uncopyable<A>),
  B (Uncopyable<B>)
}

The struct type seems required because otherwise, the values in the enum variants end up copyable, and the in-place mutation is not visible.

To implement the magic conversion function from an arbitrary type A to another arbitrary type B, we use the Magic type over raw pointers. This is because we can pull a valid raw pointer value out of thin air, for all types B, namely the null pointer. With the null pointer, we create a Magic value of the B alternative, in the magic variable. We create an alias for its Uncopyable object in the uncopied pointer. Then we overwrite the magic object with an A alternative containing a pointer of type *const A, to the function argument a. In the unsafe block, we read that pointer as a value of type *const B, and return it.

fn magic<A, B>(a: A) -> B {
  let aptr : *const A = &a;
  let bptr : *const B = std::ptr::null();
  let mut magic =
    Magic::B::<*const A, *const B>
      (Uncopyable{value: bptr});
  let uncopied : *const Uncopyable<*const B> =
    match magic {
      Magic::B(b) => &b,
      _ => panic!("no match 1")
    };
  magic = Magic::A::<*const A, *const B>
    (Uncopyable{value: aptr});
  unsafe {
    assert!((*uncopied).value != std::ptr::null());
    return std::ptr::read((*uncopied).value);
  }
}

This function can be tested like this (based on the example for std::mem::transmute:

fn main() {
  let vec : &[u8] = magic("magic string");
  for b in vec {
    println!("{:3} {}",
      b, std::char::from_u32(*b as u32).unwrap());
  }
}

The expected output, when run, is:

109 m
 97 a
103 g
105 i
 99 c
 32  
115 s
116 t
114 r
105 i
110 n
103 g

This will continue to work as long as the representation of u8 vectors and strings remains the same and the Rust or LLVM optimizers do not get confused.

While I am not entirely sure what happens under the covers (it is a bit odd that the Uncopyable type is required to inhibit the copy out of the enum value), I do believe this example shows that a borrow checker bypass which leads to undetected aliasing can be abused to write a magic conversion function, therefore proving that type safety can be bypassed completely.

Revisions


Florian Weimer
Home Blog (DE) Blog (EN) Impressum RSS Feeds