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:

the store has undefined behavior if the alignment is not set to a value which is at least the size in bytes of the pointee the load has undefined behavior if the alignment is not set to a value which is at least the size in bytes of the pointee

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 $5/month.