zig/lib/ubsan_rt.zig
2025-02-25 11:22:33 -08:00

711 lines
22 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const panic = std.debug.panicExtra;
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,
},
float: u16,
};
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 {};
const Value = extern struct {
td: *const TypeDescriptor,
handle: ValueHandle,
fn getUnsignedInteger(value: Value) u128 {
assert(!value.td.isSigned());
const size = value.td.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 => @trap(),
};
}
fn getSignedInteger(value: Value) i128 {
assert(value.td.isSigned());
const size = value.td.getIntegerSize();
const max_inline_size = @bitSizeOf(ValueHandle);
if (size <= max_inline_size) {
const extra_bits: std.math.Log2Int(usize) = @intCast(max_inline_size - size);
const handle: isize = @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 => @trap(),
};
}
fn getFloat(value: Value) f128 {
assert(value.td.kind == .float);
const size = value.td.info.float;
const max_inline_size = @bitSizeOf(ValueHandle);
if (size <= max_inline_size) {
return @as(switch (@bitSizeOf(usize)) {
32 => f32,
64 => f64,
else => @compileError("unsupported target"),
}, @bitCast(@intFromPtr(value.handle)));
}
return @floatCast(switch (size) {
64 => @as(*const f64, @alignCast(@ptrCast(value.handle))).*,
80 => @as(*const f80, @alignCast(@ptrCast(value.handle))).*,
128 => @as(*const f128, @alignCast(@ptrCast(value.handle))).*,
else => @trap(),
});
}
fn isMinusOne(value: Value) bool {
return value.td.isSigned() and
value.getSignedInteger() == -1;
}
fn isNegative(value: Value) bool {
return value.td.isSigned() and
value.getSignedInteger() < 0;
}
fn getPositiveInteger(value: Value) u128 {
if (value.td.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);
// Work around x86_64 backend limitation.
if (builtin.zig_backend == .stage2_x86_64 and builtin.os.tag == .windows) {
try writer.writeAll("(unknown)");
return;
}
switch (value.td.kind) {
.integer => {
if (value.td.isSigned()) {
try writer.print("{}", .{value.getSignedInteger()});
} else {
try writer.print("{}", .{value.getUnsignedInteger()});
}
},
.float => try writer.print("{}", .{value.getFloat()}),
.unknown => try writer.writeAll("(unknown)"),
}
}
};
const OverflowData = extern struct {
loc: SourceLocation,
td: *const TypeDescriptor,
};
fn overflowHandler(
comptime sym_name: []const u8,
comptime operator: []const u8,
) void {
const S = struct {
fn abort(
data: *const OverflowData,
lhs_handle: ValueHandle,
rhs_handle: ValueHandle,
) callconv(.c) noreturn {
handler(data, lhs_handle, rhs_handle);
}
fn handler(
data: *const OverflowData,
lhs_handle: ValueHandle,
rhs_handle: ValueHandle,
) callconv(.c) noreturn {
const lhs: Value = .{ .handle = lhs_handle, .td = data.td };
const rhs: Value = .{ .handle = rhs_handle, .td = data.td };
const is_signed = data.td.isSigned();
const fmt = "{s} integer overflow: " ++ "{} " ++
operator ++ " {} cannot be represented in type {s}";
panic(@returnAddress(), fmt, .{
if (is_signed) "signed" else "unsigned",
lhs,
rhs,
data.td.getName(),
});
}
};
exportHandlerWithAbort(&S.handler, &S.abort, sym_name);
}
fn negationHandlerAbort(
data: *const OverflowData,
value_handle: ValueHandle,
) callconv(.c) noreturn {
negationHandler(data, value_handle);
}
fn negationHandler(
data: *const OverflowData,
value_handle: ValueHandle,
) callconv(.c) noreturn {
const value: Value = .{ .handle = value_handle, .td = data.td };
panic(
@returnAddress(),
"negation of {} cannot be represented in type {s}",
.{ value, data.td.getName() },
);
}
fn divRemHandlerAbort(
data: *const OverflowData,
lhs_handle: ValueHandle,
rhs_handle: ValueHandle,
) callconv(.c) noreturn {
divRemHandler(data, lhs_handle, rhs_handle);
}
fn divRemHandler(
data: *const OverflowData,
lhs_handle: ValueHandle,
rhs_handle: ValueHandle,
) callconv(.c) noreturn {
const lhs: Value = .{ .handle = lhs_handle, .td = data.td };
const rhs: Value = .{ .handle = rhs_handle, .td = data.td };
if (rhs.isMinusOne()) {
panic(
@returnAddress(),
"division of {} by -1 cannot be represented in type {s}",
.{ lhs, data.td.getName() },
);
} else panic(@returnAddress(), "division by zero", .{});
}
const AlignmentAssumptionData = extern struct {
loc: SourceLocation,
assumption_loc: SourceLocation,
td: *const TypeDescriptor,
};
fn alignmentAssumptionHandlerAbort(
data: *const AlignmentAssumptionData,
pointer: ValueHandle,
alignment_handle: ValueHandle,
maybe_offset: ?ValueHandle,
) callconv(.c) noreturn {
alignmentAssumptionHandler(
data,
pointer,
alignment_handle,
maybe_offset,
);
}
fn alignmentAssumptionHandler(
data: *const AlignmentAssumptionData,
pointer: ValueHandle,
alignment_handle: ValueHandle,
maybe_offset: ?ValueHandle,
) callconv(.c) noreturn {
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_handle) - 1;
const misalignment_offset = real_pointer & mask;
const alignment: Value = .{ .handle = alignment_handle, .td = data.td };
if (maybe_offset) |offset| {
panic(
@returnAddress(),
"assumption of {} byte alignment (with offset of {} byte) for pointer of type {s} failed\n" ++
"offset address is {} aligned, misalignment offset is {} bytes",
.{
alignment,
@intFromPtr(offset),
data.td.getName(),
actual_alignment,
misalignment_offset,
},
);
} else {
panic(
@returnAddress(),
"assumption of {} byte alignment for pointer of type {s} failed\n" ++
"address is {} aligned, misalignment offset is {} bytes",
.{
alignment,
data.td.getName(),
actual_alignment,
misalignment_offset,
},
);
}
}
const ShiftOobData = extern struct {
loc: SourceLocation,
lhs_type: *const TypeDescriptor,
rhs_type: *const TypeDescriptor,
};
fn shiftOobAbort(
data: *const ShiftOobData,
lhs_handle: ValueHandle,
rhs_handle: ValueHandle,
) callconv(.c) noreturn {
shiftOob(data, lhs_handle, rhs_handle);
}
fn shiftOob(
data: *const ShiftOobData,
lhs_handle: ValueHandle,
rhs_handle: ValueHandle,
) callconv(.c) noreturn {
const lhs: Value = .{ .handle = lhs_handle, .td = data.lhs_type };
const rhs: Value = .{ .handle = rhs_handle, .td = data.rhs_type };
if (rhs.isNegative() or
rhs.getPositiveInteger() >= data.lhs_type.getIntegerSize())
{
if (rhs.isNegative()) {
panic(@returnAddress(), "shift exponent {} is negative", .{rhs});
} else {
panic(
@returnAddress(),
"shift exponent {} is too large for {}-bit type {s}",
.{ rhs, data.lhs_type.getIntegerSize(), data.lhs_type.getName() },
);
}
} else {
if (lhs.isNegative()) {
panic(@returnAddress(), "left shift of negative value {}", .{lhs});
} else {
panic(
@returnAddress(),
"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 outOfBoundsAbort(
data: *const OutOfBoundsData,
index_handle: ValueHandle,
) callconv(.c) noreturn {
outOfBounds(data, index_handle);
}
fn outOfBounds(
data: *const OutOfBoundsData,
index_handle: ValueHandle,
) callconv(.c) noreturn {
const index: Value = .{ .handle = index_handle, .td = data.index_type };
panic(
@returnAddress(),
"index {} out of bounds for type {s}",
.{ index, data.array_type.getName() },
);
}
const PointerOverflowData = extern struct {
loc: SourceLocation,
};
fn pointerOverflowAbort(
data: *const PointerOverflowData,
base: usize,
result: usize,
) callconv(.c) noreturn {
pointerOverflow(data, base, result);
}
fn pointerOverflow(
_: *const PointerOverflowData,
base: usize,
result: usize,
) callconv(.c) noreturn {
if (base == 0) {
if (result == 0) {
panic(@returnAddress(), "applying zero offset to null pointer", .{});
} else {
panic(@returnAddress(), "applying non-zero offset {} to null pointer", .{result});
}
} else {
if (result == 0) {
panic(
@returnAddress(),
"applying non-zero offset to non-null pointer 0x{x} produced null pointer",
.{base},
);
} else {
const signed_base: isize = @bitCast(base);
const signed_result: isize = @bitCast(result);
if ((signed_base >= 0) == (signed_result >= 0)) {
if (base > result) {
panic(
@returnAddress(),
"addition of unsigned offset to 0x{x} overflowed to 0x{x}",
.{ base, result },
);
} else {
panic(
@returnAddress(),
"subtraction of unsigned offset to 0x{x} overflowed to 0x{x}",
.{ base, result },
);
}
} else {
panic(
@returnAddress(),
"pointer index expression with base 0x{x} overflowed to 0x{x}",
.{ base, result },
);
}
}
}
}
const TypeMismatchData = extern struct {
loc: SourceLocation,
td: *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 typeMismatchAbort(
data: *const TypeMismatchData,
pointer: ?ValueHandle,
) callconv(.c) noreturn {
typeMismatch(data, pointer);
}
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) {
panic(
@returnAddress(),
"{s} null pointer of type {s}",
.{ data.kind.getName(), data.td.getName() },
);
} else if (!std.mem.isAligned(handle, alignment)) {
panic(
@returnAddress(),
"{s} misaligned address 0x{x} for type {s}, which requires {} byte alignment",
.{ data.kind.getName(), handle, data.td.getName(), alignment },
);
} else {
panic(
@returnAddress(),
"{s} address 0x{x} with insufficient space for an object of type {s}",
.{ data.kind.getName(), handle, data.td.getName() },
);
}
}
const UnreachableData = extern struct {
loc: SourceLocation,
};
fn builtinUnreachable(_: *const UnreachableData) callconv(.c) noreturn {
panic(@returnAddress(), "execution reached an unreachable program point", .{});
}
fn missingReturn(_: *const UnreachableData) callconv(.c) noreturn {
panic(@returnAddress(), "execution reached the end of a value-returning function without returning a value", .{});
}
const NonNullReturnData = extern struct {
attribute_loc: SourceLocation,
};
fn nonNullReturnAbort(data: *const NonNullReturnData) callconv(.c) noreturn {
nonNullReturn(data);
}
fn nonNullReturn(_: *const NonNullReturnData) callconv(.c) noreturn {
panic(@returnAddress(), "null pointer returned from function declared to never return null", .{});
}
const NonNullArgData = extern struct {
loc: SourceLocation,
attribute_loc: SourceLocation,
arg_index: i32,
};
fn nonNullArgAbort(data: *const NonNullArgData) callconv(.c) noreturn {
nonNullArg(data);
}
fn nonNullArg(data: *const NonNullArgData) callconv(.c) noreturn {
panic(
@returnAddress(),
"null pointer passed as argument {}, which is declared to never be null",
.{data.arg_index},
);
}
const InvalidValueData = extern struct {
loc: SourceLocation,
td: *const TypeDescriptor,
};
fn loadInvalidValueAbort(
data: *const InvalidValueData,
value_handle: ValueHandle,
) callconv(.c) noreturn {
loadInvalidValue(data, value_handle);
}
fn loadInvalidValue(
data: *const InvalidValueData,
value_handle: ValueHandle,
) callconv(.c) noreturn {
const value: Value = .{ .handle = value_handle, .td = data.td };
panic(
@returnAddress(),
"load of value {}, which is not valid for type {s}",
.{ value, data.td.getName() },
);
}
const InvalidBuiltinData = extern struct {
loc: SourceLocation,
kind: enum(u8) {
ctz,
clz,
},
};
fn invalidBuiltinAbort(data: *const InvalidBuiltinData) callconv(.c) noreturn {
invalidBuiltin(data);
}
fn invalidBuiltin(data: *const InvalidBuiltinData) callconv(.c) noreturn {
panic(
@returnAddress(),
"passing zero to {s}(), which is not a valid argument",
.{@tagName(data.kind)},
);
}
const VlaBoundNotPositive = extern struct {
loc: SourceLocation,
td: *const TypeDescriptor,
};
fn vlaBoundNotPositiveAbort(
data: *const VlaBoundNotPositive,
bound_handle: ValueHandle,
) callconv(.c) noreturn {
vlaBoundNotPositive(data, bound_handle);
}
fn vlaBoundNotPositive(
data: *const VlaBoundNotPositive,
bound_handle: ValueHandle,
) callconv(.c) noreturn {
const bound: Value = .{ .handle = bound_handle, .td = data.td };
panic(
@returnAddress(),
"variable length array bound evaluates to non-positive value {}",
.{bound},
);
}
const FloatCastOverflowData = extern struct {
from: *const TypeDescriptor,
to: *const TypeDescriptor,
};
const FloatCastOverflowDataV2 = extern struct {
loc: SourceLocation,
from: *const TypeDescriptor,
to: *const TypeDescriptor,
};
fn floatCastOverflowAbort(
data_handle: *align(8) const anyopaque,
from_handle: ValueHandle,
) callconv(.c) noreturn {
floatCastOverflow(data_handle, from_handle);
}
fn floatCastOverflow(
data_handle: *align(8) const anyopaque,
from_handle: ValueHandle,
) callconv(.c) noreturn {
// See: https://github.com/llvm/llvm-project/blob/release/19.x/compiler-rt/lib/ubsan/ubsan_handlers.cpp#L463
// for more information on this check.
const ptr: [*]const u8 = @ptrCast(data_handle);
if (@as(u16, ptr[0]) + @as(u16, ptr[1]) < 2 or ptr[0] == 0xFF or ptr[1] == 0xFF) {
const data: *const FloatCastOverflowData = @ptrCast(data_handle);
const from_value: Value = .{ .handle = from_handle, .td = data.from };
panic(@returnAddress(), "{} is outside the range of representable values of type {s}", .{
from_value, data.to.getName(),
});
} else {
const data: *const FloatCastOverflowDataV2 = @ptrCast(data_handle);
const from_value: Value = .{ .handle = from_handle, .td = data.from };
panic(@returnAddress(), "{} is outside the range of representable values of type {s}", .{
from_value, data.to.getName(),
});
}
}
fn exportHandler(
handler: anytype,
comptime sym_name: []const u8,
) void {
// Work around x86_64 backend limitation.
const linkage = if (builtin.zig_backend == .stage2_x86_64 and builtin.os.tag == .windows) .internal else .weak;
const N = "__ubsan_handle_" ++ sym_name;
@export(handler, .{ .name = N, .linkage = linkage });
}
fn exportHandlerWithAbort(
handler: anytype,
abort_handler: anytype,
comptime sym_name: []const u8,
) void {
// Work around x86_64 backend limitation.
const linkage = if (builtin.zig_backend == .stage2_x86_64 and builtin.os.tag == .windows) .internal else .weak;
{
const N = "__ubsan_handle_" ++ sym_name;
@export(handler, .{ .name = N, .linkage = linkage });
}
{
const N = "__ubsan_handle_" ++ sym_name ++ "_abort";
@export(abort_handler, .{ .name = N, .linkage = linkage });
}
}
const can_build_ubsan = switch (builtin.zig_backend) {
.stage2_riscv64 => false,
else => true,
};
comptime {
if (can_build_ubsan) {
overflowHandler("add_overflow", "+");
overflowHandler("mul_overflow", "*");
overflowHandler("sub_overflow", "-");
exportHandlerWithAbort(&alignmentAssumptionHandler, &alignmentAssumptionHandlerAbort, "alignment_assumption");
exportHandlerWithAbort(&divRemHandler, &divRemHandlerAbort, "divrem_overflow");
exportHandlerWithAbort(&floatCastOverflow, &floatCastOverflowAbort, "float_cast_overflow");
exportHandlerWithAbort(&invalidBuiltin, &invalidBuiltinAbort, "invalid_builtin");
exportHandlerWithAbort(&loadInvalidValue, &loadInvalidValueAbort, "load_invalid_value");
exportHandlerWithAbort(&negationHandler, &negationHandlerAbort, "negate_overflow");
exportHandlerWithAbort(&nonNullArg, &nonNullArgAbort, "nonnull_arg");
exportHandlerWithAbort(&nonNullReturn, &nonNullReturnAbort, "nonnull_return_v1");
exportHandlerWithAbort(&outOfBounds, &outOfBoundsAbort, "out_of_bounds");
exportHandlerWithAbort(&pointerOverflow, &pointerOverflowAbort, "pointer_overflow");
exportHandlerWithAbort(&shiftOob, &shiftOobAbort, "shift_out_of_bounds");
exportHandlerWithAbort(&typeMismatch, &typeMismatchAbort, "type_mismatch_v1");
exportHandlerWithAbort(&vlaBoundNotPositive, &vlaBoundNotPositiveAbort, "vla_bound_not_positive");
exportHandler(&builtinUnreachable, "builtin_unreachable");
exportHandler(&missingReturn, "missing_return");
}
// these checks are nearly impossible to replicate in zig, as they rely on nuances
// in the Itanium C++ ABI.
// exportHandlerWithAbort(&dynamicTypeCacheMiss, &dynamicTypeCacheMissAbort, "dynamic-type-cache-miss");
// exportHandlerWithAbort(&vptrTypeCache, &vptrTypeCacheAbort, "vptr-type-cache");
// we disable -fsanitize=function for reasons explained in src/Compilation.zig
// exportHandlerWithAbort(&functionTypeMismatch, &functionTypeMismatchAbort, "function-type-mismatch");
// exportHandlerWithAbort(&functionTypeMismatchV1, &functionTypeMismatchV1Abort, "function-type-mismatch-v1");
}