authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-06-19 16:16:17+02:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-06-20 09:45:38+02:00
log36069a2a7dbd1a3b38ecfced917733127d7dbb38
treedd04c9ab162f41eaafe880a65516407471463393
parent3a48f4bb40d219efc9b96df44850bcedf83f6285

Sema: make dereferences of comptime-known null C pointers runtime-known

This allows `@TypeOf(@as([*c]T, null).*.x)` to (continue to) work. In the long term these are probably not the semantics we want, unwrapping a comptime-known `null` pointer should always result in `unreachable`. Also adds missing runtime safety checks for loading null C pointers.

5 files changed, 63 insertions(+), 17 deletions(-)

src/Sema.zig+24-9
......@@ -3095,7 +3095,6 @@ fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
30953095 const zcu = pt.zcu;
30963096 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
30973097 const src = block.nodeOffset(inst_data.src_node);
3098 const ptr_src = block.src(.{ .node_offset_deref_ptr = inst_data.src_node });
30993098 const operand = sema.resolveInst(inst_data.operand);
31003099 const operand_ty = sema.typeOf(operand);
31013100
......@@ -3104,15 +3103,26 @@ fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
31043103 const ptr_info = operand_ty.ptrInfo(zcu);
31053104 return switch (ptr_info.flags.size) {
31063105 .many, .slice => unreachable, // cannot be dereferenced
3107 .c => ptr: {
3108 var single_ptr_flags = ptr_info.flags;
3109 single_ptr_flags.size = .one;
3110 single_ptr_flags.is_allowzero = false;
3111 const single_ptr_ty = try pt.ptrType(.{
3112 .child = ptr_info.child,
3113 .flags = single_ptr_flags,
3106 .c => single_ptr: {
3107 const single_ptr_ty = try pt.ptrType(p: {
3108 var p = ptr_info;
3109 p.flags.size = .one;
3110 p.flags.is_allowzero = false;
3111 break :p p;
31143112 });
3115 break :ptr try sema.coerceCompatiblePtrs(block, single_ptr_ty, operand, ptr_src);
3113 // https://github.com/ziglang/zig/issues/6597
3114 if (sema.resolveValue(operand)) |operand_val| {
3115 if (!operand_val.isNull(zcu)) {
3116 break :single_ptr try sema.coerceInMemory(operand_val, single_ptr_ty);
3117 }
3118 }
3119 if (block.wantSafety()) {
3120 const is_non_null = try block.addUnOp(.is_non_null, operand);
3121 try sema.addSafetyCheck(block, src, is_non_null, .unwrap_null);
3122 }
3123 const single_ptr = try block.addBitCast(single_ptr_ty, operand);
3124 try sema.checkKnownAllocPtr(block, operand, single_ptr);
3125 break :single_ptr single_ptr;
31163126 },
31173127 .one => operand,
31183128 };
......@@ -30615,6 +30625,11 @@ fn analyzeLoad(
3061530625 break :msg msg;
3061630626 });
3061730627
30628 // https://github.com/ziglang/zig/issues/6597
30629 if (block.wantSafety() and ptr_ty.isCPtr(zcu)) {
30630 const is_non_null = try block.addUnOp(.is_non_null, ptr);
30631 try sema.addSafetyCheck(block, src, is_non_null, .unwrap_null);
30632 }
3061830633 return block.addTyOp(.load, elem_ty, ptr);
3061930634}
3062030635
test/behavior/sizeof_and_typeof.zig+5
......@@ -423,3 +423,8 @@ test "@sizeOf struct is resolved when used as operand of slicing" {
423423 S.buf[@sizeOf(dummy)..][0] = 0;
424424 try expect(S.buf[0] == 0);
425425}
426
427test "@TypeOf null C pointer dereference" {
428 comptime assert(@TypeOf(@as([*c]u8, null).*) == u8);
429 comptime assert(@TypeOf(&@as([*c]u8, null).*) == *u8);
430}
test/cases/compile_errors/ref_deref_of_null_c_ptr.zig deleted-8
......@@ -1,8 +0,0 @@
1export fn entry() void {
2 const ptr: [*c]u8 = null;
3 _ = &ptr.*;
4}
5
6// error
7//
8// :3:10: error: null pointer casted to type '*u8'
test/cases/safety/deref_null_c_pointer.zig created+17
......@@ -0,0 +1,17 @@
1const std = @import("std");
2
3pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
4 _ = stack_trace;
5 if (std.mem.eql(u8, message, "attempt to use null value")) {
6 std.process.exit(0);
7 }
8 std.process.exit(1);
9}
10pub fn main() !void {
11 const p: [*c]u8 = null;
12 _ = p.*;
13}
14
15// run
16// backend=selfhosted,llvm
17// target=x86_64-linux,aarch64-linux
test/cases/safety/ref_deref_null_c_pointer.zig created+17
......@@ -0,0 +1,17 @@
1const std = @import("std");
2
3pub fn panic(message: []const u8, stack_trace: ?*std.builtin.StackTrace, _: ?usize) noreturn {
4 _ = stack_trace;
5 if (std.mem.eql(u8, message, "attempt to use null value")) {
6 std.process.exit(0);
7 }
8 std.process.exit(1);
9}
10pub fn main() !void {
11 const p: [*c]u8 = null;
12 _ = &p.*;
13}
14
15// run
16// backend=selfhosted,llvm
17// target=x86_64-linux,aarch64-linux