From 025a346728d50371073cfe8c976ee80e7c01182a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D1=81=D0=BB=D0=B0=D0=BD=20=D0=98=D0=B6=D0=B1?= =?UTF-8?q?=D1=83=D0=BB=D0=B0=D1=82=D0=BE=D0=B2?= Date: Sun, 13 Jan 2019 11:21:04 +0000 Subject: [PATCH 1/2] W32: Add a simple exception handler Install a Vectored Exception Handler[0]. Its sole purpose is to catch some exceptions (access violations, stack overflows, illegal instructions and debug breaks - by default, but it can be made to catch any exception for which a code is known) and run a debugger in response. This allows W32 glib applications to be run without a debugger, but at the same time allows a debugger to be attached in case something happens. The debugger is run with a new console, unless an environment variable is set to allow it to inherit the console of the crashing process. The short list of handleable exceptions is there to ensure that this handler won't run a debugger to "handle" utility exceptions, such as the one that is used to communicate thread names to a debugger. The handler is installed to be called last, and shouldn't interfere with any user-installed handlers. There's nothing fancy about the way it runs a debugger (it doesn't even support unicode in paths), and it deliberately avoids using glib code. The handler will also print a bit of information about the exception that it caught, and even more information for well-known exceptions, such as access violation. The whole scheme is similar to AeDebug[1] and, in fact, the signal-event gdb command was originally implemented for this very purpose. [0]: https://docs.microsoft.com/en-us/windows/desktop/debug/vectored-exception-handling [1]: https://docs.microsoft.com/en-us/windows/desktop/debug/configuring-automatic-debugging --- docs/reference/glib/running.xml | 63 ++++++++++ glib/gbacktrace.c | 10 ++ glib/glib-init.c | 2 + glib/glib-init.h | 2 + glib/gwin32-private.c | 77 ++++++++++++ glib/gwin32.c | 214 ++++++++++++++++++++++++++++++++ 6 files changed, 368 insertions(+) create mode 100644 glib/gwin32-private.c diff --git a/docs/reference/glib/running.xml b/docs/reference/glib/running.xml index 8e4ffeca3..86765c868 100644 --- a/docs/reference/glib/running.xml +++ b/docs/reference/glib/running.xml @@ -256,6 +256,69 @@ How to run and debug your GLib application + + <envar>G_DEBUGGER</envar> + + + When running on Windows, if set to a non-empty string, GLib will + try to interpret the contents of this environment variable as + a command line to a debugger, and run it if the process crashes. + The debugger command line should contain %p and %e substitution + tokens, which GLib will replace with the process ID of the crashing + process and a handle to an event that the debugger should signal + to let GLib know that the debugger successfully attached to the + process. If %e is absent, or if the debugger is not able to + signal events, GLib will resume execution after 60 seconds. + If %p is absent, the debugger won't know which process to attach to, + and GLib will also resume execution after 60 seconds. + + + Additionally, even if G_DEBUGGER is not set, GLib would still + try to print basic exception information (code and address) into + stderr. + + + By default the debugger gets a new console allocated for it. + Set the G_DEBUGGER_OLD_CONSOLE environment variable to any + non-empty string to make the debugger inherit the console of + the crashing process. Normally this is only used by the GLib + testsuite. + + + The exception handler is written with the aim of making it as + simple as possible, to minimize the risk of it invoking + buggy functions or running buggy code, which would result + in exceptions being raised recursively. Because of that + it lacks most of the amenities that one would expect of GLib. + Namely, it does not support Unicode, so it is highly advisable + to only use ASCII characters in G_DEBUGGER. + + + See also G_VEH_CATCH. + + + + + <envar>G_VEH_CATCH</envar> + + + Catching some exceptions can break the program, since Windows + will sometimes use exceptions for execution flow control and + other purposes other than signalling a crash. + + + The G_VEH_CATCH environment variable augments + Vectored Exception Handling + on Windows (see G_DEBUGGER), allowing GLib to catch more + exceptions. Set this variable to a comma-separated list of + hexademical exception codes that should additionally be caught. + + + By default GLib will only catch Access Violation, Stack Overflow and + Illegal Instruction exceptions. + + + diff --git a/glib/gbacktrace.c b/glib/gbacktrace.c index e83079985..34cb1ce46 100644 --- a/glib/gbacktrace.c +++ b/glib/gbacktrace.c @@ -125,6 +125,10 @@ volatile gboolean glib_on_error_halt = TRUE; * If "[P]roceed" is selected, the function returns. * * This function may cause different actions on non-UNIX platforms. + * + * On Windows consider using the `G_DEBUGGER` environment + * variable (see [Running GLib Applications](glib-running.html)) and + * calling g_on_error_stack_trace() instead. */ void g_on_error_query (const gchar *prg_name) @@ -207,6 +211,12 @@ g_on_error_query (const gchar *prg_name) * gdk_init(). * * This function may cause different actions on non-UNIX platforms. + * + * When running on Windows, this function is *not* called by + * g_on_error_query(). If called directly, it will raise an + * exception, which will crash the program. If the `G_DEBUGGER` environment + * variable is set, a debugger will be invoked to attach and + * handle that exception (see [Running GLib Applications](glib-running.html)). */ void g_on_error_stack_trace (const gchar *prg_name) diff --git a/glib/glib-init.c b/glib/glib-init.c index 6ffe7933b..ed800dca1 100644 --- a/glib/glib-init.c +++ b/glib/glib-init.c @@ -286,6 +286,7 @@ DllMain (HINSTANCE hinstDLL, { case DLL_PROCESS_ATTACH: glib_dll = hinstDLL; + g_crash_handler_win32_init (); g_clock_win32_init (); #ifdef THREADS_WIN32 g_thread_win32_init (); @@ -306,6 +307,7 @@ DllMain (HINSTANCE hinstDLL, if (lpvReserved == NULL) g_thread_win32_process_detach (); #endif + g_crash_handler_win32_deinit (); break; default: diff --git a/glib/glib-init.h b/glib/glib-init.h index 695dc044b..f44b9a4d2 100644 --- a/glib/glib-init.h +++ b/glib/glib-init.h @@ -36,6 +36,8 @@ void g_thread_win32_thread_detach (void); void g_thread_win32_init (void); void g_console_win32_init (void); void g_clock_win32_init (void); +void g_crash_handler_win32_init (void); +void g_crash_handler_win32_deinit (void); extern HMODULE glib_dll; #endif diff --git a/glib/gwin32-private.c b/glib/gwin32-private.c new file mode 100644 index 000000000..917bcd1b5 --- /dev/null +++ b/glib/gwin32-private.c @@ -0,0 +1,77 @@ +/* gwin32-private.c - private glib functions for gwin32.c + * + * Copyright 2019 Руслан Ижбулатов + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, see . + */ + +/* Copy @cmdline into @debugger, and substitute @pid for `%p` + * and @event for `%e`. + * If @debugger_size (in bytes) is overflowed, return %FALSE. + * Also returns %FALSE when `%` is followed by anything other + * than `e` or `p`. + */ +static gboolean +_g_win32_subst_pid_and_event (char *debugger, + gsize debugger_size, + const char *cmdline, + DWORD pid, + guintptr event) +{ + gsize i = 0, dbg_i = 0; +/* These are integers, and they can't be longer than 20 characters + * even when they are 64-bit and in decimal notation. + * Use 30 just to be sure. + */ +#define STR_BUFFER_SIZE 30 + char pid_str[STR_BUFFER_SIZE] = {0}; + gsize pid_str_len; + char event_str[STR_BUFFER_SIZE] = {0}; + gsize event_str_len; +#undef STR_BUFFER_SIZE + snprintf (pid_str, G_N_ELEMENTS (pid_str), "%lu", pid); + pid_str[G_N_ELEMENTS (pid_str) - 1] = 0; + pid_str_len = strlen (pid_str); + snprintf (event_str, G_N_ELEMENTS (pid_str), "%Iu", event); + event_str[G_N_ELEMENTS (pid_str) - 1] = 0; + event_str_len = strlen (event_str); + + while (cmdline[i] != 0 && dbg_i < debugger_size) + { + if (cmdline[i] != '%') + debugger[dbg_i++] = cmdline[i++]; + else if (cmdline[i + 1] == 'p') + { + gsize j = 0; + while (j < pid_str_len && dbg_i < debugger_size) + debugger[dbg_i++] = pid_str[j++]; + i += 2; + } + else if (cmdline[i + 1] == 'e') + { + gsize j = 0; + while (j < event_str_len && dbg_i < debugger_size) + debugger[dbg_i++] = event_str[j++]; + i += 2; + } + else + return FALSE; + } + if (dbg_i < debugger_size) + debugger[dbg_i] = 0; + else + return FALSE; + + return TRUE; +} diff --git a/glib/gwin32.c b/glib/gwin32.c index 8a7ab3aeb..6d97e4c0e 100644 --- a/glib/gwin32.c +++ b/glib/gwin32.c @@ -1018,4 +1018,218 @@ g_console_win32_init (void) } } +/* This is a handle to the Vectored Exception Handler that + * we install on library initialization. If installed correctly, + * it will be non-NULL. Only used to later de-install the handler + * on library de-initialization. + */ +static void *WinVEH_handle = NULL; + +#include "gwin32-private.c" + +/** + * Handles exceptions (useful for debugging). + * Issues a DebugBreak() call if the process is being debugged (not really + * useful - if the process is being debugged, this handler won't be invoked + * anyway). If it is not, runs a debugger from G_DEBUGGER env var, + * substituting first %p in it for PID, and the first %e for the event handle - + * that event should be set once the debugger attaches itself (otherwise the + * only way out of WaitForSingleObject() is to time out after 1 minute). + * For example, G_DEBUGGER can be set to the following command: + * ``` + * gdb.exe -ex "attach %p" -ex "signal-event %e" -ex "bt" -ex "c" + * ``` + * This will make GDB attach to the process, signal the event (GDB must be + * recent enough for the signal-event command to be available), + * show the backtrace and resume execution, which should make it catch + * the exception when Windows re-raises it again. + * The command line can't be longer than MAX_PATH (260 characters). + * + * This function will only stop (and run a debugger) on the following exceptions: + * * EXCEPTION_ACCESS_VIOLATION + * * EXCEPTION_STACK_OVERFLOW + * * EXCEPTION_ILLEGAL_INSTRUCTION + * To make it stop at other exceptions one should set the G_VEH_CATCH + * environment variable to a list of comma-separated hexademical numbers, + * where each number is the code of an exception that should be caught. + * This is done to prevent GLib from breaking when Windows uses + * exceptions to shuttle information (SetThreadName(), OutputDebugString()) + * or for control flow. + * + * This function deliberately avoids calling any GLib code. + */ +static LONG __stdcall +g_win32_veh_handler (PEXCEPTION_POINTERS ExceptionInfo) +{ + EXCEPTION_RECORD *er; + char debugger[MAX_PATH + 1]; + const char *debugger_env = NULL; + const char *catch_list; + gboolean catch = FALSE; + STARTUPINFO si; + PROCESS_INFORMATION pi; + HANDLE event; + SECURITY_ATTRIBUTES sa; + + if (ExceptionInfo == NULL || + ExceptionInfo->ExceptionRecord == NULL) + return EXCEPTION_CONTINUE_SEARCH; + + er = ExceptionInfo->ExceptionRecord; + + switch (er->ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_STACK_OVERFLOW: + case EXCEPTION_ILLEGAL_INSTRUCTION: + case EXCEPTION_BREAKPOINT: /* DebugBreak() raises this */ + break; + default: + catch_list = getenv ("G_VEH_CATCH"); + + while (!catch && + catch_list != NULL && + catch_list[0] != 0) + { + unsigned long catch_code; + char *end; + errno = 0; + catch_code = strtoul (catch_list, &end, 16); + if (errno != NO_ERROR) + break; + catch_list = end; + if (catch_list != NULL && catch_list[0] == ',') + catch_list++; + if (catch_code == er->ExceptionCode) + catch = TRUE; + } + + if (catch) + break; + + return EXCEPTION_CONTINUE_SEARCH; + } + + if (IsDebuggerPresent ()) + { + /* This shouldn't happen, but still try to + * avoid recursion with EXCEPTION_BREAKPOINT and + * DebugBreak(). + */ + if (er->ExceptionCode != EXCEPTION_BREAKPOINT) + DebugBreak (); + return EXCEPTION_CONTINUE_EXECUTION; + } + + fprintf (stderr, + "Exception code=0x%lx flags=0x%lx at 0x%p", + er->ExceptionCode, + er->ExceptionFlags, + er->ExceptionAddress); + + switch (er->ExceptionCode) + { + case EXCEPTION_ACCESS_VIOLATION: + fprintf (stderr, + ". Access violation - attempting to %s at address 0x%p\n", + er->ExceptionInformation[0] == 0 ? "read data" : + er->ExceptionInformation[0] == 1 ? "write data" : + er->ExceptionInformation[0] == 8 ? "execute data" : + "do something bad", + (void *) er->ExceptionInformation[1]); + break; + case EXCEPTION_IN_PAGE_ERROR: + fprintf (stderr, + ". Page access violation - attempting to %s at address 0x%p with status %Ix\n", + er->ExceptionInformation[0] == 0 ? "read from an inaccessible page" : + er->ExceptionInformation[0] == 1 ? "write to an inaccessible page" : + er->ExceptionInformation[0] == 8 ? "execute data in page" : + "do something bad with a page", + (void *) er->ExceptionInformation[1], + er->ExceptionInformation[2]); + break; + default: + fprintf (stderr, "\n"); + break; + } + + fflush (stderr); + + debugger_env = getenv ("G_DEBUGGER"); + + if (debugger_env == NULL) + return EXCEPTION_CONTINUE_SEARCH; + + /* Create an inheritable event */ + memset (&si, 0, sizeof (si)); + memset (&pi, 0, sizeof (pi)); + memset (&sa, 0, sizeof (sa)); + si.cb = sizeof (si); + sa.nLength = sizeof (sa); + sa.bInheritHandle = TRUE; + event = CreateEvent (&sa, FALSE, FALSE, NULL); + + /* Put process ID and event handle into debugger commandline */ + if (!_g_win32_subst_pid_and_event (debugger, G_N_ELEMENTS (debugger), + debugger_env, GetCurrentProcessId (), + (guintptr) event)) + { + CloseHandle (event); + return EXCEPTION_CONTINUE_SEARCH; + } + + /* Run the debugger */ + debugger[MAX_PATH] = '\0'; + if (0 != CreateProcessA (NULL, + debugger, + NULL, + NULL, + TRUE, + getenv ("G_DEBUGGER_OLD_CONSOLE") != NULL ? 0 : CREATE_NEW_CONSOLE, + NULL, + NULL, + &si, + &pi)) + { + CloseHandle (pi.hProcess); + CloseHandle (pi.hThread); + /* If successful, wait for 60 seconds on the event + * we passed. The debugger should signal that event. + * 60 second limit is here to prevent us from hanging + * up forever in case the debugger does not support + * event signalling. + */ + WaitForSingleObject (event, 60000); + } + + CloseHandle (event); + + /* Now the debugger is present, and we can try + * resuming execution, re-triggering the exception, + * which will be caught by debugger this time around. + */ + if (IsDebuggerPresent ()) + return EXCEPTION_CONTINUE_EXECUTION; + + return EXCEPTION_CONTINUE_SEARCH; +} + +void +g_crash_handler_win32_init (void) +{ + if (WinVEH_handle != NULL) + return; + + WinVEH_handle = AddVectoredExceptionHandler (0, &g_win32_veh_handler); +} + +void +g_crash_handler_win32_deinit (void) +{ + if (WinVEH_handle != NULL) + RemoveVectoredExceptionHandler (WinVEH_handle); + + WinVEH_handle = NULL; +} + #endif From 66f4c104ba4b807032eeb62393801c25b1c88c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D1=83=D1=81=D0=BB=D0=B0=D0=BD=20=D0=98=D0=B6=D0=B1?= =?UTF-8?q?=D1=83=D0=BB=D0=B0=D1=82=D0=BE=D0=B2?= Date: Sat, 2 Feb 2019 11:13:29 +0000 Subject: [PATCH 2/2] W32 VEH: Add tests One test is for _g_win32_subst_pid_and_event(). Two tests for crashing with different exceptions (access violation and illegal instruction). And one test for running a debugger. --- glib/tests/meson.build | 3 + glib/tests/win32.c | 173 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 glib/tests/win32.c diff --git a/glib/tests/meson.build b/glib/tests/meson.build index d54fc41fa..2b3f902ab 100644 --- a/glib/tests/meson.build +++ b/glib/tests/meson.build @@ -140,6 +140,9 @@ if host_machine.system() == 'windows' }, } endif + glib_tests += { + 'win32' : {}, + } else glib_tests += { 'include' : {}, diff --git a/glib/tests/win32.c b/glib/tests/win32.c new file mode 100644 index 000000000..d4b1fc523 --- /dev/null +++ b/glib/tests/win32.c @@ -0,0 +1,173 @@ +/* Unit test for VEH on Windows + * Copyright (C) 2019 Руслан Ижбулатов + * + * This work is provided "as is"; redistribution and modification + * in whole or in part, in any medium, physical or electronic is + * permitted without restriction. + * + * This work is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * In no event shall the authors or contributors be liable for any + * direct, indirect, incidental, special, exemplary, or consequential + * damages (including, but not limited to, procurement of substitute + * goods or services; loss of use, data, or profits; or business + * interruption) however caused and on any theory of liability, whether + * in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even + * if advised of the possibility of such damage. + */ + +#include "config.h" + +#include +#include +#include +#include + +static char *argv0 = NULL; + +#include "../gwin32-private.c" + +static void +test_subst_pid_and_event (void) +{ + const gchar not_enough[] = "too long when %e and %p are substituted"; + gchar debugger_3[3]; + gchar debugger_not_enough[G_N_ELEMENTS (not_enough)]; + gchar debugger_enough[G_N_ELEMENTS (not_enough) + 1]; + gchar debugger_big[65535] = {0}; + gchar *output; + DWORD be = 0xFFFFFFFF; + guintptr bp = (guintptr) G_MAXSIZE; + + /* %f is not valid */ + g_assert_false (_g_win32_subst_pid_and_event (debugger_3, G_N_ELEMENTS (debugger_3), + "%f", 0, 0)); + + g_assert_false (_g_win32_subst_pid_and_event (debugger_3, G_N_ELEMENTS (debugger_3), + "string longer than 10", 0, 0)); + /* 200 is longer than %e, so the string doesn't fit by 1 byte */ + g_assert_false (_g_win32_subst_pid_and_event (debugger_not_enough, G_N_ELEMENTS (debugger_not_enough), + "too long when %e and %p are substituted", 10, 200)); + + /* This should fit */ + g_assert_true (_g_win32_subst_pid_and_event (debugger_enough, G_N_ELEMENTS (debugger_enough), + "too long when %e and %p are substituted", 10, 200)); + g_assert_cmpstr (debugger_enough, ==, "too long when 200 and 10 are substituted"); + + g_assert_true (_g_win32_subst_pid_and_event (debugger_big, G_N_ELEMENTS (debugger_big), + "multipl%e big %e %entries and %pids are %provided here", bp, be)); + output = g_strdup_printf ("multipl%lu big %lu %luntries and %lluids are %llurovided here", be, be, be, (guint64) bp, (guint64) bp); + g_assert_cmpstr (debugger_big, ==, output); + g_free (output); +} + +/* Crash with access violation */ +static void +test_access_violation (void) +{ + int *integer = NULL; + /* Use SEM_NOGPFAULTERRORBOX to prevent an error dialog + * from being shown. + */ + DWORD dwMode = SetErrorMode (SEM_NOGPFAULTERRORBOX); + SetErrorMode (dwMode | SEM_NOGPFAULTERRORBOX); + *integer = 1; + SetErrorMode (dwMode); +} + +/* Crash with illegal instruction */ +static void +test_illegal_instruction (void) +{ + DWORD dwMode = SetErrorMode (SEM_NOGPFAULTERRORBOX); + SetErrorMode (dwMode | SEM_NOGPFAULTERRORBOX); + RaiseException (EXCEPTION_ILLEGAL_INSTRUCTION, 0, 0, NULL); + SetErrorMode (dwMode); +} + +static void +test_veh_crash_access_violation (void) +{ + g_unsetenv ("G_DEBUGGER"); + /* Run a test that crashes */ + g_test_trap_subprocess ("/win32/subprocess/access_violation", 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("Exception code=0xc0000005*"); +} + +static void +test_veh_crash_illegal_instruction (void) +{ + g_unsetenv ("G_DEBUGGER"); + /* Run a test that crashes */ + g_test_trap_subprocess ("/win32/subprocess/illegal_instruction", 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("Exception code=0xc000001d*"); +} + +static void +test_veh_debug (void) +{ + /* Run a test that crashes and runs a debugger */ + g_test_trap_subprocess ("/win32/subprocess/debugee", 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("Exception code=0xc0000005*Debugger invoked, attaching to*"); +} + +static void +test_veh_debugee (void) +{ + /* Set up a debugger to be run on crash */ + gchar *command = g_strdup_printf ("%s %s", argv0, "%p %e"); + g_setenv ("G_DEBUGGER", command, TRUE); + /* Because the "debugger" here is not really a debugger, + * it can't write into stderr of this process, unless + * we allow it to inherit our stderr. + */ + g_setenv ("G_DEBUGGER_OLD_CONSOLE", "1", TRUE); + g_free (command); + /* Crash */ + test_access_violation (); +} + +static void +veh_debugger (int argc, char *argv[]) +{ + char *end; + DWORD pid = strtoul (argv[1], &end, 10); + guintptr event = (guintptr) _strtoui64 (argv[2], &end, 10); + /* Unfreeze the debugee and announce ourselves */ + SetEvent ((HANDLE) event); + CloseHandle ((HANDLE) event); + g_fprintf (stderr, "Debugger invoked, attaching to %lu and signalling %" G_GUINTPTR_FORMAT, pid, event); +} + +int +main (int argc, + char *argv[]) +{ + argv0 = argv[0]; + + g_test_init (&argc, &argv, NULL); + + if (argc > 2) + { + veh_debugger (argc, argv); + return 0; + } + + g_test_add_func ("/win32/substitute-pid-and-event", test_subst_pid_and_event); + + g_test_add_func ("/win32/veh/access_violation", test_veh_crash_access_violation); + g_test_add_func ("/win32/veh/illegal_instruction", test_veh_crash_illegal_instruction); + g_test_add_func ("/win32/veh/debug", test_veh_debug); + + g_test_add_func ("/win32/subprocess/debugee", test_veh_debugee); + g_test_add_func ("/win32/subprocess/access_violation", test_access_violation); + g_test_add_func ("/win32/subprocess/illegal_instruction", test_illegal_instruction); + + return g_test_run(); +}