Merge branch 'exceptions' into 'master'

Basic W32 exception handling for glib

See merge request GNOME/glib!582
This commit is contained in:
Philip Withnall 2019-03-06 11:58:45 +00:00
commit a73c9411f9
8 changed files with 544 additions and 0 deletions

View File

@ -256,6 +256,69 @@ How to run and debug your GLib application
</para>
</formalpara>
<formalpara id="G_DEBUGGER">
<title><envar>G_DEBUGGER</envar></title>
<para>
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 <literal>%p</literal> and <literal>%e</literal> 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 <literal>%e</literal> is absent, or if the debugger is not able to
signal events, GLib will resume execution after 60 seconds.
If <literal>%p</literal> is absent, the debugger won't know which process to attach to,
and GLib will also resume execution after 60 seconds.
</para>
<para>
Additionally, even if <envar>G_DEBUGGER</envar> is not set, GLib would still
try to print basic exception information (code and address) into
stderr.
</para>
<para>
By default the debugger gets a new console allocated for it.
Set the <envar>G_DEBUGGER_OLD_CONSOLE</envar> 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.
</para>
<para>
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 <envar>G_DEBUGGER</envar>.
</para>
<para>
See also <link linkend="G_VEH_CATCH"><envar>G_VEH_CATCH</envar></link>.
</para>
</formalpara>
<formalpara id="G_VEH_CATCH">
<title><envar>G_VEH_CATCH</envar></title>
<para>
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.
</para>
<para>
The <envar>G_VEH_CATCH</envar> environment variable augments
<ulink url="https://docs.microsoft.com/en-us/windows/desktop/debug/vectored-exception-handling">Vectored Exception Handling</ulink>
on Windows (see <link linkend="G_DEBUGGER"><envar>G_DEBUGGER</envar></link>), allowing GLib to catch more
exceptions. Set this variable to a comma-separated list of
hexademical exception codes that should additionally be caught.
</para>
<para>
By default GLib will only catch Access Violation, Stack Overflow and
Illegal Instruction <ulink url="https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_exception_record">exceptions</ulink>.
</para>
</formalpara>
</refsect2>
<refsect2 id="setlocale">

View File

@ -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)

View File

@ -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:

View File

@ -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

77
glib/gwin32-private.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
/* 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;
}

View File

@ -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

View File

@ -140,6 +140,9 @@ if host_machine.system() == 'windows'
},
}
endif
glib_tests += {
'win32' : {},
}
else
glib_tests += {
'include' : {},

173
glib/tests/win32.c Normal file
View File

@ -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 <glib.h>
#include <gprintf.h>
#include <stdio.h>
#include <windows.h>
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();
}