//! Minimal UBSan Runtime const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const SourceLocation = extern struct { file_name: ?[*:0]const u8, line: u32, col: u32, }; const TypeDescriptor = extern struct { kind: Kind, info: Info, // name: [?:0]u8 const Kind = enum(u16) { integer = 0x0000, float = 0x0001, unknown = 0xFFFF, }; const Info = extern union { integer: packed struct(u16) { signed: bool, bit_width: u15, }, }; fn getIntegerSize(desc: TypeDescriptor) u64 { assert(desc.kind == .integer); const bit_width = desc.info.integer.bit_width; return @as(u64, 1) << @intCast(bit_width); } fn isSigned(desc: TypeDescriptor) bool { return desc.kind == .integer and desc.info.integer.signed; } fn getName(desc: *const TypeDescriptor) [:0]const u8 { return std.mem.span(@as([*:0]const u8, @ptrCast(desc)) + @sizeOf(TypeDescriptor)); } }; const ValueHandle = *const opaque { fn getValue(handle: ValueHandle, data: anytype) Value { return .{ .handle = handle, .type_descriptor = data.type_descriptor }; } }; const Value = extern struct { type_descriptor: *const TypeDescriptor, handle: ValueHandle, fn getUnsignedInteger(value: Value) u128 { assert(!value.type_descriptor.isSigned()); const size = value.type_descriptor.getIntegerSize(); const max_inline_size = @bitSizeOf(ValueHandle); if (size <= max_inline_size) { return @intFromPtr(value.handle); } return switch (size) { 64 => @as(*const u64, @alignCast(@ptrCast(value.handle))).*, 128 => @as(*const u128, @alignCast(@ptrCast(value.handle))).*, else => unreachable, }; } fn getSignedInteger(value: Value) i128 { assert(value.type_descriptor.isSigned()); const size = value.type_descriptor.getIntegerSize(); const max_inline_size = @bitSizeOf(ValueHandle); if (size <= max_inline_size) { const extra_bits: u6 = @intCast(max_inline_size - size); const handle: i64 = @bitCast(@intFromPtr(value.handle)); return (handle << extra_bits) >> extra_bits; } return switch (size) { 64 => @as(*const i64, @alignCast(@ptrCast(value.handle))).*, 128 => @as(*const i128, @alignCast(@ptrCast(value.handle))).*, else => unreachable, }; } fn isMinusOne(value: Value) bool { return value.type_descriptor.isSigned() and value.getSignedInteger() == -1; } fn isNegative(value: Value) bool { return value.type_descriptor.isSigned() and value.getSignedInteger() < 0; } fn getPositiveInteger(value: Value) u128 { if (value.type_descriptor.isSigned()) { const signed = value.getSignedInteger(); assert(signed >= 0); return @intCast(signed); } else { return value.getUnsignedInteger(); } } pub fn format( value: Value, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype, ) !void { comptime assert(fmt.len == 0); switch (value.type_descriptor.kind) { .integer => { if (value.type_descriptor.isSigned()) { try writer.print("{}", .{value.getSignedInteger()}); } else { try writer.print("{}", .{value.getUnsignedInteger()}); } }, .float => @panic("TODO: write float"), .unknown => try writer.writeAll("(unknown)"), } } }; const OverflowData = extern struct { loc: SourceLocation, type_descriptor: *const TypeDescriptor, }; fn overflowHandler( comptime sym_name: []const u8, comptime operator: []const u8, ) void { const S = struct { fn handler( data: *OverflowData, lhs_handle: ValueHandle, rhs_handle: ValueHandle, ) callconv(.c) noreturn { const lhs = lhs_handle.getValue(data); const rhs = rhs_handle.getValue(data); const is_signed = data.type_descriptor.isSigned(); const fmt = "{s} integer overflow: " ++ "{} " ++ operator ++ " {} cannot be represented in type {s}"; logMessage(fmt, .{ if (is_signed) "signed" else "unsigned", lhs, rhs, data.type_descriptor.getName(), }); } }; exportHandler(&S.handler, sym_name, true); } fn negationHandler( data: *const OverflowData, old_value_handle: ValueHandle, ) callconv(.c) noreturn { const old_value = old_value_handle.getValue(data); logMessage( "negation of {} cannot be represented in type {s}", .{ old_value, data.type_descriptor.getName() }, ); } fn divRemHandler( data: *const OverflowData, lhs_handle: ValueHandle, rhs_handle: ValueHandle, ) callconv(.c) noreturn { const is_signed = data.type_descriptor.isSigned(); const lhs = lhs_handle.getValue(data); const rhs = rhs_handle.getValue(data); if (is_signed and rhs.getSignedInteger() == -1) { logMessage( "division of {} by -1 cannot be represented in type {s}", .{ lhs, data.type_descriptor.getName() }, ); } else logMessage("division by zero", .{}); } const AlignmentAssumptionData = extern struct { loc: SourceLocation, assumption_loc: SourceLocation, type_descriptor: *const TypeDescriptor, }; fn alignmentAssumptionHandler( data: *const AlignmentAssumptionData, pointer: ValueHandle, alignment: ValueHandle, maybe_offset: ?ValueHandle, ) callconv(.c) noreturn { _ = pointer; // TODO: add the hint here? // const real_pointer = @intFromPtr(pointer) - @intFromPtr(maybe_offset); // const lsb = @ctz(real_pointer); // const actual_alignment = @as(u64, 1) << @intCast(lsb); // const mask = @intFromPtr(alignment) - 1; // const misalignment_offset = real_pointer & mask; // _ = actual_alignment; // _ = misalignment_offset; if (maybe_offset) |offset| { logMessage( "assumption of {} byte alignment (with offset of {} byte) for pointer of type {s} failed", .{ alignment.getValue(data), @intFromPtr(offset), data.type_descriptor.getName() }, ); } else { logMessage( "assumption of {} byte alignment for pointer of type {s} failed", .{ alignment.getValue(data), data.type_descriptor.getName() }, ); } } const ShiftOobData = extern struct { loc: SourceLocation, lhs_type: *const TypeDescriptor, rhs_type: *const TypeDescriptor, }; fn shiftOob( data: *const ShiftOobData, lhs_handle: ValueHandle, rhs_handle: ValueHandle, ) callconv(.c) noreturn { const lhs: Value = .{ .handle = lhs_handle, .type_descriptor = data.lhs_type }; const rhs: Value = .{ .handle = rhs_handle, .type_descriptor = data.rhs_type }; if (rhs.isNegative() or rhs.getPositiveInteger() >= data.lhs_type.getIntegerSize()) { if (rhs.isNegative()) { logMessage("shift exponent {} is negative", .{rhs}); } else { logMessage( "shift exponent {} is too large for {}-bit type {s}", .{ rhs, data.lhs_type.getIntegerSize(), data.lhs_type.getName() }, ); } } else { if (lhs.isNegative()) { logMessage("left shift of negative value {}", .{lhs}); } else { logMessage( "left shift of {} by {} places cannot be represented in type {s}", .{ lhs, rhs, data.lhs_type.getName() }, ); } } } const OutOfBoundsData = extern struct { loc: SourceLocation, array_type: *const TypeDescriptor, index_type: *const TypeDescriptor, }; fn outOfBounds(data: *const OutOfBoundsData, index_handle: ValueHandle) callconv(.c) noreturn { const index: Value = .{ .handle = index_handle, .type_descriptor = data.index_type }; logMessage( "index {} out of bounds for type {s}", .{ index, data.array_type.getName() }, ); } const PointerOverflowData = extern struct { loc: SourceLocation, }; fn pointerOverflow( _: *const PointerOverflowData, base: usize, result: usize, ) callconv(.c) noreturn { if (base == 0) { if (result == 0) { logMessage("applying zero offset to null pointer", .{}); } else { logMessage("applying non-zero offset {} to null pointer", .{result}); } } else { if (result == 0) { logMessage( "applying non-zero offset to non-null pointer 0x{x} produced null pointer", .{base}, ); } else { @panic("TODO"); } } } const TypeMismatchData = extern struct { loc: SourceLocation, type_descriptor: *const TypeDescriptor, log_alignment: u8, kind: enum(u8) { load, store, reference_binding, member_access, member_call, constructor_call, downcast_pointer, downcast_reference, upcast, upcast_to_virtual_base, nonnull_assign, dynamic_operation, fn getName(kind: @This()) []const u8 { return switch (kind) { .load => "load of", .store => "store of", .reference_binding => "reference binding to", .member_access => "member access within", .member_call => "member call on", .constructor_call => "constructor call on", .downcast_pointer, .downcast_reference => "downcast of", .upcast => "upcast of", .upcast_to_virtual_base => "cast to virtual base of", .nonnull_assign => "_Nonnull binding to", .dynamic_operation => "dynamic operation on", }; } }, }; fn typeMismatch( data: *const TypeMismatchData, pointer: ?ValueHandle, ) callconv(.c) noreturn { const alignment = @as(usize, 1) << @intCast(data.log_alignment); const handle: usize = @intFromPtr(pointer); if (pointer == null) { logMessage( "{s} null pointer of type {s}", .{ data.kind.getName(), data.type_descriptor.getName() }, ); } else if (!std.mem.isAligned(handle, alignment)) { logMessage( "{s} misaligned address 0x{x} for type {s}, which requires {} byte alignment", .{ data.kind.getName(), handle, data.type_descriptor.getName(), alignment }, ); } else { logMessage( "{s} address 0x{x} with insufficient space for an object of type {s}", .{ data.kind.getName(), handle, data.type_descriptor.getName() }, ); } } const UnreachableData = extern struct { loc: SourceLocation, }; fn builtinUnreachable(_: *const UnreachableData) callconv(.c) noreturn { logMessage("execution reached an unreachable program point", .{}); } fn missingReturn(_: *const UnreachableData) callconv(.c) noreturn { logMessage("execution reached the end of a value-returning function without returning a value", .{}); } const NonNullReturnData = extern struct { attribute_loc: SourceLocation, }; fn nonNullReturn(_: *const NonNullReturnData) callconv(.c) noreturn { logMessage("null pointer returned from function declared to never return null", .{}); } const NonNullArgData = extern struct { loc: SourceLocation, attribute_loc: SourceLocation, arg_index: i32, }; fn nonNullArg(data: *const NonNullArgData) callconv(.c) noreturn { logMessage( "null pointer passed as argument {}, which is declared to never be null", .{data.arg_index}, ); } const InvalidValueData = extern struct { loc: SourceLocation, type_descriptor: *const TypeDescriptor, }; fn loadInvalidValue( data: *const InvalidValueData, value_handle: ValueHandle, ) callconv(.c) noreturn { logMessage("load of value {}, which is not valid for type {s}", .{ value_handle.getValue(data), data.type_descriptor.getName(), }); } fn SimpleHandler(comptime error_name: []const u8) type { return struct { fn handler() callconv(.c) noreturn { logMessage("{s}", .{error_name}); } }; } inline fn logMessage(comptime fmt: []const u8, args: anytype) noreturn { std.debug.panicExtra(null, @returnAddress(), fmt, args); } fn exportHandler( handler: anytype, comptime sym_name: []const u8, comptime abort: bool, ) void { const linkage = if (builtin.is_test) .internal else .weak; { const N = "__ubsan_handle_" ++ sym_name; @export(handler, .{ .name = N, .linkage = linkage }); } if (abort) { const N = "__ubsan_handle_" ++ sym_name ++ "_abort"; @export(handler, .{ .name = N, .linkage = linkage }); } } fn exportMinimal( err_name: anytype, comptime sym_name: []const u8, comptime abort: bool, ) void { const handler = &SimpleHandler(err_name).handler; const linkage = if (builtin.is_test) .internal else .weak; { const N = "__ubsan_handle_" ++ sym_name ++ "_minimal"; @export(handler, .{ .name = N, .linkage = linkage }); } if (abort) { const N = "__ubsan_handle_" ++ sym_name ++ "_minimal_abort"; @export(handler, .{ .name = N, .linkage = linkage }); } } fn exportHelper( comptime err_name: []const u8, comptime sym_name: []const u8, comptime abort: bool, ) void { exportHandler(&SimpleHandler(err_name).handler, sym_name, abort); exportMinimal(err_name, sym_name, abort); } comptime { overflowHandler("add_overflow", "+"); overflowHandler("sub_overflow", "-"); overflowHandler("mul_overflow", "*"); exportHandler(&negationHandler, "negate_overflow", true); exportHandler(&divRemHandler, "divrem_overflow", true); exportHandler(&alignmentAssumptionHandler, "alignment_assumption", true); exportHandler(&shiftOob, "shift_out_of_bounds", true); exportHandler(&outOfBounds, "out_of_bounds", true); exportHandler(&pointerOverflow, "pointer_overflow", true); exportHandler(&typeMismatch, "type_mismatch_v1", true); exportHandler(&builtinUnreachable, "builtin_unreachable", false); exportHandler(&missingReturn, "missing_return", false); exportHandler(&nonNullReturn, "nonnull_return_v1", true); exportHandler(&nonNullArg, "nonnull_arg", true); exportHandler(&loadInvalidValue, "load_invalid_value", true); exportHelper("vla-bound-not-positive", "vla_bound_not_positive", true); exportHelper("float-cast-overflow", "float_cast_overflow", true); exportHelper("invalid-builtin", "invalid_builtin", true); exportHelper("function-type-mismatch", "function_type_mismatch", true); exportHelper("implicit-conversion", "implicit_conversion", true); exportHelper("nullability-arg", "nullability_arg", true); exportHelper("nullability-return", "nullability_return", true); exportHelper("cfi-check-fail", "cfi_check_fail", true); exportHelper("function-type-mismatch-v1", "function_type_mismatch_v1", true); exportMinimal("builtin-unreachable", "builtin_unreachable", false); exportMinimal("add-overflow", "add_overflow", true); exportMinimal("sub-overflow", "sub_overflow", true); exportMinimal("mul-overflow", "mul_overflow", true); exportMinimal("negation-handler", "negate_overflow", true); exportMinimal("divrem-handler", "divrem_overflow", true); exportMinimal("alignment-assumption-handler", "alignment_assumption", true); exportMinimal("shift-oob", "shift_out_of_bounds", true); exportMinimal("out-of-bounds", "out_of_bounds", true); exportMinimal("pointer-overflow", "pointer_overflow", true); exportMinimal("type-mismatch", "type_mismatch", true); // these checks are nearly impossible to duplicate in zig, as they rely on nuances // in the Itanium C++ ABI. // exportHelper("dynamic_type_cache_miss", "dynamic-type-cache-miss", true); // exportHelper("vptr_type_cache", "vptr-type-cache", true); }