Andrew Kelley

Become a patron


programming language

Unsafe Zig is Safer than Unsafe Rust

2018 January 24

Consider the following Rust code:

struct Foo {
    a: i32,
    b: i32,
}

fn main() {
    unsafe {
        let mut array: [u8; 1024] = [1; 1024];
        let foo = std::mem::transmute::<&mut u8, &mut Foo>(&mut array[0]);
        foo.a += 1;
    }
}

This pattern is pretty common if you are interacting with Operating System APIs. Another example.

Can you spot the problem with the code?

It's pretty subtle, but there is actually undefined behavior going on here. Let's take a look at the LLVM IR:

define internal void @_ZN4test4main17h916a53db53ad90a1E() unnamed_addr #0 {
start:
  %transmute_temp = alloca %Foo*
  %array = alloca [1024 x i8]
  %0 = getelementptr inbounds [1024 x i8], [1024 x i8]* %array, i32 0, i32 0
  call void @llvm.memset.p0i8.i64(i8* %0, i8 1, i64 1024, i32 1, i1 false)
  br label %bb1

bb1:                                              ; preds = %start
  %1 = getelementptr inbounds [1024 x i8], [1024 x i8]* %array, i64 0, i64 0
  %2 = bitcast %Foo** %transmute_temp to i8**
  store i8* %1, i8** %2, align 8
  %3 = load %Foo*, %Foo** %transmute_temp, !nonnull !1
  br label %bb2

bb2:                                              ; preds = %bb1
  %4 = getelementptr inbounds %Foo, %Foo* %3, i32 0, i32 0
  %5 = load i32, i32* %4
  %6 = call { i32, i1 } @llvm.sadd.with.overflow.i32(i32 %5, i32 1)
  %7 = extractvalue { i32, i1 } %6, 0
  %8 = extractvalue { i32, i1 } %6, 1
  %9 = call i1 @llvm.expect.i1(i1 %8, i1 false)
  br i1 %9, label %panic, label %bb3

bb3:                                              ; preds = %bb2
  %10 = getelementptr inbounds %Foo, %Foo* %3, i32 0, i32 0
  store i32 %7, i32* %10
  ret void

panic:                                            ; preds = %bb2
; call core::panicking::panic
  call void @_ZN4core9panicking5panic17hfecc01813e436969E({ %str_slice, [0 x i8], %str_slice, [0 x i8], i32, [0 x i8], i32, [0 x i8] }* noalias readonly dereferenceable(40) bitcast ({ %str_slice, %str_slice, i32, i32 }* @panic_loc.2 to { %str_slice, [0 x i8], %str_slice, [0 x i8], i32, [0 x i8], i32, [0 x i8] }*))
  unreachable
}

That's the code for the main function. This is using rustc version 1.21.0. Let's zoom in on the problematic parts:

  %array = alloca [1024 x i8]
  ; loading foo.a in order to do + 1
  %5 = load i32, i32* %4
  ; storing the result of + 1 into foo.a
  store i32 %7, i32* %10

None of these alloca, load, or store instructions have alignment attributes on them, so they use the ABI alignment of the respective types.

That means the i8 array gets alignment of 1, since the ABI alignment of i8 is 1, and the load and store instructions get alignment 4, since the ABI alignment of i32 is 4. This is undefined behavior:

It's a nasty bug, because besides being an easy mistake to make, on some architectures it will only cause mysterious slowness, while on others it can cause an illegal instruction exception on the CPU. Regardless, it's undefined behavior, and we are professionals, and so we do not accept undefined behavior.

Let's try writing the equivalent code in Zig:

const Foo = struct {
    a: i32,
    b: i32,
};

pub fn main() {
    var array = []u8{1} ** 1024;
    const foo = @ptrCast(&Foo, &array[0]);
    foo.a += 1;
}

And now we compile it:

/home/andy/tmp/test.zig:8:17: error: cast increases pointer alignment
    const foo = @ptrCast(&Foo, &array[0]);
                ^
/home/andy/tmp/test.zig:8:38: note: '&u8' has alignment 1
    const foo = @ptrCast(&Foo, &array[0]);
                                     ^
/home/andy/tmp/test.zig:8:27: note: '&Foo' has alignment 4
    const foo = @ptrCast(&Foo, &array[0]);
                          ^

Zig knows not to compile this code. Here's how to fix it:

@@ -4,7 +4,7 @@
 };
 
 pub fn main() {
-    var array = []u8{1} ** 1024;
+    var array align(@alignOf(Foo)) = []u8{1} ** 1024;
     const foo = @ptrCast(&Foo, &array[0]);
     foo.a += 1;
 }

Now it compiles fine. Let's have a look at the LLVM IR:

define internal fastcc void @main() unnamed_addr #0 !dbg !8911 {
Entry:
  %array = alloca [1024 x i8], align 4
  %foo = alloca %Foo*, align 8
  %0 = bitcast [1024 x i8]* %array to i8*, !dbg !8923
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* %0, i8* getelementptr inbounds ([1024 x i8], [1024 x i8]* @266, i32 0, i32 0), i64 1024, i32 4, i1 false), !dbg !8923
  call void @llvm.dbg.declare(metadata [1024 x i8]* %array, metadata !8914, metadata !529), !dbg !8923
  %1 = getelementptr inbounds [1024 x i8], [1024 x i8]* %array, i64 0, i64 0, !dbg !8924
  %2 = bitcast i8* %1 to %Foo*, !dbg !8925
  store %Foo* %2, %Foo** %foo, align 8, !dbg !8926
  call void @llvm.dbg.declare(metadata %Foo** %foo, metadata !8916, metadata !529), !dbg !8926
  %3 = load %Foo*, %Foo** %foo, align 8, !dbg !8927
  %4 = getelementptr inbounds %Foo, %Foo* %3, i32 0, i32 0, !dbg !8927
  %5 = load i32, i32* %4, align 4, !dbg !8927
  %6 = call { i32, i1 } @llvm.sadd.with.overflow.i32(i32 %5, i32 1), !dbg !8929
  %7 = extractvalue { i32, i1 } %6, 0, !dbg !8929
  %8 = extractvalue { i32, i1 } %6, 1, !dbg !8929
  br i1 %8, label %OverflowFail, label %OverflowOk, !dbg !8929

OverflowFail:                                     ; preds = %Entry
  tail call fastcc void @panic(%"[]u8"* @88, %StackTrace* null), !dbg !8929
  unreachable, !dbg !8929

OverflowOk:                                       ; preds = %Entry
  store i32 %7, i32* %4, align 4, !dbg !8929
  ret void, !dbg !8930
}

Zooming in on the relevant parts:

  %array = alloca [1024 x i8], align 4
  %5 = load i32, i32* %4, align 4, !dbg !8927
  store i32 %7, i32* %4, align 4, !dbg !8929

Notice that the alloca, load, and store all agree on the alignment.

In Zig the problem of alignment is solved completely; the compiler catches all possible alignment issues. In the situation where you need to assert to the compiler that something is more aligned than Zig thinks it is, you can use @alignCast. This inserts a cheap safety check in debug mode to make sure the alignment assertion is correct.

Thanks for reading. If you would like to support the development of Zig, please consider pledging $1/month on Patreon.