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...@@ -3095,7 +3095,6 @@ fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
3095 const zcu = pt.zcu;3095 const zcu = pt.zcu;
3096 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;3096 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
3097 const src = block.nodeOffset(inst_data.src_node);3097 const src = block.nodeOffset(inst_data.src_node);
3098 const ptr_src = block.src(.{ .node_offset_deref_ptr = inst_data.src_node });
3099 const operand = sema.resolveInst(inst_data.operand);3098 const operand = sema.resolveInst(inst_data.operand);
3100 const operand_ty = sema.typeOf(operand);3099 const operand_ty = sema.typeOf(operand);
31013100
...@@ -3104,15 +3103,26 @@ fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai...@@ -3104,15 +3103,26 @@ fn zirRefDeref(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
3104 const ptr_info = operand_ty.ptrInfo(zcu);3103 const ptr_info = operand_ty.ptrInfo(zcu);
3105 return switch (ptr_info.flags.size) {3104 return switch (ptr_info.flags.size) {
3106 .many, .slice => unreachable, // cannot be dereferenced3105 .many, .slice => unreachable, // cannot be dereferenced
3107 .c => ptr: {3106 .c => single_ptr: {
3108 var single_ptr_flags = ptr_info.flags;3107 const single_ptr_ty = try pt.ptrType(p: {
3109 single_ptr_flags.size = .one;3108 var p = ptr_info;
3110 single_ptr_flags.is_allowzero = false;3109 p.flags.size = .one;
3111 const single_ptr_ty = try pt.ptrType(.{3110 p.flags.is_allowzero = false;
3112 .child = ptr_info.child,3111 break :p p;
3113 .flags = single_ptr_flags,
3114 });3112 });
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;
3116 },3126 },
3117 .one => operand,3127 .one => operand,
3118 };3128 };
...@@ -30615,6 +30625,11 @@ fn analyzeLoad(...@@ -30615,6 +30625,11 @@ fn analyzeLoad(
30615 break :msg msg;30625 break :msg msg;
30616 });30626 });
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 }
30618 return block.addTyOp(.load, elem_ty, ptr);30633 return block.addTyOp(.load, elem_ty, ptr);
30619}30634}
3062030635
test/behavior/sizeof_and_typeof.zig+5
...@@ -423,3 +423,8 @@ test "@sizeOf struct is resolved when used as operand of slicing" {...@@ -423,3 +423,8 @@ test "@sizeOf struct is resolved when used as operand of slicing" {
423 S.buf[@sizeOf(dummy)..][0] = 0;423 S.buf[@sizeOf(dummy)..][0] = 0;
424 try expect(S.buf[0] == 0);424 try expect(S.buf[0] == 0);
425}425}
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