mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-09 02:54:49 +00:00
Merge pull request #8581 from roc-lang/roc-program-stack-overflow
Make a helper for Roc programs stack overflowing
This commit is contained in:
commit
516f5ea102
10 changed files with 735 additions and 291 deletions
|
|
@ -1,307 +1,122 @@
|
|||
//! Stack overflow detection and handling for the Roc compiler.
|
||||
//! Signal handling for the Roc compiler (stack overflow, segfault, division by zero).
|
||||
//!
|
||||
//! This module provides a mechanism to catch stack overflows and report them
|
||||
//! with a helpful error message instead of a generic segfault. This is particularly
|
||||
//! useful during compiler development when recursive algorithms might blow the stack.
|
||||
//! This module provides a thin wrapper around the generic signal handlers in
|
||||
//! builtins.handlers, configured with compiler-specific error messages.
|
||||
//!
|
||||
//! On POSIX systems (Linux, macOS), we use sigaltstack to set up an alternate
|
||||
//! signal stack and install a SIGSEGV handler that detects stack overflows.
|
||||
//! signal stack and install handlers for SIGSEGV, SIGBUS, and SIGFPE.
|
||||
//!
|
||||
//! On Windows, we use SetUnhandledExceptionFilter to catch EXCEPTION_STACK_OVERFLOW.
|
||||
//! On Windows, we use SetUnhandledExceptionFilter to catch various exceptions.
|
||||
//!
|
||||
//! WASI is not currently supported (no signal handling available).
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const handlers = @import("builtins").handlers;
|
||||
const posix = if (builtin.os.tag != .windows and builtin.os.tag != .wasi) std.posix else undefined;
|
||||
|
||||
// Windows types and constants
|
||||
const DWORD = u32;
|
||||
const LONG = i32;
|
||||
const ULONG_PTR = usize;
|
||||
const PVOID = ?*anyopaque;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const BOOL = i32;
|
||||
|
||||
const EXCEPTION_STACK_OVERFLOW: DWORD = 0xC00000FD;
|
||||
const EXCEPTION_ACCESS_VIOLATION: DWORD = 0xC0000005;
|
||||
const EXCEPTION_CONTINUE_SEARCH: LONG = 0;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
const INVALID_HANDLE_VALUE: HANDLE = @ptrFromInt(std.math.maxInt(usize));
|
||||
|
||||
const EXCEPTION_RECORD = extern struct {
|
||||
ExceptionCode: DWORD,
|
||||
ExceptionFlags: DWORD,
|
||||
ExceptionRecord: ?*EXCEPTION_RECORD,
|
||||
ExceptionAddress: PVOID,
|
||||
NumberParameters: DWORD,
|
||||
ExceptionInformation: [15]ULONG_PTR,
|
||||
};
|
||||
|
||||
const CONTEXT = extern struct {
|
||||
// We don't need the full context, just enough to make the struct valid
|
||||
data: [1232]u8, // Size varies by arch, this is x64 size
|
||||
};
|
||||
|
||||
const EXCEPTION_POINTERS = extern struct {
|
||||
ExceptionRecord: *EXCEPTION_RECORD,
|
||||
ContextRecord: *CONTEXT,
|
||||
};
|
||||
|
||||
const LPTOP_LEVEL_EXCEPTION_FILTER = ?*const fn (*EXCEPTION_POINTERS) callconv(.winapi) LONG;
|
||||
|
||||
// Windows API imports
|
||||
extern "kernel32" fn SetUnhandledExceptionFilter(lpTopLevelExceptionFilter: LPTOP_LEVEL_EXCEPTION_FILTER) callconv(.winapi) LPTOP_LEVEL_EXCEPTION_FILTER;
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) BOOL;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
|
||||
/// Size of the alternate signal stack (64KB should be plenty for the handler)
|
||||
const ALT_STACK_SIZE = 64 * 1024;
|
||||
|
||||
/// Storage for the alternate signal stack (POSIX only)
|
||||
var alt_stack_storage: [ALT_STACK_SIZE]u8 align(16) = undefined;
|
||||
|
||||
/// Whether the handler has been installed
|
||||
var handler_installed = false;
|
||||
|
||||
/// Error message to display on stack overflow
|
||||
const STACK_OVERFLOW_MESSAGE =
|
||||
\\
|
||||
\\================================================================================
|
||||
\\STACK OVERFLOW in the Roc compiler
|
||||
\\================================================================================
|
||||
\\
|
||||
\\The Roc compiler ran out of stack space. This is a bug in the compiler,
|
||||
\\not in your code.
|
||||
\\
|
||||
\\This often happens due to:
|
||||
\\ - Infinite recursion in type translation or unification
|
||||
\\ - Very deeply nested expressions without tail-call optimization
|
||||
\\ - Cyclic data structures without proper cycle detection
|
||||
\\
|
||||
\\Please report this issue at: https://github.com/roc-lang/roc/issues
|
||||
\\
|
||||
\\Include the Roc code that triggered this error if possible.
|
||||
\\
|
||||
\\================================================================================
|
||||
\\
|
||||
\\
|
||||
;
|
||||
|
||||
/// Install the stack overflow handler.
|
||||
/// This should be called early in main() before any significant work is done.
|
||||
/// Returns true if the handler was installed successfully, false otherwise.
|
||||
pub fn install() bool {
|
||||
if (handler_installed) return true;
|
||||
const STACK_OVERFLOW_MESSAGE = "\nThe Roc compiler overflowed its stack memory and had to exit.\n\n";
|
||||
|
||||
/// Callback for stack overflow in the compiler
|
||||
fn handleStackOverflow() noreturn {
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
return installWindows();
|
||||
}
|
||||
// Windows: use WriteFile for signal-safe output
|
||||
const DWORD = u32;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
|
||||
if (comptime builtin.os.tag == .wasi) {
|
||||
// WASI doesn't support signal handling
|
||||
return false;
|
||||
}
|
||||
|
||||
return installPosix();
|
||||
}
|
||||
|
||||
fn installPosix() bool {
|
||||
// Set up the alternate signal stack
|
||||
var alt_stack = posix.stack_t{
|
||||
.sp = &alt_stack_storage,
|
||||
.flags = 0,
|
||||
.size = ALT_STACK_SIZE,
|
||||
};
|
||||
|
||||
posix.sigaltstack(&alt_stack, null) catch {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Install the SIGSEGV handler
|
||||
const action = posix.Sigaction{
|
||||
.handler = .{ .sigaction = handleSignalPosix },
|
||||
.mask = posix.sigemptyset(),
|
||||
.flags = posix.SA.SIGINFO | posix.SA.ONSTACK,
|
||||
};
|
||||
|
||||
posix.sigaction(posix.SIG.SEGV, &action, null);
|
||||
|
||||
// Also catch SIGBUS which can occur on some systems for stack overflow
|
||||
posix.sigaction(posix.SIG.BUS, &action, null);
|
||||
|
||||
handler_installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn installWindows() bool {
|
||||
_ = SetUnhandledExceptionFilter(handleExceptionWindows);
|
||||
handler_installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Windows exception handler function
|
||||
fn handleExceptionWindows(exception_info: *EXCEPTION_POINTERS) callconv(.winapi) LONG {
|
||||
const exception_code = exception_info.ExceptionRecord.ExceptionCode;
|
||||
|
||||
// Check if this is a stack overflow or access violation
|
||||
const is_stack_overflow = (exception_code == EXCEPTION_STACK_OVERFLOW);
|
||||
const is_access_violation = (exception_code == EXCEPTION_ACCESS_VIOLATION);
|
||||
|
||||
if (!is_stack_overflow and !is_access_violation) {
|
||||
// Let other handlers deal with this exception
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// Write error message to stderr
|
||||
const stderr_handle = GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (stderr_handle != INVALID_HANDLE_VALUE and stderr_handle != null) {
|
||||
var bytes_written: DWORD = 0;
|
||||
if (is_stack_overflow) {
|
||||
_ = WriteFile(stderr_handle, STACK_OVERFLOW_MESSAGE.ptr, STACK_OVERFLOW_MESSAGE.len, &bytes_written, null);
|
||||
} else {
|
||||
const msg = "\nAccess violation in the Roc compiler.\n\nPlease report this issue at: https://github.com/roc-lang/roc/issues\n\n";
|
||||
_ = WriteFile(stderr_handle, msg.ptr, msg.len, &bytes_written, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Exit with appropriate code
|
||||
const exit_code: c_uint = if (is_stack_overflow) 134 else 139;
|
||||
ExitProcess(exit_code);
|
||||
}
|
||||
|
||||
/// The POSIX signal handler function
|
||||
fn handleSignalPosix(sig: i32, info: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
|
||||
// Get the fault address - access differs by platform
|
||||
const fault_addr: usize = getFaultAddress(info);
|
||||
|
||||
// Get the current stack pointer to help determine if this is a stack overflow
|
||||
var current_sp: usize = 0;
|
||||
asm volatile (""
|
||||
: [sp] "={sp}" (current_sp),
|
||||
);
|
||||
|
||||
// A stack overflow typically occurs when the fault address is near the stack pointer
|
||||
// or below the stack (stacks grow downward on most architectures)
|
||||
const likely_stack_overflow = isLikelyStackOverflow(fault_addr, current_sp);
|
||||
|
||||
// Write our error message to stderr (use STDERR_FILENO directly for signal safety)
|
||||
const stderr_fd = posix.STDERR_FILENO;
|
||||
|
||||
if (likely_stack_overflow) {
|
||||
_ = posix.write(stderr_fd, STACK_OVERFLOW_MESSAGE) catch {};
|
||||
} else {
|
||||
// Generic segfault - provide some context
|
||||
const generic_msg = switch (sig) {
|
||||
posix.SIG.SEGV => "\nSegmentation fault (SIGSEGV) in the Roc compiler.\nFault address: ",
|
||||
posix.SIG.BUS => "\nBus error (SIGBUS) in the Roc compiler.\nFault address: ",
|
||||
else => "\nFatal signal in the Roc compiler.\nFault address: ",
|
||||
const kernel32 = struct {
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
};
|
||||
_ = posix.write(stderr_fd, generic_msg) catch {};
|
||||
|
||||
const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE);
|
||||
var bytes_written: DWORD = 0;
|
||||
_ = kernel32.WriteFile(stderr_handle, STACK_OVERFLOW_MESSAGE.ptr, STACK_OVERFLOW_MESSAGE.len, &bytes_written, null);
|
||||
kernel32.ExitProcess(134);
|
||||
} else if (comptime builtin.os.tag != .wasi) {
|
||||
// POSIX: use direct write syscall for signal-safety
|
||||
_ = posix.write(posix.STDERR_FILENO, STACK_OVERFLOW_MESSAGE) catch {};
|
||||
posix.exit(134);
|
||||
} else {
|
||||
// WASI fallback
|
||||
std.process.exit(134);
|
||||
}
|
||||
}
|
||||
|
||||
/// Error message to display on arithmetic error (division by zero, etc.)
|
||||
const ARITHMETIC_ERROR_MESSAGE = "\nThe Roc compiler divided by zero and had to exit.\n\n";
|
||||
|
||||
/// Callback for arithmetic errors (division by zero) in the compiler
|
||||
fn handleArithmeticError() noreturn {
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
const DWORD = u32;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
|
||||
const kernel32 = struct {
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
};
|
||||
|
||||
const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE);
|
||||
var bytes_written: DWORD = 0;
|
||||
_ = kernel32.WriteFile(stderr_handle, ARITHMETIC_ERROR_MESSAGE.ptr, ARITHMETIC_ERROR_MESSAGE.len, &bytes_written, null);
|
||||
kernel32.ExitProcess(136);
|
||||
} else if (comptime builtin.os.tag != .wasi) {
|
||||
_ = posix.write(posix.STDERR_FILENO, ARITHMETIC_ERROR_MESSAGE) catch {};
|
||||
posix.exit(136); // 128 + 8 (SIGFPE)
|
||||
} else {
|
||||
std.process.exit(136);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback for access violation in the compiler
|
||||
fn handleAccessViolation(fault_addr: usize) noreturn {
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
const DWORD = u32;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
|
||||
const kernel32 = struct {
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
};
|
||||
|
||||
var addr_buf: [18]u8 = undefined;
|
||||
const addr_str = handlers.formatHex(fault_addr, &addr_buf);
|
||||
|
||||
const msg1 = "\nAccess violation in the Roc compiler.\nFault address: ";
|
||||
const msg2 = "\n\nPlease report this issue at: https://github.com/roc-lang/roc/issues\n\n";
|
||||
const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE);
|
||||
var bytes_written: DWORD = 0;
|
||||
_ = kernel32.WriteFile(stderr_handle, msg1.ptr, msg1.len, &bytes_written, null);
|
||||
_ = kernel32.WriteFile(stderr_handle, addr_str.ptr, @intCast(addr_str.len), &bytes_written, null);
|
||||
_ = kernel32.WriteFile(stderr_handle, msg2.ptr, msg2.len, &bytes_written, null);
|
||||
kernel32.ExitProcess(139);
|
||||
} else {
|
||||
// POSIX (and WASI fallback): use direct write syscall for signal-safety
|
||||
const generic_msg = "\nSegmentation fault (SIGSEGV) in the Roc compiler.\nFault address: ";
|
||||
_ = posix.write(posix.STDERR_FILENO, generic_msg) catch {};
|
||||
|
||||
// Write the fault address as hex
|
||||
var addr_buf: [18]u8 = undefined;
|
||||
const addr_str = formatHex(fault_addr, &addr_buf);
|
||||
_ = posix.write(stderr_fd, addr_str) catch {};
|
||||
_ = posix.write(stderr_fd, "\n\nPlease report this issue at: https://github.com/roc-lang/roc/issues\n\n") catch {};
|
||||
}
|
||||
|
||||
// Exit with a distinct error code for stack overflow
|
||||
if (likely_stack_overflow) {
|
||||
posix.exit(134); // 128 + 6 (SIGABRT-like)
|
||||
} else {
|
||||
posix.exit(139); // 128 + 11 (SIGSEGV)
|
||||
const addr_str = handlers.formatHex(fault_addr, &addr_buf);
|
||||
_ = posix.write(posix.STDERR_FILENO, addr_str) catch {};
|
||||
_ = posix.write(posix.STDERR_FILENO, "\n\nPlease report this issue at: https://github.com/roc-lang/roc/issues\n\n") catch {};
|
||||
posix.exit(139);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the fault address from siginfo_t (platform-specific)
|
||||
fn getFaultAddress(info: *const posix.siginfo_t) usize {
|
||||
// The siginfo_t structure varies by platform
|
||||
if (comptime builtin.os.tag == .linux) {
|
||||
// Linux: fault address is in fields.sigfault.addr
|
||||
return @intFromPtr(info.fields.sigfault.addr);
|
||||
} else if (comptime builtin.os.tag == .macos or
|
||||
builtin.os.tag == .ios or
|
||||
builtin.os.tag == .tvos or
|
||||
builtin.os.tag == .watchos or
|
||||
builtin.os.tag == .visionos or
|
||||
builtin.os.tag == .freebsd or
|
||||
builtin.os.tag == .dragonfly or
|
||||
builtin.os.tag == .netbsd or
|
||||
builtin.os.tag == .openbsd)
|
||||
{
|
||||
// macOS/iOS/BSD: fault address is in addr field
|
||||
return @intFromPtr(info.addr);
|
||||
} else {
|
||||
// Fallback: return 0 if we can't determine the address
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Heuristic to determine if a fault is likely a stack overflow
|
||||
fn isLikelyStackOverflow(fault_addr: usize, current_sp: usize) bool {
|
||||
// If fault address is 0 or very low, it's likely a null pointer dereference
|
||||
if (fault_addr < 4096) return false;
|
||||
|
||||
// Stack overflows typically fault near the stack guard page
|
||||
// The fault address will be close to (but below) the current stack pointer
|
||||
// We use a generous range since the stack pointer in the signal handler
|
||||
// is on the alternate stack
|
||||
|
||||
// On most systems, the main stack is in high memory and grows down
|
||||
// A stack overflow fault will be at an address lower than the normal stack
|
||||
|
||||
// Check if fault address is within a reasonable range of where stack would be
|
||||
// This is a heuristic - we check if the fault is in the lower part of address space
|
||||
// where guard pages typically are
|
||||
|
||||
const max_addr = std.math.maxInt(usize);
|
||||
const high_memory_threshold = max_addr - (16 * 1024 * 1024 * 1024); // 16GB from top
|
||||
|
||||
// If the fault is in the high memory region (where stacks live) but at a page boundary
|
||||
// it's likely a stack guard page hit
|
||||
if (fault_addr > high_memory_threshold) {
|
||||
// Check if it's at a page boundary (guard pages are typically page-aligned)
|
||||
const page_size = std.heap.page_size_min;
|
||||
const page_aligned = (fault_addr & (page_size - 1)) == 0 or (fault_addr & (page_size - 1)) < 64;
|
||||
if (page_aligned) return true;
|
||||
}
|
||||
|
||||
// Also check if the fault address is suspiciously close to the current SP
|
||||
// This catches cases where we're still on the main stack when the overflow happens
|
||||
const sp_distance = if (fault_addr < current_sp) current_sp - fault_addr else fault_addr - current_sp;
|
||||
if (sp_distance < 1024 * 1024) { // Within 1MB of stack pointer
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Format a usize as hexadecimal
|
||||
fn formatHex(value: usize, buf: []u8) []const u8 {
|
||||
const hex_chars = "0123456789abcdef";
|
||||
var i: usize = buf.len;
|
||||
|
||||
if (value == 0) {
|
||||
i -= 1;
|
||||
buf[i] = '0';
|
||||
} else {
|
||||
var v = value;
|
||||
while (v > 0 and i > 2) {
|
||||
i -= 1;
|
||||
buf[i] = hex_chars[v & 0xf];
|
||||
v >>= 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Add 0x prefix
|
||||
i -= 1;
|
||||
buf[i] = 'x';
|
||||
i -= 1;
|
||||
buf[i] = '0';
|
||||
|
||||
return buf[i..];
|
||||
/// Install signal handlers for stack overflow, segfault, and division by zero.
|
||||
/// This should be called early in main() before any significant work is done.
|
||||
/// Returns true if the handlers were installed successfully, false otherwise.
|
||||
pub fn install() bool {
|
||||
return handlers.install(handleStackOverflow, handleAccessViolation, handleArithmeticError);
|
||||
}
|
||||
|
||||
/// Test function that intentionally causes a stack overflow.
|
||||
|
|
@ -330,13 +145,13 @@ pub fn triggerStackOverflowForTest() noreturn {
|
|||
test "formatHex" {
|
||||
var buf: [18]u8 = undefined;
|
||||
|
||||
const zero = formatHex(0, &buf);
|
||||
const zero = handlers.formatHex(0, &buf);
|
||||
try std.testing.expectEqualStrings("0x0", zero);
|
||||
|
||||
const small = formatHex(0xff, &buf);
|
||||
const small = handlers.formatHex(0xff, &buf);
|
||||
try std.testing.expectEqualStrings("0xff", small);
|
||||
|
||||
const medium = formatHex(0xdeadbeef, &buf);
|
||||
const medium = handlers.formatHex(0xdeadbeef, &buf);
|
||||
try std.testing.expectEqualStrings("0xdeadbeef", medium);
|
||||
}
|
||||
|
||||
|
|
@ -430,13 +245,11 @@ fn verifyHandlerOutput(exited_normally: bool, exit_code: u8, termination_signal:
|
|||
// Exit code 139 = generic segfault (handler caught it but didn't classify as stack overflow)
|
||||
if (exited_normally and (exit_code == 134 or exit_code == 139)) {
|
||||
// Check that our handler message was printed
|
||||
const has_stack_overflow_msg = std.mem.indexOf(u8, stderr_output, "STACK OVERFLOW") != null;
|
||||
const has_stack_overflow_msg = std.mem.indexOf(u8, stderr_output, "overflowed its stack memory") != null;
|
||||
const has_segfault_msg = std.mem.indexOf(u8, stderr_output, "Segmentation fault") != null;
|
||||
const has_roc_compiler_msg = std.mem.indexOf(u8, stderr_output, "Roc compiler") != null;
|
||||
|
||||
// Handler should have printed EITHER stack overflow message OR segfault message
|
||||
try std.testing.expect(has_stack_overflow_msg or has_segfault_msg);
|
||||
try std.testing.expect(has_roc_compiler_msg);
|
||||
} else if (!exited_normally and (termination_signal == posix.SIG.SEGV or termination_signal == posix.SIG.BUS)) {
|
||||
// The handler might not have caught it - this can happen on some systems
|
||||
// where the signal delivery is different. Just warn and skip.
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ pub const ModuleType = enum {
|
|||
.fs => &.{},
|
||||
.tracy => &.{ .build_options, .builtins },
|
||||
.collections => &.{},
|
||||
.base => &.{.collections},
|
||||
.base => &.{ .collections, .builtins },
|
||||
.roc_src => &.{},
|
||||
.types => &.{ .base, .collections },
|
||||
.reporting => &.{ .collections, .base },
|
||||
|
|
|
|||
338
src/builtins/handlers.zig
Normal file
338
src/builtins/handlers.zig
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
//! Generic signal handlers for stack overflow, access violation, and arithmetic errors.
|
||||
//!
|
||||
//! This module provides a mechanism to catch runtime errors like stack overflows,
|
||||
//! access violations, and division by zero, handling them with custom callbacks
|
||||
//! instead of crashing with a raw signal.
|
||||
//!
|
||||
//! On POSIX systems (Linux, macOS), we use sigaltstack to set up an alternate
|
||||
//! signal stack and install handlers for SIGSEGV, SIGBUS, and SIGFPE.
|
||||
//!
|
||||
//! On Windows, we use SetUnhandledExceptionFilter to catch various exceptions.
|
||||
//!
|
||||
//! WASI is not currently supported (no signal handling available).
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const posix = if (builtin.os.tag != .windows and builtin.os.tag != .wasi) std.posix else undefined;
|
||||
|
||||
// Windows types and constants
|
||||
const DWORD = u32;
|
||||
const LONG = i32;
|
||||
const ULONG_PTR = usize;
|
||||
const PVOID = ?*anyopaque;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const BOOL = i32;
|
||||
|
||||
const EXCEPTION_STACK_OVERFLOW: DWORD = 0xC00000FD;
|
||||
const EXCEPTION_ACCESS_VIOLATION: DWORD = 0xC0000005;
|
||||
const EXCEPTION_INT_DIVIDE_BY_ZERO: DWORD = 0xC0000094;
|
||||
const EXCEPTION_INT_OVERFLOW: DWORD = 0xC0000095;
|
||||
const EXCEPTION_CONTINUE_SEARCH: LONG = 0;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
const INVALID_HANDLE_VALUE: HANDLE = @ptrFromInt(std.math.maxInt(usize));
|
||||
|
||||
const EXCEPTION_RECORD = extern struct {
|
||||
ExceptionCode: DWORD,
|
||||
ExceptionFlags: DWORD,
|
||||
ExceptionRecord: ?*EXCEPTION_RECORD,
|
||||
ExceptionAddress: PVOID,
|
||||
NumberParameters: DWORD,
|
||||
ExceptionInformation: [15]ULONG_PTR,
|
||||
};
|
||||
|
||||
const CONTEXT = extern struct {
|
||||
// We don't need the full context, just enough to make the struct valid
|
||||
data: [1232]u8, // Size varies by arch, this is x64 size
|
||||
};
|
||||
|
||||
const EXCEPTION_POINTERS = extern struct {
|
||||
ExceptionRecord: *EXCEPTION_RECORD,
|
||||
ContextRecord: *CONTEXT,
|
||||
};
|
||||
|
||||
const LPTOP_LEVEL_EXCEPTION_FILTER = ?*const fn (*EXCEPTION_POINTERS) callconv(.winapi) LONG;
|
||||
|
||||
// Windows API imports
|
||||
extern "kernel32" fn SetUnhandledExceptionFilter(lpTopLevelExceptionFilter: LPTOP_LEVEL_EXCEPTION_FILTER) callconv(.winapi) LPTOP_LEVEL_EXCEPTION_FILTER;
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) BOOL;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
|
||||
/// Size of the alternate signal stack (64KB should be plenty for the handler)
|
||||
const ALT_STACK_SIZE = 64 * 1024;
|
||||
|
||||
/// Storage for the alternate signal stack (POSIX only)
|
||||
var alt_stack_storage: [ALT_STACK_SIZE]u8 align(16) = undefined;
|
||||
|
||||
/// Whether the handler has been installed
|
||||
var handler_installed = false;
|
||||
|
||||
/// Callback function type for handling stack overflow
|
||||
pub const StackOverflowCallback = *const fn () noreturn;
|
||||
|
||||
/// Callback function type for handling access violation/segfault
|
||||
pub const AccessViolationCallback = *const fn (fault_addr: usize) noreturn;
|
||||
|
||||
/// Callback function type for handling division by zero (and other arithmetic errors)
|
||||
pub const ArithmeticErrorCallback = *const fn () noreturn;
|
||||
|
||||
/// Stored callbacks (set during install)
|
||||
var stack_overflow_callback: ?StackOverflowCallback = null;
|
||||
var access_violation_callback: ?AccessViolationCallback = null;
|
||||
var arithmetic_error_callback: ?ArithmeticErrorCallback = null;
|
||||
|
||||
/// Install signal handlers with custom callbacks.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - on_stack_overflow: Called when a stack overflow is detected. Must not return.
|
||||
/// - on_access_violation: Called for other memory access violations (segfaults).
|
||||
/// Receives the fault address. Must not return.
|
||||
/// - on_arithmetic_error: Called for arithmetic errors like division by zero. Must not return.
|
||||
///
|
||||
/// Returns true if the handlers were installed successfully, false otherwise.
|
||||
pub fn install(
|
||||
on_stack_overflow: StackOverflowCallback,
|
||||
on_access_violation: AccessViolationCallback,
|
||||
on_arithmetic_error: ArithmeticErrorCallback,
|
||||
) bool {
|
||||
if (handler_installed) return true;
|
||||
|
||||
stack_overflow_callback = on_stack_overflow;
|
||||
access_violation_callback = on_access_violation;
|
||||
arithmetic_error_callback = on_arithmetic_error;
|
||||
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
return installWindows();
|
||||
}
|
||||
|
||||
if (comptime builtin.os.tag == .wasi) {
|
||||
// WASI doesn't support signal handling
|
||||
return false;
|
||||
}
|
||||
|
||||
return installPosix();
|
||||
}
|
||||
|
||||
fn installPosix() bool {
|
||||
// Set up the alternate signal stack
|
||||
var alt_stack = posix.stack_t{
|
||||
.sp = &alt_stack_storage,
|
||||
.flags = 0,
|
||||
.size = ALT_STACK_SIZE,
|
||||
};
|
||||
|
||||
posix.sigaltstack(&alt_stack, null) catch {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Install the SIGSEGV handler for stack overflow and access violations
|
||||
const segv_action = posix.Sigaction{
|
||||
.handler = .{ .sigaction = handleSegvSignal },
|
||||
.mask = posix.sigemptyset(),
|
||||
.flags = posix.SA.SIGINFO | posix.SA.ONSTACK,
|
||||
};
|
||||
|
||||
posix.sigaction(posix.SIG.SEGV, &segv_action, null);
|
||||
|
||||
// Also catch SIGBUS which can occur on some systems for stack overflow
|
||||
posix.sigaction(posix.SIG.BUS, &segv_action, null);
|
||||
|
||||
// Install the SIGFPE handler for division by zero and other arithmetic errors
|
||||
const fpe_action = posix.Sigaction{
|
||||
.handler = .{ .sigaction = handleFpeSignal },
|
||||
.mask = posix.sigemptyset(),
|
||||
.flags = posix.SA.SIGINFO | posix.SA.ONSTACK,
|
||||
};
|
||||
|
||||
posix.sigaction(posix.SIG.FPE, &fpe_action, null);
|
||||
|
||||
handler_installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn installWindows() bool {
|
||||
_ = SetUnhandledExceptionFilter(handleExceptionWindows);
|
||||
handler_installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Windows exception handler function
|
||||
fn handleExceptionWindows(exception_info: *EXCEPTION_POINTERS) callconv(.winapi) LONG {
|
||||
const exception_code = exception_info.ExceptionRecord.ExceptionCode;
|
||||
|
||||
// Check if this is a known exception type
|
||||
const is_stack_overflow = (exception_code == EXCEPTION_STACK_OVERFLOW);
|
||||
const is_access_violation = (exception_code == EXCEPTION_ACCESS_VIOLATION);
|
||||
const is_divide_by_zero = (exception_code == EXCEPTION_INT_DIVIDE_BY_ZERO);
|
||||
const is_int_overflow = (exception_code == EXCEPTION_INT_OVERFLOW);
|
||||
const is_arithmetic_error = is_divide_by_zero or is_int_overflow;
|
||||
|
||||
if (!is_stack_overflow and !is_access_violation and !is_arithmetic_error) {
|
||||
// Let other handlers deal with this exception
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
if (is_stack_overflow) {
|
||||
if (stack_overflow_callback) |callback| {
|
||||
callback();
|
||||
}
|
||||
ExitProcess(134);
|
||||
} else if (is_arithmetic_error) {
|
||||
if (arithmetic_error_callback) |callback| {
|
||||
callback();
|
||||
}
|
||||
ExitProcess(136); // 128 + 8 (SIGFPE)
|
||||
} else {
|
||||
if (access_violation_callback) |callback| {
|
||||
// Get fault address from ExceptionInformation[1] for access violations
|
||||
const fault_addr = exception_info.ExceptionRecord.ExceptionInformation[1];
|
||||
callback(fault_addr);
|
||||
}
|
||||
ExitProcess(139);
|
||||
}
|
||||
}
|
||||
|
||||
/// The POSIX SIGSEGV/SIGBUS signal handler function
|
||||
fn handleSegvSignal(_: i32, info: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
|
||||
// Get the fault address - access differs by platform
|
||||
const fault_addr: usize = getFaultAddress(info);
|
||||
|
||||
// Get the current stack pointer to help determine if this is a stack overflow
|
||||
var current_sp: usize = 0;
|
||||
asm volatile (""
|
||||
: [sp] "={sp}" (current_sp),
|
||||
);
|
||||
|
||||
// A stack overflow typically occurs when the fault address is near the stack pointer
|
||||
// or below the stack (stacks grow downward on most architectures)
|
||||
const likely_stack_overflow = isLikelyStackOverflow(fault_addr, current_sp);
|
||||
|
||||
if (likely_stack_overflow) {
|
||||
if (stack_overflow_callback) |callback| {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
if (access_violation_callback) |callback| {
|
||||
callback(fault_addr);
|
||||
}
|
||||
}
|
||||
|
||||
// If no callback was set, exit with appropriate code
|
||||
if (likely_stack_overflow) {
|
||||
posix.exit(134); // 128 + 6 (SIGABRT-like)
|
||||
} else {
|
||||
posix.exit(139); // 128 + 11 (SIGSEGV)
|
||||
}
|
||||
}
|
||||
|
||||
/// The POSIX SIGFPE signal handler function (division by zero, etc.)
|
||||
fn handleFpeSignal(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
|
||||
if (arithmetic_error_callback) |callback| {
|
||||
callback();
|
||||
}
|
||||
|
||||
// If no callback was set, exit with SIGFPE code
|
||||
posix.exit(136); // 128 + 8 (SIGFPE)
|
||||
}
|
||||
|
||||
/// Get the fault address from siginfo_t (platform-specific)
|
||||
fn getFaultAddress(info: *const posix.siginfo_t) usize {
|
||||
// The siginfo_t structure varies by platform
|
||||
if (comptime builtin.os.tag == .linux) {
|
||||
// Linux: fault address is in fields.sigfault.addr
|
||||
return @intFromPtr(info.fields.sigfault.addr);
|
||||
} else if (comptime builtin.os.tag == .macos or
|
||||
builtin.os.tag == .ios or
|
||||
builtin.os.tag == .tvos or
|
||||
builtin.os.tag == .watchos or
|
||||
builtin.os.tag == .visionos or
|
||||
builtin.os.tag == .freebsd or
|
||||
builtin.os.tag == .dragonfly or
|
||||
builtin.os.tag == .netbsd or
|
||||
builtin.os.tag == .openbsd)
|
||||
{
|
||||
// macOS/iOS/BSD: fault address is in addr field
|
||||
return @intFromPtr(info.addr);
|
||||
} else {
|
||||
// Fallback: return 0 if we can't determine the address
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Heuristic to determine if a fault is likely a stack overflow
|
||||
fn isLikelyStackOverflow(fault_addr: usize, current_sp: usize) bool {
|
||||
// If fault address is 0 or very low, it's likely a null pointer dereference
|
||||
if (fault_addr < 4096) return false;
|
||||
|
||||
// If the fault address is close to the current stack pointer (within 16MB),
|
||||
// it's very likely a stack overflow. The signal handler runs on an alternate
|
||||
// stack, but the fault address should still be near where the stack was.
|
||||
const sp_distance = if (fault_addr < current_sp) current_sp - fault_addr else fault_addr - current_sp;
|
||||
if (sp_distance < 16 * 1024 * 1024) { // Within 16MB of stack pointer
|
||||
return true;
|
||||
}
|
||||
|
||||
// On 64-bit systems, stacks are typically placed in high memory.
|
||||
// On macOS, the stack is around 0x16XXXXXXXX (about 6GB mark).
|
||||
// On Linux, it's typically near 0x7FFFFFFFFFFF.
|
||||
// If the fault address is in the upper half of the address space,
|
||||
// it's more likely to be a stack-related issue.
|
||||
if (comptime @sizeOf(usize) == 8) {
|
||||
// 64-bit: check if address is in upper portion of address space
|
||||
// On macOS, stacks start around 0x100000000 (4GB) and go up
|
||||
// On Linux, stacks are near 0x7FFFFFFFFFFF
|
||||
const lower_bound: usize = 0x100000000; // 4GB
|
||||
if (fault_addr > lower_bound) {
|
||||
// This is in the region where stacks typically are on 64-bit systems
|
||||
// Default to assuming it's a stack overflow for addresses in this range
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// 32-bit: stacks are typically in the upper portion of the 4GB space
|
||||
const lower_bound: usize = 0x40000000; // 1GB
|
||||
if (fault_addr > lower_bound) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Format a usize as hexadecimal (for use in callbacks)
|
||||
pub fn formatHex(value: usize, buf: []u8) []const u8 {
|
||||
const hex_chars = "0123456789abcdef";
|
||||
var i: usize = buf.len;
|
||||
|
||||
if (value == 0) {
|
||||
i -= 1;
|
||||
buf[i] = '0';
|
||||
} else {
|
||||
var v = value;
|
||||
while (v > 0 and i > 2) {
|
||||
i -= 1;
|
||||
buf[i] = hex_chars[v & 0xf];
|
||||
v >>= 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Add 0x prefix
|
||||
i -= 1;
|
||||
buf[i] = 'x';
|
||||
i -= 1;
|
||||
buf[i] = '0';
|
||||
|
||||
return buf[i..];
|
||||
}
|
||||
|
||||
test "formatHex" {
|
||||
var buf: [18]u8 = undefined;
|
||||
|
||||
const zero = formatHex(0, &buf);
|
||||
try std.testing.expectEqualStrings("0x0", zero);
|
||||
|
||||
const small = formatHex(0xff, &buf);
|
||||
try std.testing.expectEqualStrings("0xff", small);
|
||||
|
||||
const medium = formatHex(0xdeadbeef, &buf);
|
||||
try std.testing.expectEqualStrings("0xdeadbeef", medium);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ const std = @import("std");
|
|||
|
||||
pub const host_abi = @import("host_abi.zig");
|
||||
pub const dec = @import("dec.zig");
|
||||
pub const handlers = @import("handlers.zig");
|
||||
pub const hash = @import("hash.zig");
|
||||
pub const list = @import("list.zig");
|
||||
pub const num = @import("num.zig");
|
||||
|
|
@ -12,6 +13,7 @@ pub const utils = @import("utils.zig");
|
|||
|
||||
test "builtins tests" {
|
||||
std.testing.refAllDecls(@import("dec.zig"));
|
||||
std.testing.refAllDecls(@import("handlers.zig"));
|
||||
std.testing.refAllDecls(@import("hash.zig"));
|
||||
std.testing.refAllDecls(@import("host_abi.zig"));
|
||||
std.testing.refAllDecls(@import("list.zig"));
|
||||
|
|
|
|||
|
|
@ -1137,3 +1137,97 @@ test "fx platform sublist method on inferred type" {
|
|||
|
||||
try checkSuccess(run_result);
|
||||
}
|
||||
|
||||
test "fx platform runtime stack overflow" {
|
||||
// Tests that stack overflow in a running Roc program is caught and reported
|
||||
// with a helpful error message instead of crashing with a raw signal.
|
||||
//
|
||||
// The Roc program contains an infinitely recursive function that will
|
||||
// overflow the stack at runtime. Once proper stack overflow handling is
|
||||
// implemented in the host/platform, this test will pass.
|
||||
const allocator = testing.allocator;
|
||||
|
||||
const run_result = try runRoc(allocator, "test/fx/stack_overflow_runtime.roc", .{});
|
||||
defer allocator.free(run_result.stdout);
|
||||
defer allocator.free(run_result.stderr);
|
||||
|
||||
// After stack overflow handling is implemented, we expect:
|
||||
// 1. The process exits with code 134 (indicating stack overflow was caught)
|
||||
// 2. Stderr contains a helpful message about stack overflow
|
||||
switch (run_result.term) {
|
||||
.Exited => |code| {
|
||||
if (code == 134) {
|
||||
// Stack overflow was caught and handled properly
|
||||
// Verify the helpful error message was printed
|
||||
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "overflowed its stack memory") != null);
|
||||
} else if (code == 139) {
|
||||
// Exit code 139 = 128 + 11 (SIGSEGV) - stack overflow was NOT handled
|
||||
// The Roc program crashed with a segfault that wasn't caught
|
||||
std.debug.print("\n", .{});
|
||||
std.debug.print("Stack overflow handling NOT YET IMPLEMENTED for Roc programs.\n", .{});
|
||||
std.debug.print("Process crashed with SIGSEGV (exit code 139).\n", .{});
|
||||
std.debug.print("Expected: exit code 134 with stack overflow message\n", .{});
|
||||
return error.StackOverflowNotHandled;
|
||||
} else {
|
||||
std.debug.print("Unexpected exit code: {}\n", .{code});
|
||||
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
|
||||
return error.UnexpectedExitCode;
|
||||
}
|
||||
},
|
||||
.Signal => |sig| {
|
||||
// Process was killed directly by a signal (likely SIGSEGV = 11).
|
||||
std.debug.print("\n", .{});
|
||||
std.debug.print("Stack overflow handling NOT YET IMPLEMENTED for Roc programs.\n", .{});
|
||||
std.debug.print("Process was killed by signal: {}\n", .{sig});
|
||||
std.debug.print("Expected: exit code 134 with stack overflow message\n", .{});
|
||||
return error.StackOverflowNotHandled;
|
||||
},
|
||||
else => {
|
||||
std.debug.print("Unexpected termination: {}\n", .{run_result.term});
|
||||
return error.UnexpectedTermination;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test "fx platform runtime division by zero" {
|
||||
// Tests that division by zero in a running Roc program is caught and reported
|
||||
// with a helpful error message instead of crashing with a raw signal.
|
||||
//
|
||||
// The error can be caught by either:
|
||||
// 1. The Roc interpreter (exit code 1, "DivisionByZero" message) - most common
|
||||
// 2. The SIGFPE signal handler (exit code 136, "divided by zero" message) - native code
|
||||
const allocator = testing.allocator;
|
||||
|
||||
// The Roc program uses a var to prevent compile-time constant folding
|
||||
const run_result = try runRoc(allocator, "test/fx/division_by_zero.roc", .{});
|
||||
defer allocator.free(run_result.stdout);
|
||||
defer allocator.free(run_result.stderr);
|
||||
|
||||
switch (run_result.term) {
|
||||
.Exited => |code| {
|
||||
if (code == 136) {
|
||||
// Division by zero was caught by the SIGFPE handler (native code)
|
||||
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "divided by zero") != null);
|
||||
} else if (code == 1) {
|
||||
// Division by zero was caught by the interpreter - this is the expected case
|
||||
// The interpreter catches it and reports "DivisionByZero"
|
||||
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "DivisionByZero") != null);
|
||||
} else {
|
||||
std.debug.print("Unexpected exit code: {}\n", .{code});
|
||||
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
|
||||
return error.UnexpectedExitCode;
|
||||
}
|
||||
},
|
||||
.Signal => |sig| {
|
||||
// Process was killed directly by a signal without being caught
|
||||
std.debug.print("\n", .{});
|
||||
std.debug.print("Division by zero was not caught!\n", .{});
|
||||
std.debug.print("Process was killed by signal: {}\n", .{sig});
|
||||
return error.DivisionByZeroNotHandled;
|
||||
},
|
||||
else => {
|
||||
std.debug.print("Unexpected termination: {}\n", .{run_result.term});
|
||||
return error.UnexpectedTermination;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1514,8 +1514,12 @@ pub const ComptimeEvaluator = struct {
|
|||
try self.reportProblem(expect_info.message, expect_info.region, .expect_failed);
|
||||
},
|
||||
.error_eval => |error_info| {
|
||||
const error_name = @errorName(error_info.err);
|
||||
try self.reportProblem(error_name, error_info.region, .error_eval);
|
||||
// Provide user-friendly messages for specific errors
|
||||
const error_message = switch (error_info.err) {
|
||||
error.DivisionByZero => "Division by zero",
|
||||
else => @errorName(error_info.err),
|
||||
};
|
||||
try self.reportProblem(error_message, error_info.region, .error_eval);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1763,3 +1763,66 @@ test "comptime eval - to_str on unbound number literal" {
|
|||
// Flex var defaults to Dec; Dec.to_str is provided by builtins
|
||||
try testing.expectEqual(@as(usize, 0), result.problems.len());
|
||||
}
|
||||
|
||||
// --- Division by zero tests ---
|
||||
|
||||
test "comptime eval - division by zero produces error" {
|
||||
const src =
|
||||
\\x = 5 // 0
|
||||
;
|
||||
|
||||
var result = try parseCheckAndEvalModule(src);
|
||||
defer cleanupEvalModule(&result);
|
||||
|
||||
const summary = try result.evaluator.evalAll();
|
||||
|
||||
// Should evaluate 1 declaration with no crashes (it's an error, not a crash)
|
||||
try testing.expectEqual(@as(u32, 1), summary.evaluated);
|
||||
try testing.expectEqual(@as(u32, 0), summary.crashed);
|
||||
|
||||
// Should have 1 problem reported (division by zero)
|
||||
try testing.expect(result.problems.len() >= 1);
|
||||
try testing.expect(errorContains(result.problems, "Division by zero"));
|
||||
}
|
||||
|
||||
test "comptime eval - division by zero in expression" {
|
||||
const src =
|
||||
\\a = 10
|
||||
\\b = 0
|
||||
\\c = a // b
|
||||
;
|
||||
|
||||
var result = try parseCheckAndEvalModule(src);
|
||||
defer cleanupEvalModule(&result);
|
||||
|
||||
const summary = try result.evaluator.evalAll();
|
||||
|
||||
// Should evaluate 3 declarations, c will cause an error
|
||||
try testing.expectEqual(@as(u32, 3), summary.evaluated);
|
||||
|
||||
// Should have 1 problem reported (division by zero)
|
||||
try testing.expect(result.problems.len() >= 1);
|
||||
try testing.expect(errorContains(result.problems, "Division by zero"));
|
||||
}
|
||||
|
||||
test "comptime eval - modulo by zero produces error" {
|
||||
const src =
|
||||
\\x = 10 % 0
|
||||
;
|
||||
|
||||
var result = try parseCheckAndEvalModule(src);
|
||||
defer cleanupEvalModule(&result);
|
||||
|
||||
const summary = try result.evaluator.evalAll();
|
||||
|
||||
// Should evaluate 1 declaration
|
||||
try testing.expectEqual(@as(u32, 1), summary.evaluated);
|
||||
|
||||
// Should have 1 problem reported (division by zero for modulo)
|
||||
try testing.expect(result.problems.len() >= 1);
|
||||
try testing.expect(errorContains(result.problems, "Division by zero"));
|
||||
}
|
||||
|
||||
// Note: "division by zero does not halt other defs" test is skipped because
|
||||
// the interpreter state after an eval error may not allow continuing evaluation
|
||||
// of subsequent definitions that share the same evaluation context.
|
||||
|
|
|
|||
14
test/fx/division_by_zero.roc
Normal file
14
test/fx/division_by_zero.roc
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
app [main!] { pf: platform "./platform/main.roc" }
|
||||
|
||||
import pf.Stdout
|
||||
|
||||
# Use a mutable variable to prevent compile-time evaluation
|
||||
main! = || {
|
||||
# The var keyword creates a runtime variable that can't be constant-folded
|
||||
var $divisor = 0
|
||||
|
||||
# This will trigger a division by zero error at runtime
|
||||
result = 42 / $divisor
|
||||
|
||||
Stdout.line!("Result: ${U64.to_str(result)}")
|
||||
}
|
||||
|
|
@ -26,11 +26,106 @@
|
|||
//! - 0: All expectations matched in order
|
||||
//! - 1: Test failed (mismatch, missing output, extra output, or invalid spec)
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const builtins = @import("builtins");
|
||||
const build_options = @import("build_options");
|
||||
const posix = if (builtin.os.tag != .windows and builtin.os.tag != .wasi) std.posix else undefined;
|
||||
|
||||
const trace_refcount = build_options.trace_refcount;
|
||||
|
||||
/// Error message to display on stack overflow in a Roc program
|
||||
const STACK_OVERFLOW_MESSAGE = "\nThis Roc application overflowed its stack memory and crashed.\n\n";
|
||||
|
||||
/// Callback for stack overflow in a Roc program
|
||||
fn handleRocStackOverflow() noreturn {
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
const DWORD = u32;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
|
||||
const kernel32 = struct {
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
};
|
||||
|
||||
const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE);
|
||||
var bytes_written: DWORD = 0;
|
||||
_ = kernel32.WriteFile(stderr_handle, STACK_OVERFLOW_MESSAGE.ptr, STACK_OVERFLOW_MESSAGE.len, &bytes_written, null);
|
||||
kernel32.ExitProcess(134);
|
||||
} else if (comptime builtin.os.tag != .wasi) {
|
||||
_ = posix.write(posix.STDERR_FILENO, STACK_OVERFLOW_MESSAGE) catch {};
|
||||
posix.exit(134);
|
||||
} else {
|
||||
std.process.exit(134);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback for access violation in a Roc program
|
||||
fn handleRocAccessViolation(fault_addr: usize) noreturn {
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
const DWORD = u32;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
|
||||
const kernel32 = struct {
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
};
|
||||
|
||||
var addr_buf: [18]u8 = undefined;
|
||||
const addr_str = builtins.handlers.formatHex(fault_addr, &addr_buf);
|
||||
|
||||
const msg1 = "\nSegmentation fault (SIGSEGV) in this Roc program.\nFault address: ";
|
||||
const msg2 = "\n\n";
|
||||
const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE);
|
||||
var bytes_written: DWORD = 0;
|
||||
_ = kernel32.WriteFile(stderr_handle, msg1.ptr, msg1.len, &bytes_written, null);
|
||||
_ = kernel32.WriteFile(stderr_handle, addr_str.ptr, @intCast(addr_str.len), &bytes_written, null);
|
||||
_ = kernel32.WriteFile(stderr_handle, msg2.ptr, msg2.len, &bytes_written, null);
|
||||
kernel32.ExitProcess(139);
|
||||
} else {
|
||||
// POSIX (and WASI fallback)
|
||||
const msg = "\nSegmentation fault (SIGSEGV) in this Roc program.\nFault address: ";
|
||||
_ = posix.write(posix.STDERR_FILENO, msg) catch {};
|
||||
|
||||
var addr_buf: [18]u8 = undefined;
|
||||
const addr_str = builtins.handlers.formatHex(fault_addr, &addr_buf);
|
||||
_ = posix.write(posix.STDERR_FILENO, addr_str) catch {};
|
||||
_ = posix.write(posix.STDERR_FILENO, "\n\n") catch {};
|
||||
posix.exit(139);
|
||||
}
|
||||
}
|
||||
|
||||
/// Error message to display on division by zero in a Roc program
|
||||
const DIVISION_BY_ZERO_MESSAGE = "\nThis Roc application divided by zero and crashed.\n\n";
|
||||
|
||||
/// Callback for arithmetic errors (division by zero) in a Roc program
|
||||
fn handleRocArithmeticError() noreturn {
|
||||
if (comptime builtin.os.tag == .windows) {
|
||||
const DWORD = u32;
|
||||
const HANDLE = ?*anyopaque;
|
||||
const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12));
|
||||
|
||||
const kernel32 = struct {
|
||||
extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE;
|
||||
extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32;
|
||||
extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn;
|
||||
};
|
||||
|
||||
const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE);
|
||||
var bytes_written: DWORD = 0;
|
||||
_ = kernel32.WriteFile(stderr_handle, DIVISION_BY_ZERO_MESSAGE.ptr, DIVISION_BY_ZERO_MESSAGE.len, &bytes_written, null);
|
||||
kernel32.ExitProcess(136);
|
||||
} else if (comptime builtin.os.tag != .wasi) {
|
||||
_ = posix.write(posix.STDERR_FILENO, DIVISION_BY_ZERO_MESSAGE) catch {};
|
||||
posix.exit(136); // 128 + 8 (SIGFPE)
|
||||
} else {
|
||||
std.process.exit(136);
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of IO operation in test spec
|
||||
const EffectType = enum(u8) {
|
||||
stdin_input, // 0<
|
||||
|
|
@ -128,6 +223,7 @@ fn parseTestSpec(allocator: std.mem.Allocator, spec: []const u8) ParseError![]Sp
|
|||
return entries.toOwnedSlice(allocator) catch ParseError.OutOfMemory;
|
||||
}
|
||||
|
||||
|
||||
/// Host environment - contains GeneralPurposeAllocator for leak detection
|
||||
const HostEnv = struct {
|
||||
gpa: std.heap.GeneralPurposeAllocator(.{}),
|
||||
|
|
@ -578,6 +674,10 @@ const hosted_function_ptrs = [_]builtins.host_abi.HostedFn{
|
|||
|
||||
/// Platform host entrypoint
|
||||
fn platform_main(test_spec: ?[]const u8, test_verbose: bool) !c_int {
|
||||
// Install signal handlers for stack overflow, access violations, and division by zero
|
||||
// This allows us to display helpful error messages instead of crashing
|
||||
_ = builtins.handlers.install(handleRocStackOverflow, handleRocAccessViolation, handleRocArithmeticError);
|
||||
|
||||
var host_env = HostEnv{
|
||||
.gpa = std.heap.GeneralPurposeAllocator(.{}){},
|
||||
.test_state = TestState.init(),
|
||||
|
|
|
|||
16
test/fx/stack_overflow_runtime.roc
Normal file
16
test/fx/stack_overflow_runtime.roc
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
app [main!] { pf: platform "./platform/main.roc" }
|
||||
|
||||
import pf.Stdout
|
||||
|
||||
# This function causes infinite recursion, leading to stack overflow at runtime.
|
||||
# It cannot be tail-call optimized because there's work after the recursive call.
|
||||
overflow : I64 -> I64
|
||||
overflow = |n|
|
||||
# Prevent tail-call optimization by adding to the result after recursion
|
||||
overflow(n + 1) + 1
|
||||
|
||||
main! = || {
|
||||
# This will overflow the stack at runtime
|
||||
result = overflow(0)
|
||||
Stdout.line!("Result: ${I64.to_str(result)}")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue