Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add crash handler on Windows #11570

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion spec/std/exception/call_stack_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe "Backtrace" do
error.to_s.should contain("IndexError")
end

pending_win32 "prints crash backtrace to stderr" do
it "prints crash backtrace to stderr" do
sample = datapath("crash_backtrace_sample")

_, output, error = compile_and_run_file(sample)
Expand Down
18 changes: 8 additions & 10 deletions spec/std/exception_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,14 @@ describe "Exception" do
ex.inspect_with_backtrace.should contain("inner")
end

{% unless flag?(:win32) %}
it "collect memory within ensure block" do
sample = datapath("collect_within_ensure")
it "collect memory within ensure block" do
sample = datapath("collect_within_ensure")

_, output, error = compile_and_run_file(sample, ["--release"])
_, output, error = compile_and_run_file(sample, ["--release"])

output.to_s.empty?.should be_true
error.to_s.should contain("Unhandled exception: Oh no! (Exception)")
error.to_s.should_not contain("Invalid memory access")
error.to_s.should_not contain("Illegal instruction")
end
{% end %}
output.to_s.empty?.should be_true
error.to_s.should contain("Unhandled exception: Oh no! (Exception)")
error.to_s.should_not contain("Invalid memory access")
error.to_s.should_not contain("Illegal instruction")
end
end
10 changes: 5 additions & 5 deletions spec/std/kernel_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ describe "at_exit" do
end
end

pending_win32 describe: "seg fault" do
it "reports SIGSEGV" do
describe "hardware exception" do
it "reports invalid memory access" do
status, _, error = compile_and_run_source <<-'CODE'
puts Pointer(Int64).null.value
CODE
Expand All @@ -232,7 +232,7 @@ pending_win32 describe: "seg fault" do
# will address this.
status, _, error = compile_and_run_source <<-'CODE'
def foo
y = StaticArray(Int8,512).new(0)
y = StaticArray(Int8, 512).new(0)
foo
end
foo
Expand All @@ -243,10 +243,10 @@ pending_win32 describe: "seg fault" do
end
{% end %}

it "detects stack overflow on a fiber stack" do
pending_win32 "detects stack overflow on a fiber stack" do
status, _, error = compile_and_run_source <<-'CODE'
def foo
y = StaticArray(Int8,512).new(0)
y = StaticArray(Int8, 512).new(0)
foo
end

Expand Down
4 changes: 4 additions & 0 deletions src/exception/call_stack/libunwind.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ struct Exception::CallStack
end
{% end %}

def self.setup_crash_handler
Signal.setup_segfault_handler
end

{% if flag?(:interpreted) %} @[Primitive(:interpreter_call_stack_unwind)] {% end %}
protected def self.unwind : Array(Void*)
callstack = [] of Void*
Expand Down
171 changes: 148 additions & 23 deletions src/exception/call_stack/stackwalk.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "c/dbghelp"
require "c/malloc"

# :nodoc:
struct Exception::CallStack
Expand Down Expand Up @@ -31,7 +32,49 @@ struct Exception::CallStack
LibC.SymSetOptions(LibC.SymGetOptions | LibC::SYMOPT_UNDNAME | LibC::SYMOPT_LOAD_LINES | LibC::SYMOPT_FAIL_CRITICAL_ERRORS | LibC::SYMOPT_NO_PROMPTS)
end

def self.unwind
def self.setup_crash_handler
LibC.AddVectoredExceptionHandler(1, ->(exception_info) do
case status = exception_info.value.exceptionRecord.value.exceptionCode
when LibC::EXCEPTION_ACCESS_VIOLATION
addr = exception_info.value.exceptionRecord.value.exceptionInformation[1]
Crystal::System.print_error "Invalid memory access (C0000005) at address 0x%llx\n", addr
print_backtrace(exception_info)
LibC._exit(1)
when LibC::EXCEPTION_STACK_OVERFLOW
LibC._resetstkoflw
Crystal::System.print_error "Stack overflow (e.g., infinite or very deep recursion)\n"
print_backtrace(exception_info)
LibC._exit(1)
else
LibC::EXCEPTION_CONTINUE_SEARCH
end
end)

# ensure that even in the case of stack overflow there is enough reserved
# stack space for recovery
stack_size = LibC::DWORD.new!(0x10000)
LibC.SetThreadStackGuarantee(pointerof(stack_size))
end

{% if flag?(:interpreted) %} @[Primitive(:interpreter_call_stack_unwind)] {% end %}
protected def self.unwind : Array(Void*)
# TODO: use stack if possible (must be 16-byte aligned)
context = Pointer(LibC::CONTEXT).malloc(1)
context.value.contextFlags = LibC::CONTEXT_FULL
LibC.RtlCaptureContext(context)

stack = [] of Void*
each_frame(context) do |frame|
(frame.count + 1).times do
stack << frame.ip
end
end
stack
end

private def self.each_frame(context, &)
# unlike DWARF, this is required on Windows to even be able to produce
# correct stack traces, so we do it here but not in `libunwind.cr`
load_debug_info

machine_type = {% if flag?(:x86_64) %}
Expand All @@ -43,11 +86,6 @@ struct Exception::CallStack
{% raise "architecture not supported" %}
{% end %}

# TODO: use stack if possible (must be 16-byte aligned)
context = Pointer(LibC::CONTEXT).malloc(1)
context.value.contextFlags = LibC::CONTEXT_FULL
LibC.RtlCaptureContext(context)

stack_frame = LibC::STACKFRAME64.new
stack_frame.addrPC.mode = LibC::ADDRESS_MODE::AddrModeFlat
stack_frame.addrFrame.mode = LibC::ADDRESS_MODE::AddrModeFlat
Expand All @@ -57,13 +95,15 @@ struct Exception::CallStack
stack_frame.addrFrame.offset = context.value.rbp
stack_frame.addrStack.offset = context.value.rsp

stack = [] of Void*
last_frame = nil
cur_proc = LibC.GetCurrentProcess
cur_thread = LibC.GetCurrentThread

while true
ret = LibC.StackWalk64(
machine_type,
LibC.GetCurrentProcess,
LibC.GetCurrentThread,
cur_proc,
cur_thread,
pointerof(stack_frame),
context,
nil,
Expand All @@ -72,10 +112,70 @@ struct Exception::CallStack
nil
)
break if ret == 0
stack << Pointer(Void).new(stack_frame.addrPC.offset)

ip = Pointer(Void).new(stack_frame.addrPC.offset)
if last_frame
if ip != last_frame.ip
yield last_frame
last_frame = RepeatedFrame.new(ip)
else
last_frame.incr
end
else
last_frame = RepeatedFrame.new(ip)
end
end

stack
yield last_frame if last_frame
end

struct RepeatedFrame
getter ip : Void*, count : Int32

def initialize(@ip : Void*)
@count = 0
end

def incr
@count += 1
end
end

private record StackContext, context : LibC::CONTEXT*, thread : LibC::HANDLE

def self.print_backtrace(exception_info) : Nil
each_frame(exception_info.value.contextRecord) do |frame|
print_frame(frame)
end
end

private def self.print_frame(repeated_frame)
if name = decode_function_name(repeated_frame.ip.address)
file, line, _ = decode_line_number(repeated_frame.ip.address)
if file != "??" && line != 0
if repeated_frame.count == 0
Crystal::System.print_error "[0x%llx] %s at %s:%ld\n", repeated_frame.ip, name, file, line
else
Crystal::System.print_error "[0x%llx] %s at %s:%ld (%ld times)\n", repeated_frame.ip, name, file, line, repeated_frame.count + 1
end
return
end
end

if frame = decode_frame(repeated_frame.ip)
offset, sname, fname = frame
if repeated_frame.count == 0
Crystal::System.print_error "[0x%llx] %s +%lld in %s\n", repeated_frame.ip, sname, offset, fname
else
Crystal::System.print_error "[0x%llx] %s +%lld in %s (%ld times)\n", repeated_frame.ip, sname, offset, fname, repeated_frame.count + 1
end
else
if repeated_frame.count == 0
Crystal::System.print_error "[0x%llx] ???\n", repeated_frame.ip
else
Crystal::System.print_error "[0x%llx] ??? (%ld times)\n", repeated_frame.ip, repeated_frame.count + 1
end
end
end

protected def self.decode_line_number(pc)
Expand All @@ -86,19 +186,15 @@ struct Exception::CallStack

if LibC.SymGetLineFromAddrW64(LibC.GetCurrentProcess, pc, out displacement, pointerof(line_info)) != 0
file_name = String.from_utf16(line_info.fileName)[0]
line_number = line_info.lineNumber
line_number = line_info.lineNumber.to_i32
else
line_number = 0
end

unless file_name
module_info = Pointer(LibC::IMAGEHLP_MODULEW64).malloc(1)
module_info.value.sizeOfStruct = sizeof(LibC::IMAGEHLP_MODULEW64)

if LibC.SymGetModuleInfoW64(LibC.GetCurrentProcess, pc, module_info) != 0
mod_displacement = pc - LibC.SymGetModuleBase64(LibC.GetCurrentProcess, pc)
image_name = String.from_utf16(module_info.value.loadedImageName.to_unsafe)[0]
file_name = "#{image_name} +#{mod_displacement}"
if m_info = sym_get_module_info(pc)
offset, image_name = m_info
file_name = "#{image_name} +#{offset}"
else
file_name = "??"
end
Expand All @@ -108,6 +204,37 @@ struct Exception::CallStack
end

protected def self.decode_function_name(pc)
if sym = sym_from_addr(pc)
_, sname = sym
sname
end
end

protected def self.decode_frame(ip)
pc = decode_address(ip)
if sym = sym_from_addr(pc)
if m_info = sym_get_module_info(pc)
offset, sname = sym
_, fname = m_info
{offset, sname, fname}
end
end
end

private def self.sym_get_module_info(pc)
load_debug_info

module_info = Pointer(LibC::IMAGEHLP_MODULEW64).malloc(1)
module_info.value.sizeOfStruct = sizeof(LibC::IMAGEHLP_MODULEW64)

if LibC.SymGetModuleInfoW64(LibC.GetCurrentProcess, pc, module_info) != 0
mod_displacement = pc - LibC.SymGetModuleBase64(LibC.GetCurrentProcess, pc)
image_name = String.from_utf16(module_info.value.loadedImageName.to_unsafe)[0]
{mod_displacement, image_name}
end
end

private def self.sym_from_addr(pc)
load_debug_info

symbol_size = sizeof(LibC::SYMBOL_INFOW) + (LibC::MAX_SYM_NAME - 1) * sizeof(LibC::WCHAR)
Expand All @@ -117,13 +244,11 @@ struct Exception::CallStack

sym_displacement = LibC::DWORD64.zero
if LibC.SymFromAddrW(LibC.GetCurrentProcess, pc, pointerof(sym_displacement), symbol) != 0
String.from_utf16(symbol.value.name.to_unsafe.to_slice(symbol.value.nameLen))
symbol_str = String.from_utf16(symbol.value.name.to_unsafe.to_slice(symbol.value.nameLen))
{sym_displacement, symbol_str}
end
end

protected def self.decode_frame(pc)
end

protected def self.decode_address(ip)
ip.address
end
Expand Down
2 changes: 1 addition & 1 deletion src/kernel.cr
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,6 @@ end
end

Signal.setup_default_handlers
Signal.setup_segfault_handler
{% end %}

# load debug info on start up of the program is executed with CRYSTAL_LOAD_DEBUG_INFO=1
Expand All @@ -544,6 +543,7 @@ end
# - CRYSTAL_LOAD_DEBUG_INFO=1 will load debug info on startup
# - Other values will load debug info on demand: when the backtrace of the first exception is generated
Exception::CallStack.load_debug_info if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "1"
Exception::CallStack.setup_crash_handler

{% if flag?(:preview_mt) %}
Crystal::Scheduler.init_workers
Expand Down
8 changes: 8 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/errhandlingapi.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
require "c/int_safe"

lib LibC
EXCEPTION_CONTINUE_SEARCH = LONG.new!(0)

EXCEPTION_ACCESS_VIOLATION = 0xC0000005_u32
EXCEPTION_STACK_OVERFLOW = 0xC00000FD_u32

alias PVECTORED_EXCEPTION_HANDLER = EXCEPTION_POINTERS* -> LONG

fun GetLastError : DWORD
fun SetLastError(dwErrCode : DWORD)
fun AddVectoredExceptionHandler(first : DWORD, handler : PVECTORED_EXCEPTION_HANDLER) : Void*
end
3 changes: 3 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/malloc.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lib LibC
fun _resetstkoflw : Int
end
1 change: 1 addition & 0 deletions src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ lib LibC
bInheritHandles : BOOL, dwCreationFlags : DWORD,
lpEnvironment : Void*, lpCurrentDirectory : LPWSTR,
lpStartupInfo : STARTUPINFOW*, lpProcessInformation : PROCESS_INFORMATION*) : BOOL
fun SetThreadStackGuarantee(stackSizeInBytes : DWORD*) : BOOL
fun GetProcessTimes(hProcess : HANDLE, lpCreationTime : FILETIME*, lpExitTime : FILETIME*,
lpKernelTime : FILETIME*, lpUserTime : FILETIME*) : BOOL

Expand Down
15 changes: 15 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/winnt.cr
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,19 @@ lib LibC
{% end %}

fun RtlCaptureContext(contextRecord : CONTEXT*)

struct EXCEPTION_RECORD64
exceptionCode : DWORD
exceptionFlags : DWORD
exceptionRecord : DWORD64
exceptionAddress : DWORD64
numberParameters : DWORD
__unusedAlignment : DWORD
exceptionInformation : DWORD64[15]
end

struct EXCEPTION_POINTERS
exceptionRecord : EXCEPTION_RECORD64*
contextRecord : CONTEXT*
end
end