Skip to content

Commit

Permalink
Add crash handler on Windows (#11570)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Jan 31, 2022
1 parent 0c0b1b8 commit 966b3a5
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 40 deletions.
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 @@ -239,8 +239,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 @@ -261,7 +261,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 @@ -272,10 +272,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

0 comments on commit 966b3a5

Please sign in to comment.