From 278656943f0ee96900efa7890690fbbb90b03741 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 09:39:31 -0500 Subject: [PATCH 1/8] Test for stack overflows in roc programs --- src/cli/test/fx_platform_test.zig | 51 ++++++++++++++++++++++++++++++ test/fx/stack_overflow_runtime.roc | 16 ++++++++++ 2 files changed, 67 insertions(+) create mode 100644 test/fx/stack_overflow_runtime.roc diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index b759b86e42..fd2ff08ed5 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -1582,3 +1582,54 @@ 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 "STACK OVERFLOW" message + 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, "STACK OVERFLOW") != 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; + }, + } +} diff --git a/test/fx/stack_overflow_runtime.roc b/test/fx/stack_overflow_runtime.roc new file mode 100644 index 0000000000..bc72921a3a --- /dev/null +++ b/test/fx/stack_overflow_runtime.roc @@ -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)}") +} From 7faec88e29c16515874ce9b05db2be88edf4d232 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 10:00:37 -0500 Subject: [PATCH 2/8] Add stack overflow handling for roc programs --- src/base/stack_overflow.zig | 338 ++++++++---------------------------- src/build/modules.zig | 2 +- src/builtins/handlers.zig | 300 ++++++++++++++++++++++++++++++++ src/builtins/mod.zig | 2 + test/fx/platform/host.zig | 86 +++++++++ 5 files changed, 462 insertions(+), 266 deletions(-) create mode 100644 src/builtins/handlers.zig diff --git a/src/base/stack_overflow.zig b/src/base/stack_overflow.zig index d501d0601a..1650196d4b 100644 --- a/src/base/stack_overflow.zig +++ b/src/base/stack_overflow.zig @@ -1,8 +1,7 @@ //! Stack overflow detection and handling for the Roc compiler. //! -//! 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. @@ -13,58 +12,9 @@ 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 = \\ @@ -89,219 +39,77 @@ const STACK_OVERFLOW_MESSAGE = \\ ; +/// Callback for stack overflow in the compiler +fn handleStackOverflow() noreturn { + if (comptime builtin.os.tag == .windows) { + // Windows: use WriteFile for signal-safe output + 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: 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); + } +} + +/// 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 = 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); + } +} + /// 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; - - 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 - 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: ", - }; - _ = posix.write(stderr_fd, 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) - } -} - -/// 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..]; + return handlers.install(handleStackOverflow, handleAccessViolation); } /// Test function that intentionally causes a stack overflow. @@ -330,13 +138,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); } diff --git a/src/build/modules.zig b/src/build/modules.zig index 3e621ab6af..1fe8d4a854 100644 --- a/src/build/modules.zig +++ b/src/build/modules.zig @@ -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 }, diff --git a/src/builtins/handlers.zig b/src/builtins/handlers.zig new file mode 100644 index 0000000000..705964ac98 --- /dev/null +++ b/src/builtins/handlers.zig @@ -0,0 +1,300 @@ +//! Generic signal handlers for stack overflow and access violation detection. +//! +//! This module provides a mechanism to catch stack overflows and access violations +//! and handle 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 a SIGSEGV handler that detects stack overflows. +//! +//! On Windows, we use SetUnhandledExceptionFilter to catch EXCEPTION_STACK_OVERFLOW. +//! +//! 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_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; + +/// Stored callbacks (set during install) +var stack_overflow_callback: ?StackOverflowCallback = null; +var access_violation_callback: ?AccessViolationCallback = 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. +/// +/// Returns true if the handlers were installed successfully, false otherwise. +pub fn install(on_stack_overflow: StackOverflowCallback, on_access_violation: AccessViolationCallback) bool { + if (handler_installed) return true; + + stack_overflow_callback = on_stack_overflow; + access_violation_callback = on_access_violation; + + 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 + 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; + } + + if (is_stack_overflow) { + if (stack_overflow_callback) |callback| { + callback(); + } + } 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); + } + } + + // If no callback was set, 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(_: 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) + } +} + +/// 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); +} diff --git a/src/builtins/mod.zig b/src/builtins/mod.zig index a670440110..e2c08edaa7 100644 --- a/src/builtins/mod.zig +++ b/src/builtins/mod.zig @@ -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")); diff --git a/test/fx/platform/host.zig b/test/fx/platform/host.zig index 2b3b30bf95..b0352f7c58 100644 --- a/test/fx/platform/host.zig +++ b/test/fx/platform/host.zig @@ -1,10 +1,92 @@ ///! Platform host that tests effectful functions writing to stdout and stderr. 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 = + \\ + \\================================================================================ + \\STACK OVERFLOW in this Roc program + \\================================================================================ + \\ + \\This Roc program ran out of stack space. This can happen with: + \\ - Infinite recursion (a function that calls itself without stopping) + \\ - Very deeply nested function calls + \\ + \\Check your code for functions that might recurse infinitely. + \\ + \\================================================================================ + \\ + \\ +; + +/// 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); + } +} + /// Host environment - contains GeneralPurposeAllocator for leak detection const HostEnv = struct { gpa: std.heap.GeneralPurposeAllocator(.{}), @@ -264,6 +346,10 @@ const hosted_function_ptrs = [_]builtins.host_abi.HostedFn{ /// Platform host entrypoint fn platform_main() !void { + // Install signal handlers for stack overflow and access violations + // This allows us to display helpful error messages instead of crashing + _ = builtins.handlers.install(handleRocStackOverflow, handleRocAccessViolation); + var host_env = HostEnv{ .gpa = std.heap.GeneralPurposeAllocator(.{}){}, }; From e3e9b2b135373bfd8d64e435a0fc7cb3517bd8b6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 10:15:30 -0500 Subject: [PATCH 3/8] Add div by 0 checks --- src/base/stack_overflow.zig | 56 ++++++++++++++++++++--- src/builtins/handlers.zig | 76 +++++++++++++++++++++++-------- src/cli/test/fx_platform_test.zig | 43 +++++++++++++++++ test/fx/division_by_zero.roc | 14 ++++++ test/fx/platform/host.zig | 45 +++++++++++++++++- 5 files changed, 207 insertions(+), 27 deletions(-) create mode 100644 test/fx/division_by_zero.roc diff --git a/src/base/stack_overflow.zig b/src/base/stack_overflow.zig index 1650196d4b..a0a55e0f39 100644 --- a/src/base/stack_overflow.zig +++ b/src/base/stack_overflow.zig @@ -1,12 +1,12 @@ -//! 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 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). @@ -67,6 +67,50 @@ fn handleStackOverflow() noreturn { } } +/// Error message to display on arithmetic error (division by zero, etc.) +const ARITHMETIC_ERROR_MESSAGE = + \\ + \\================================================================================ + \\ARITHMETIC ERROR in the Roc compiler + \\================================================================================ + \\ + \\The Roc compiler encountered an arithmetic error (likely division by zero). + \\This is a bug in the compiler, not in your code. + \\ + \\Please report this issue at: https://github.com/roc-lang/roc/issues + \\ + \\Include the Roc code that triggered this error if possible. + \\ + \\================================================================================ + \\ + \\ +; + +/// 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) { @@ -105,11 +149,11 @@ fn handleAccessViolation(fault_addr: usize) noreturn { } } -/// Install the stack overflow handler. +/// 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 handler was installed successfully, false otherwise. +/// Returns true if the handlers were installed successfully, false otherwise. pub fn install() bool { - return handlers.install(handleStackOverflow, handleAccessViolation); + return handlers.install(handleStackOverflow, handleAccessViolation, handleArithmeticError); } /// Test function that intentionally causes a stack overflow. diff --git a/src/builtins/handlers.zig b/src/builtins/handlers.zig index 705964ac98..41a29a7eb6 100644 --- a/src/builtins/handlers.zig +++ b/src/builtins/handlers.zig @@ -1,12 +1,13 @@ -//! Generic signal handlers for stack overflow and access violation detection. +//! Generic signal handlers for stack overflow, access violation, and arithmetic errors. //! -//! This module provides a mechanism to catch stack overflows and access violations -//! and handle them with custom callbacks instead of crashing with a raw signal. +//! 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 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). @@ -24,6 +25,8 @@ 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)); @@ -70,9 +73,13 @@ 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. /// @@ -80,13 +87,19 @@ var access_violation_callback: ?AccessViolationCallback = null; /// - 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) bool { +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(); @@ -112,17 +125,26 @@ fn installPosix() bool { return false; }; - // Install the SIGSEGV handler - const action = posix.Sigaction{ - .handler = .{ .sigaction = handleSignalPosix }, + // 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, &action, null); + 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, &action, null); + 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; @@ -138,11 +160,14 @@ fn installWindows() bool { 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 + // 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) { + if (!is_stack_overflow and !is_access_violation and !is_arithmetic_error) { // Let other handlers deal with this exception return EXCEPTION_CONTINUE_SEARCH; } @@ -151,21 +176,24 @@ fn handleExceptionWindows(exception_info: *EXCEPTION_POINTERS) callconv(.winapi) 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); } - - // If no callback was set, 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(_: i32, info: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void { +/// 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); @@ -197,6 +225,16 @@ fn handleSignalPosix(_: i32, info: *const posix.siginfo_t, _: ?*anyopaque) callc } } +/// 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 diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index fd2ff08ed5..3c9579c7d5 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -1633,3 +1633,46 @@ test "fx platform runtime stack overflow" { }, } } + +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, "DIVISION 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, "DIVISION 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; + }, + } +} diff --git a/test/fx/division_by_zero.roc b/test/fx/division_by_zero.roc new file mode 100644 index 0000000000..bcc398fcd1 --- /dev/null +++ b/test/fx/division_by_zero.roc @@ -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)}") +} diff --git a/test/fx/platform/host.zig b/test/fx/platform/host.zig index b0352f7c58..476c731f53 100644 --- a/test/fx/platform/host.zig +++ b/test/fx/platform/host.zig @@ -87,6 +87,47 @@ fn handleRocAccessViolation(fault_addr: usize) noreturn { } } +/// Error message to display on division by zero in a Roc program +const DIVISION_BY_ZERO_MESSAGE = + \\ + \\================================================================================ + \\DIVISION BY ZERO in this Roc program + \\================================================================================ + \\ + \\This Roc program attempted to divide by zero. + \\ + \\Check your code for places where a divisor might be zero. + \\ + \\================================================================================ + \\ + \\ +; + +/// 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); + } +} + /// Host environment - contains GeneralPurposeAllocator for leak detection const HostEnv = struct { gpa: std.heap.GeneralPurposeAllocator(.{}), @@ -346,9 +387,9 @@ const hosted_function_ptrs = [_]builtins.host_abi.HostedFn{ /// Platform host entrypoint fn platform_main() !void { - // Install signal handlers for stack overflow and access violations + // 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); + _ = builtins.handlers.install(handleRocStackOverflow, handleRocAccessViolation, handleRocArithmeticError); var host_env = HostEnv{ .gpa = std.heap.GeneralPurposeAllocator(.{}){}, From c2d313dece1af23a2e492bdf3c5a1bd4eefb7c0e Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 11:46:51 -0500 Subject: [PATCH 4/8] Improve comptime evaluator --- src/eval/comptime_evaluator.zig | 8 +++- src/eval/test/comptime_eval_test.zig | 63 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/eval/comptime_evaluator.zig b/src/eval/comptime_evaluator.zig index 1c29f677ed..10686f1470 100644 --- a/src/eval/comptime_evaluator.zig +++ b/src/eval/comptime_evaluator.zig @@ -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); }, } } diff --git a/src/eval/test/comptime_eval_test.zig b/src/eval/test/comptime_eval_test.zig index e174a78e6e..982fece6cd 100644 --- a/src/eval/test/comptime_eval_test.zig +++ b/src/eval/test/comptime_eval_test.zig @@ -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. From c2a33a8313f4e9ea644f78e0a9e15f194f167906 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 11:56:11 -0500 Subject: [PATCH 5/8] Revise stack overflow error message --- src/base/stack_overflow.zig | 27 ++------------------------- src/cli/test/fx_platform_test.zig | 8 ++++---- test/fx/platform/host.zig | 17 +---------------- 3 files changed, 7 insertions(+), 45 deletions(-) diff --git a/src/base/stack_overflow.zig b/src/base/stack_overflow.zig index a0a55e0f39..31f2da791d 100644 --- a/src/base/stack_overflow.zig +++ b/src/base/stack_overflow.zig @@ -16,28 +16,7 @@ const handlers = @import("builtins").handlers; const posix = if (builtin.os.tag != .windows and builtin.os.tag != .wasi) std.posix else undefined; /// 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. - \\ - \\================================================================================ - \\ - \\ -; +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 { @@ -282,13 +261,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. diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 3c9579c7d5..526f13d7dc 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -1598,20 +1598,20 @@ test "fx platform runtime stack overflow" { // 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 "STACK OVERFLOW" message + // 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, "STACK OVERFLOW") != null); + 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", .{}); + std.debug.print("Expected: exit code 134 with stack overflow message\n", .{}); return error.StackOverflowNotHandled; } else { std.debug.print("Unexpected exit code: {}\n", .{code}); @@ -1624,7 +1624,7 @@ test "fx platform runtime stack overflow" { 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", .{}); + std.debug.print("Expected: exit code 134 with stack overflow message\n", .{}); return error.StackOverflowNotHandled; }, else => { diff --git a/test/fx/platform/host.zig b/test/fx/platform/host.zig index 476c731f53..e230b0733f 100644 --- a/test/fx/platform/host.zig +++ b/test/fx/platform/host.zig @@ -8,22 +8,7 @@ const posix = if (builtin.os.tag != .windows and builtin.os.tag != .wasi) std.po const trace_refcount = build_options.trace_refcount; /// Error message to display on stack overflow in a Roc program -const STACK_OVERFLOW_MESSAGE = - \\ - \\================================================================================ - \\STACK OVERFLOW in this Roc program - \\================================================================================ - \\ - \\This Roc program ran out of stack space. This can happen with: - \\ - Infinite recursion (a function that calls itself without stopping) - \\ - Very deeply nested function calls - \\ - \\Check your code for functions that might recurse infinitely. - \\ - \\================================================================================ - \\ - \\ -; +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 { From b2034b944bdc7ad1de5d6d862611edd4ef80e2ab Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 12:15:56 -0500 Subject: [PATCH 6/8] Revise some error messages --- src/base/stack_overflow.zig | 18 +----------------- src/cli/test/fx_platform_test.zig | 4 ++-- test/fx/platform/host.zig | 15 +-------------- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/base/stack_overflow.zig b/src/base/stack_overflow.zig index 31f2da791d..20a818099f 100644 --- a/src/base/stack_overflow.zig +++ b/src/base/stack_overflow.zig @@ -47,23 +47,7 @@ fn handleStackOverflow() noreturn { } /// Error message to display on arithmetic error (division by zero, etc.) -const ARITHMETIC_ERROR_MESSAGE = - \\ - \\================================================================================ - \\ARITHMETIC ERROR in the Roc compiler - \\================================================================================ - \\ - \\The Roc compiler encountered an arithmetic error (likely division by zero). - \\This is a bug in the compiler, not in your code. - \\ - \\Please report this issue at: https://github.com/roc-lang/roc/issues - \\ - \\Include the Roc code that triggered this error if possible. - \\ - \\================================================================================ - \\ - \\ -; +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 { diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 526f13d7dc..e43c7aba8f 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -1640,7 +1640,7 @@ test "fx platform runtime division by zero" { // // 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, "DIVISION BY ZERO" message) - native code + // 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 @@ -1652,7 +1652,7 @@ test "fx platform runtime division by zero" { .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, "DIVISION BY ZERO") != null); + 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" diff --git a/test/fx/platform/host.zig b/test/fx/platform/host.zig index e230b0733f..f00f57abce 100644 --- a/test/fx/platform/host.zig +++ b/test/fx/platform/host.zig @@ -73,20 +73,7 @@ fn handleRocAccessViolation(fault_addr: usize) noreturn { } /// Error message to display on division by zero in a Roc program -const DIVISION_BY_ZERO_MESSAGE = - \\ - \\================================================================================ - \\DIVISION BY ZERO in this Roc program - \\================================================================================ - \\ - \\This Roc program attempted to divide by zero. - \\ - \\Check your code for places where a divisor might be zero. - \\ - \\================================================================================ - \\ - \\ -; +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 { From 3934fca617c9f1cc88d1ade9f238521c17d681c3 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 6 Dec 2025 13:07:41 -0500 Subject: [PATCH 7/8] Fix tests --- test/snapshots/repl/try_is_eq.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/snapshots/repl/try_is_eq.md b/test/snapshots/repl/try_is_eq.md index 195a05f80a..ac1d26f9f5 100644 --- a/test/snapshots/repl/try_is_eq.md +++ b/test/snapshots/repl/try_is_eq.md @@ -7,10 +7,16 @@ type=repl ~~~roc » Try.Ok(1) == Try.Ok(1) » Try.Ok(1) == Try.Ok(2) +» Try.Ok(1) != Try.Ok(1) +» Try.Ok(1) != Try.Ok(2) ~~~ # OUTPUT -Crash: e_closure: failed to resolve capture value +True --- -Crash: e_closure: failed to resolve capture value +False +--- +False +--- +True # PROBLEMS NIL From fb742d2ecd90723d7e195fca42941ccb44392670 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sun, 7 Dec 2025 17:34:12 -0500 Subject: [PATCH 8/8] Revert a test for now --- test/snapshots/repl/try_is_eq.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/snapshots/repl/try_is_eq.md b/test/snapshots/repl/try_is_eq.md index ac1d26f9f5..195a05f80a 100644 --- a/test/snapshots/repl/try_is_eq.md +++ b/test/snapshots/repl/try_is_eq.md @@ -7,16 +7,10 @@ type=repl ~~~roc » Try.Ok(1) == Try.Ok(1) » Try.Ok(1) == Try.Ok(2) -» Try.Ok(1) != Try.Ok(1) -» Try.Ok(1) != Try.Ok(2) ~~~ # OUTPUT -True +Crash: e_closure: failed to resolve capture value --- -False ---- -False ---- -True +Crash: e_closure: failed to resolve capture value # PROBLEMS NIL