master
  1const std = @import("std");
  2const builtin = @import("builtin");
  3const assert = std.debug.assert;
  4const panic = std.debug.panicExtra;
  5
  6const SourceLocation = extern struct {
  7    file_name: ?[*:0]const u8,
  8    line: u32,
  9    col: u32,
 10};
 11
 12const TypeDescriptor = extern struct {
 13    kind: Kind,
 14    info: Info,
 15    // name: [?:0]u8
 16
 17    const Kind = enum(u16) {
 18        integer = 0x0000,
 19        float = 0x0001,
 20        unknown = 0xFFFF,
 21    };
 22
 23    const Info = extern union {
 24        integer: packed struct(u16) {
 25            signed: bool,
 26            bit_width: u15,
 27        },
 28        float: u16,
 29    };
 30
 31    fn getIntegerSize(desc: TypeDescriptor) u64 {
 32        assert(desc.kind == .integer);
 33        const bit_width = desc.info.integer.bit_width;
 34        return @as(u64, 1) << @intCast(bit_width);
 35    }
 36
 37    fn isSigned(desc: TypeDescriptor) bool {
 38        return desc.kind == .integer and desc.info.integer.signed;
 39    }
 40
 41    fn getName(desc: *const TypeDescriptor) [:0]const u8 {
 42        return std.mem.span(@as([*:0]const u8, @ptrCast(desc)) + @sizeOf(TypeDescriptor));
 43    }
 44};
 45
 46const ValueHandle = *const opaque {};
 47
 48const Value = extern struct {
 49    td: *const TypeDescriptor,
 50    handle: ValueHandle,
 51
 52    fn getUnsignedInteger(value: Value) u128 {
 53        assert(!value.td.isSigned());
 54        const size = value.td.getIntegerSize();
 55        const max_inline_size = @bitSizeOf(ValueHandle);
 56        if (size <= max_inline_size) {
 57            return @intFromPtr(value.handle);
 58        }
 59
 60        return switch (size) {
 61            64 => @as(*const u64, @ptrCast(@alignCast(value.handle))).*,
 62            128 => @as(*const u128, @ptrCast(@alignCast(value.handle))).*,
 63            else => @trap(),
 64        };
 65    }
 66
 67    fn getSignedInteger(value: Value) i128 {
 68        assert(value.td.isSigned());
 69        const size = value.td.getIntegerSize();
 70        const max_inline_size = @bitSizeOf(ValueHandle);
 71        if (size <= max_inline_size) {
 72            const extra_bits: std.math.Log2Int(usize) = @intCast(max_inline_size - size);
 73            const handle: isize = @bitCast(@intFromPtr(value.handle));
 74            return (handle << extra_bits) >> extra_bits;
 75        }
 76        return switch (size) {
 77            64 => @as(*const i64, @ptrCast(@alignCast(value.handle))).*,
 78            128 => @as(*const i128, @ptrCast(@alignCast(value.handle))).*,
 79            else => @trap(),
 80        };
 81    }
 82
 83    fn getFloat(value: Value) f128 {
 84        assert(value.td.kind == .float);
 85        const size = value.td.info.float;
 86        const max_inline_size = @bitSizeOf(ValueHandle);
 87        if (size <= max_inline_size) {
 88            return @as(switch (@bitSizeOf(usize)) {
 89                32 => f32,
 90                64 => f64,
 91                else => @compileError("unsupported target"),
 92            }, @bitCast(@intFromPtr(value.handle)));
 93        }
 94        return @floatCast(switch (size) {
 95            64 => @as(*const f64, @ptrCast(@alignCast(value.handle))).*,
 96            80 => @as(*const f80, @ptrCast(@alignCast(value.handle))).*,
 97            128 => @as(*const f128, @ptrCast(@alignCast(value.handle))).*,
 98            else => @trap(),
 99        });
100    }
101
102    fn isMinusOne(value: Value) bool {
103        return value.td.isSigned() and
104            value.getSignedInteger() == -1;
105    }
106
107    fn isNegative(value: Value) bool {
108        return value.td.isSigned() and
109            value.getSignedInteger() < 0;
110    }
111
112    fn getPositiveInteger(value: Value) u128 {
113        if (value.td.isSigned()) {
114            const signed = value.getSignedInteger();
115            assert(signed >= 0);
116            return @intCast(signed);
117        } else {
118            return value.getUnsignedInteger();
119        }
120    }
121
122    pub fn format(value: Value, writer: *std.Io.Writer) std.Io.Writer.Error!void {
123        switch (value.td.kind) {
124            .integer => {
125                if (value.td.isSigned()) {
126                    try writer.print("{d}", .{value.getSignedInteger()});
127                } else {
128                    try writer.print("{d}", .{value.getUnsignedInteger()});
129                }
130            },
131            .float => try writer.print("{d}", .{value.getFloat()}),
132            .unknown => try writer.writeAll("(unknown)"),
133        }
134    }
135};
136
137const OverflowData = extern struct {
138    loc: SourceLocation,
139    td: *const TypeDescriptor,
140};
141
142fn overflowHandler(
143    comptime sym_name: []const u8,
144    comptime operator: []const u8,
145) void {
146    const S = struct {
147        fn abort(
148            data: *const OverflowData,
149            lhs_handle: ValueHandle,
150            rhs_handle: ValueHandle,
151        ) callconv(.c) noreturn {
152            handler(data, lhs_handle, rhs_handle);
153        }
154
155        fn handler(
156            data: *const OverflowData,
157            lhs_handle: ValueHandle,
158            rhs_handle: ValueHandle,
159        ) callconv(.c) noreturn {
160            const lhs: Value = .{ .handle = lhs_handle, .td = data.td };
161            const rhs: Value = .{ .handle = rhs_handle, .td = data.td };
162            const signed_str = if (data.td.isSigned()) "signed" else "unsigned";
163            panic(
164                @returnAddress(),
165                "{s} integer overflow: {f} " ++ operator ++ " {f} cannot be represented in type {s}",
166                .{ signed_str, lhs, rhs, data.td.getName() },
167            );
168        }
169    };
170
171    exportHandlerWithAbort(&S.handler, &S.abort, sym_name);
172}
173
174fn negationHandlerAbort(
175    data: *const OverflowData,
176    value_handle: ValueHandle,
177) callconv(.c) noreturn {
178    negationHandler(data, value_handle);
179}
180
181fn negationHandler(
182    data: *const OverflowData,
183    value_handle: ValueHandle,
184) callconv(.c) noreturn {
185    const value: Value = .{ .handle = value_handle, .td = data.td };
186    panic(@returnAddress(), "negation of {f} cannot be represented in type {s}", .{
187        value, data.td.getName(),
188    });
189}
190
191fn divRemHandlerAbort(
192    data: *const OverflowData,
193    lhs_handle: ValueHandle,
194    rhs_handle: ValueHandle,
195) callconv(.c) noreturn {
196    divRemHandler(data, lhs_handle, rhs_handle);
197}
198
199fn divRemHandler(
200    data: *const OverflowData,
201    lhs_handle: ValueHandle,
202    rhs_handle: ValueHandle,
203) callconv(.c) noreturn {
204    const lhs: Value = .{ .handle = lhs_handle, .td = data.td };
205    const rhs: Value = .{ .handle = rhs_handle, .td = data.td };
206
207    if (rhs.isMinusOne()) {
208        panic(@returnAddress(), "division of {f} by -1 cannot be represented in type {s}", .{
209            lhs, data.td.getName(),
210        });
211    } else panic(@returnAddress(), "division by zero", .{});
212}
213
214const AlignmentAssumptionData = extern struct {
215    loc: SourceLocation,
216    assumption_loc: SourceLocation,
217    td: *const TypeDescriptor,
218};
219
220fn alignmentAssumptionHandlerAbort(
221    data: *const AlignmentAssumptionData,
222    pointer: ValueHandle,
223    alignment_handle: ValueHandle,
224    maybe_offset: ?ValueHandle,
225) callconv(.c) noreturn {
226    alignmentAssumptionHandler(
227        data,
228        pointer,
229        alignment_handle,
230        maybe_offset,
231    );
232}
233
234fn alignmentAssumptionHandler(
235    data: *const AlignmentAssumptionData,
236    pointer: ValueHandle,
237    alignment_handle: ValueHandle,
238    maybe_offset: ?ValueHandle,
239) callconv(.c) noreturn {
240    const real_pointer = @intFromPtr(pointer) - @intFromPtr(maybe_offset);
241    const lsb = @ctz(real_pointer);
242    const actual_alignment = @as(u64, 1) << @intCast(lsb);
243    const mask = @intFromPtr(alignment_handle) - 1;
244    const misalignment_offset = real_pointer & mask;
245    const alignment: Value = .{ .handle = alignment_handle, .td = data.td };
246
247    if (maybe_offset) |offset| {
248        panic(
249            @returnAddress(),
250            "assumption of {f} byte alignment (with offset of {d} byte) for pointer of type {s} failed\n" ++
251                "offset address is {d} aligned, misalignment offset is {d} bytes",
252            .{
253                alignment,
254                @intFromPtr(offset),
255                data.td.getName(),
256                actual_alignment,
257                misalignment_offset,
258            },
259        );
260    } else {
261        panic(
262            @returnAddress(),
263            "assumption of {f} byte alignment for pointer of type {s} failed\n" ++
264                "address is {d} aligned, misalignment offset is {d} bytes",
265            .{
266                alignment,
267                data.td.getName(),
268                actual_alignment,
269                misalignment_offset,
270            },
271        );
272    }
273}
274
275const ShiftOobData = extern struct {
276    loc: SourceLocation,
277    lhs_type: *const TypeDescriptor,
278    rhs_type: *const TypeDescriptor,
279};
280
281fn shiftOobAbort(
282    data: *const ShiftOobData,
283    lhs_handle: ValueHandle,
284    rhs_handle: ValueHandle,
285) callconv(.c) noreturn {
286    shiftOob(data, lhs_handle, rhs_handle);
287}
288
289fn shiftOob(
290    data: *const ShiftOobData,
291    lhs_handle: ValueHandle,
292    rhs_handle: ValueHandle,
293) callconv(.c) noreturn {
294    const lhs: Value = .{ .handle = lhs_handle, .td = data.lhs_type };
295    const rhs: Value = .{ .handle = rhs_handle, .td = data.rhs_type };
296
297    if (rhs.isNegative() or
298        rhs.getPositiveInteger() >= data.lhs_type.getIntegerSize())
299    {
300        if (rhs.isNegative()) {
301            panic(@returnAddress(), "shift exponent {f} is negative", .{rhs});
302        } else {
303            panic(
304                @returnAddress(),
305                "shift exponent {f} is too large for {d}-bit type {s}",
306                .{ rhs, data.lhs_type.getIntegerSize(), data.lhs_type.getName() },
307            );
308        }
309    } else {
310        if (lhs.isNegative()) {
311            panic(@returnAddress(), "left shift of negative value {f}", .{lhs});
312        } else {
313            panic(
314                @returnAddress(),
315                "left shift of {f} by {f} places cannot be represented in type {s}",
316                .{ lhs, rhs, data.lhs_type.getName() },
317            );
318        }
319    }
320}
321
322const OutOfBoundsData = extern struct {
323    loc: SourceLocation,
324    array_type: *const TypeDescriptor,
325    index_type: *const TypeDescriptor,
326};
327
328fn outOfBoundsAbort(
329    data: *const OutOfBoundsData,
330    index_handle: ValueHandle,
331) callconv(.c) noreturn {
332    outOfBounds(data, index_handle);
333}
334
335fn outOfBounds(
336    data: *const OutOfBoundsData,
337    index_handle: ValueHandle,
338) callconv(.c) noreturn {
339    const index: Value = .{ .handle = index_handle, .td = data.index_type };
340    panic(@returnAddress(), "index {f} out of bounds for type {s}", .{
341        index,
342        data.array_type.getName(),
343    });
344}
345
346const PointerOverflowData = extern struct {
347    loc: SourceLocation,
348};
349
350fn pointerOverflowAbort(
351    data: *const PointerOverflowData,
352    base: usize,
353    result: usize,
354) callconv(.c) noreturn {
355    pointerOverflow(data, base, result);
356}
357
358fn pointerOverflow(
359    _: *const PointerOverflowData,
360    base: usize,
361    result: usize,
362) callconv(.c) noreturn {
363    if (base == 0) {
364        if (result == 0) {
365            panic(@returnAddress(), "applying zero offset to null pointer", .{});
366        } else {
367            panic(@returnAddress(), "applying non-zero offset {d} to null pointer", .{result});
368        }
369    } else {
370        if (result == 0) {
371            panic(
372                @returnAddress(),
373                "applying non-zero offset to non-null pointer 0x{x} produced null pointer",
374                .{base},
375            );
376        } else {
377            const signed_base: isize = @bitCast(base);
378            const signed_result: isize = @bitCast(result);
379            if ((signed_base >= 0) == (signed_result >= 0)) {
380                if (base > result) {
381                    panic(
382                        @returnAddress(),
383                        "addition of unsigned offset to 0x{x} overflowed to 0x{x}",
384                        .{ base, result },
385                    );
386                } else {
387                    panic(
388                        @returnAddress(),
389                        "subtraction of unsigned offset to 0x{x} overflowed to 0x{x}",
390                        .{ base, result },
391                    );
392                }
393            } else {
394                panic(
395                    @returnAddress(),
396                    "pointer index expression with base 0x{x} overflowed to 0x{x}",
397                    .{ base, result },
398                );
399            }
400        }
401    }
402}
403
404const TypeMismatchData = extern struct {
405    loc: SourceLocation,
406    td: *const TypeDescriptor,
407    log_alignment: u8,
408    kind: enum(u8) {
409        load,
410        store,
411        reference_binding,
412        member_access,
413        member_call,
414        constructor_call,
415        downcast_pointer,
416        downcast_reference,
417        upcast,
418        upcast_to_virtual_base,
419        nonnull_assign,
420        dynamic_operation,
421
422        fn getName(kind: @This()) []const u8 {
423            return switch (kind) {
424                .load => "load of",
425                .store => "store of",
426                .reference_binding => "reference binding to",
427                .member_access => "member access within",
428                .member_call => "member call on",
429                .constructor_call => "constructor call on",
430                .downcast_pointer, .downcast_reference => "downcast of",
431                .upcast => "upcast of",
432                .upcast_to_virtual_base => "cast to virtual base of",
433                .nonnull_assign => "_Nonnull binding to",
434                .dynamic_operation => "dynamic operation on",
435            };
436        }
437    },
438};
439
440fn typeMismatchAbort(
441    data: *const TypeMismatchData,
442    pointer: ?ValueHandle,
443) callconv(.c) noreturn {
444    typeMismatch(data, pointer);
445}
446
447fn typeMismatch(
448    data: *const TypeMismatchData,
449    pointer: ?ValueHandle,
450) callconv(.c) noreturn {
451    const alignment = @as(usize, 1) << @intCast(data.log_alignment);
452    const handle: usize = @intFromPtr(pointer);
453
454    if (pointer == null) {
455        panic(
456            @returnAddress(),
457            "{s} null pointer of type {s}",
458            .{ data.kind.getName(), data.td.getName() },
459        );
460    } else if (!std.mem.isAligned(handle, alignment)) {
461        panic(
462            @returnAddress(),
463            "{s} misaligned address 0x{x} for type {s}, which requires {d} byte alignment",
464            .{ data.kind.getName(), handle, data.td.getName(), alignment },
465        );
466    } else {
467        panic(
468            @returnAddress(),
469            "{s} address 0x{x} with insufficient space for an object of type {s}",
470            .{ data.kind.getName(), handle, data.td.getName() },
471        );
472    }
473}
474
475const UnreachableData = extern struct {
476    loc: SourceLocation,
477};
478
479fn builtinUnreachable(_: *const UnreachableData) callconv(.c) noreturn {
480    panic(@returnAddress(), "execution reached an unreachable program point", .{});
481}
482
483fn missingReturn(_: *const UnreachableData) callconv(.c) noreturn {
484    panic(@returnAddress(), "execution reached the end of a value-returning function without returning a value", .{});
485}
486
487const NonNullReturnData = extern struct {
488    attribute_loc: SourceLocation,
489};
490
491fn nonNullReturnAbort(data: *const NonNullReturnData) callconv(.c) noreturn {
492    nonNullReturn(data);
493}
494fn nonNullReturn(_: *const NonNullReturnData) callconv(.c) noreturn {
495    panic(@returnAddress(), "null pointer returned from function declared to never return null", .{});
496}
497
498const NonNullArgData = extern struct {
499    loc: SourceLocation,
500    attribute_loc: SourceLocation,
501    arg_index: i32,
502};
503
504fn nonNullArgAbort(data: *const NonNullArgData) callconv(.c) noreturn {
505    nonNullArg(data);
506}
507
508fn nonNullArg(data: *const NonNullArgData) callconv(.c) noreturn {
509    panic(
510        @returnAddress(),
511        "null pointer passed as argument {d}, which is declared to never be null",
512        .{data.arg_index},
513    );
514}
515
516const InvalidValueData = extern struct {
517    loc: SourceLocation,
518    td: *const TypeDescriptor,
519};
520
521fn loadInvalidValueAbort(
522    data: *const InvalidValueData,
523    value_handle: ValueHandle,
524) callconv(.c) noreturn {
525    loadInvalidValue(data, value_handle);
526}
527
528fn loadInvalidValue(
529    data: *const InvalidValueData,
530    value_handle: ValueHandle,
531) callconv(.c) noreturn {
532    const value: Value = .{ .handle = value_handle, .td = data.td };
533    panic(@returnAddress(), "load of value {f}, which is not valid for type {s}", .{
534        value, data.td.getName(),
535    });
536}
537
538const InvalidBuiltinData = extern struct {
539    loc: SourceLocation,
540    kind: enum(u8) {
541        ctz,
542        clz,
543    },
544};
545fn invalidBuiltinAbort(data: *const InvalidBuiltinData) callconv(.c) noreturn {
546    invalidBuiltin(data);
547}
548
549fn invalidBuiltin(data: *const InvalidBuiltinData) callconv(.c) noreturn {
550    panic(
551        @returnAddress(),
552        "passing zero to {s}(), which is not a valid argument",
553        .{@tagName(data.kind)},
554    );
555}
556
557const VlaBoundNotPositive = extern struct {
558    loc: SourceLocation,
559    td: *const TypeDescriptor,
560};
561
562fn vlaBoundNotPositiveAbort(
563    data: *const VlaBoundNotPositive,
564    bound_handle: ValueHandle,
565) callconv(.c) noreturn {
566    vlaBoundNotPositive(data, bound_handle);
567}
568
569fn vlaBoundNotPositive(
570    data: *const VlaBoundNotPositive,
571    bound_handle: ValueHandle,
572) callconv(.c) noreturn {
573    const bound: Value = .{ .handle = bound_handle, .td = data.td };
574    panic(@returnAddress(), "variable length array bound evaluates to non-positive value {f}", .{bound});
575}
576
577const FloatCastOverflowData = extern struct {
578    from: *const TypeDescriptor,
579    to: *const TypeDescriptor,
580};
581
582const FloatCastOverflowDataV2 = extern struct {
583    loc: SourceLocation,
584    from: *const TypeDescriptor,
585    to: *const TypeDescriptor,
586};
587
588fn floatCastOverflowAbort(
589    data_handle: *align(8) const anyopaque,
590    from_handle: ValueHandle,
591) callconv(.c) noreturn {
592    floatCastOverflow(data_handle, from_handle);
593}
594
595fn floatCastOverflow(
596    data_handle: *align(8) const anyopaque,
597    from_handle: ValueHandle,
598) callconv(.c) noreturn {
599    // See: https://github.com/llvm/llvm-project/blob/release/19.x/compiler-rt/lib/ubsan/ubsan_handlers.cpp#L463
600    // for more information on this check.
601    const ptr: [*]const u8 = @ptrCast(data_handle);
602    if (@as(u16, ptr[0]) + @as(u16, ptr[1]) < 2 or ptr[0] == 0xFF or ptr[1] == 0xFF) {
603        const data: *const FloatCastOverflowData = @ptrCast(data_handle);
604        const from_value: Value = .{ .handle = from_handle, .td = data.from };
605        panic(@returnAddress(), "{f} is outside the range of representable values of type {s}", .{
606            from_value, data.to.getName(),
607        });
608    } else {
609        const data: *const FloatCastOverflowDataV2 = @ptrCast(data_handle);
610        const from_value: Value = .{ .handle = from_handle, .td = data.from };
611        panic(@returnAddress(), "{f} is outside the range of representable values of type {s}", .{
612            from_value, data.to.getName(),
613        });
614    }
615}
616
617fn exportHandler(
618    handler: anytype,
619    comptime sym_name: []const u8,
620) void {
621    @export(handler, .{
622        .name = "__ubsan_handle_" ++ sym_name,
623        .linkage = .weak,
624        .visibility = .hidden,
625    });
626}
627
628fn exportHandlerWithAbort(
629    handler: anytype,
630    abort_handler: anytype,
631    comptime sym_name: []const u8,
632) void {
633    @export(handler, .{
634        .name = "__ubsan_handle_" ++ sym_name,
635        .linkage = .weak,
636        .visibility = .hidden,
637    });
638    @export(abort_handler, .{
639        .name = "__ubsan_handle_" ++ sym_name ++ "_abort",
640        .linkage = .weak,
641        .visibility = .hidden,
642    });
643}
644
645const can_build_ubsan = switch (builtin.zig_backend) {
646    .stage2_powerpc,
647    .stage2_riscv64,
648    => false,
649    else => true,
650};
651
652comptime {
653    if (can_build_ubsan) {
654        overflowHandler("add_overflow", "+");
655        overflowHandler("mul_overflow", "*");
656        overflowHandler("sub_overflow", "-");
657        exportHandlerWithAbort(&alignmentAssumptionHandler, &alignmentAssumptionHandlerAbort, "alignment_assumption");
658
659        exportHandlerWithAbort(&divRemHandler, &divRemHandlerAbort, "divrem_overflow");
660        exportHandlerWithAbort(&floatCastOverflow, &floatCastOverflowAbort, "float_cast_overflow");
661        exportHandlerWithAbort(&invalidBuiltin, &invalidBuiltinAbort, "invalid_builtin");
662        exportHandlerWithAbort(&loadInvalidValue, &loadInvalidValueAbort, "load_invalid_value");
663
664        exportHandlerWithAbort(&negationHandler, &negationHandlerAbort, "negate_overflow");
665        exportHandlerWithAbort(&nonNullArg, &nonNullArgAbort, "nonnull_arg");
666        exportHandlerWithAbort(&nonNullReturn, &nonNullReturnAbort, "nonnull_return_v1");
667        exportHandlerWithAbort(&outOfBounds, &outOfBoundsAbort, "out_of_bounds");
668        exportHandlerWithAbort(&pointerOverflow, &pointerOverflowAbort, "pointer_overflow");
669        exportHandlerWithAbort(&shiftOob, &shiftOobAbort, "shift_out_of_bounds");
670        exportHandlerWithAbort(&typeMismatch, &typeMismatchAbort, "type_mismatch_v1");
671        exportHandlerWithAbort(&vlaBoundNotPositive, &vlaBoundNotPositiveAbort, "vla_bound_not_positive");
672
673        exportHandler(&builtinUnreachable, "builtin_unreachable");
674        exportHandler(&missingReturn, "missing_return");
675    }
676
677    // these checks are nearly impossible to replicate in zig, as they rely on nuances
678    // in the Itanium C++ ABI.
679    // exportHandlerWithAbort(&dynamicTypeCacheMiss, &dynamicTypeCacheMissAbort, "dynamic-type-cache-miss");
680    // exportHandlerWithAbort(&vptrTypeCache, &vptrTypeCacheAbort, "vptr-type-cache");
681
682    // we disable -fsanitize=function for reasons explained in src/Compilation.zig
683    // exportHandlerWithAbort(&functionTypeMismatch, &functionTypeMismatchAbort, "function-type-mismatch");
684    // exportHandlerWithAbort(&functionTypeMismatchV1, &functionTypeMismatchV1Abort, "function-type-mismatch-v1");
685}