Slice operation permits creation of invalid pointers at compile time
Zig Version
0.13.0-dev.46+3648d7df1
Steps to Reproduce and Observed Behaviour
Compile example program with zig build-obj demo_various_unusable_results.zig
demo_various_unusable_results.zig:
const T = extern struct {
a: usize = 1,
b: u32 = 0,
c: [4]u16 = .{ 2, 3, 4, 5 },
};
const S = extern struct {
a: usize = 1,
b: T = .{},
c: [4]u8 = .{ 2, 3, 4, 5 },
};
var mem1: [2]S = .{ .{}, .{} };
const mem2: [2]S = .{ .{}, .{} };
comptime {
const ptr1: [*]usize = @ptrCast(&mem1);
const len1: usize = (2 * @sizeOf(S)) / @sizeOf(usize);
const slice1: []usize = ptr1[0..len1];
_ = ptr1[slice1.len + 1 ..];
}
comptime {
const ptr1: [*]const usize = @ptrCast(&mem2);
const ptr2: [*]const u32 = @ptrCast(ptr1[2..]);
const len2: usize = ((2 * @sizeOf(S)) - 2 * @sizeOf(usize)) / @sizeOf(u32);
const slice2: []const u32 = ptr2[0..len2];
_ = ptr2[slice2.len + 1 ..];
}
comptime {
var mem3: [2]S = .{ .{}, .{} };
const ptr1: [*]usize = @ptrCast(&mem3);
const ptr2: [*]u32 = @ptrCast(ptr1[2..]);
const ptr3: [*]u8 = @ptrCast(ptr2[1..]);
const len3: usize = (((2 * @sizeOf(S)) - 2 * @sizeOf(usize)) - @sizeOf(u32)) / @sizeOf(u8);
const slice3: []u8 = ptr3[0..len3];
_ = ptr3[slice3.len + 1 ..];
}
comptime {
const mem4: [2]S = .{ .{}, .{} };
const ptr4: [*]const u16 = @ptrCast(&mem4[0].b.c[2]);
const len4: usize = ((2 * @sizeOf(S)) - (@offsetOf(S, "b") + @offsetOf(T, "c") + (2 * @sizeOf(u16)))) / @sizeOf(u16);
const slice4: []const u16 = ptr4[0..len4];
_ = ptr4[slice4.len + 1 ..];
}
comptime {
var mem5: comptime_int = 0;
const ptr5: [*]comptime_int = @ptrCast(&mem5);
const slice5: []comptime_int = ptr5[0..1];
_ = ptr5[slice5.len + 1 ..];
}
comptime {
var mem6: comptime_int = 0;
const ptr6: [*]type = @ptrCast(&mem6);
_ = ptr6[0..1];
}
Output:
zig build-obj demo_various_unusable_results.zig
Each of the six discarded expressions are invalid. The first five are out of bounds, while the sixth is an impossible reinterpretation. The program compiles normally.
The primary issue with allowing the creation of invalid references at compile time is that they are indistinguishable from valid references at runtime by runtime safety checks.
Compile and run example program with zig run ub_subsequent_runtime_use.zig
ub_subsequent_runtime_use.zig:
var data: [32]u8 = .{'a'} ** 32;
pub fn main() void {
const ptr1: [*]u8 = data[0..32];
// Slice of 32 bytes above the actual declaration at runtime.
const ptr2 = ptr1[32..64];
// All subsequent runtime safety checks are based on incorrect information.
const less: []const u8 = ptr2[0..16];
// The random memory will probably be usable.
@import("std").debug.print("{s}\n", .{less[0..16] ++ ptr2[16..]});
}
Output:
zig run ub_subsequent_runtime_use.zig
thread {} panic: invalid error
At this point the state of the program is completely undefined and the best outcome is SIGSEGV.
Expected Behaviour
Slicing should not permit the creation of slices which are known to be invalid at compile time.