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.
2015-03-22: published
2019-03-30: Fix some formatting errors.