Commit 5200bdeb authored by Tamar Christina's avatar Tamar Christina Committed by Austin Seipp

Replaced SEH handles with VEH handlers which should work uniformly across x86 and x64

Summary:
On Windows, the default action for things like division by zero and
segfaults is to pop up a Dr. Watson error reporting dialog if the exception
is unhandled by the user code.

This is a pain when we are SSHed into a Windows machine, or when we
want to debug a problem with gdb (gdb will get a first and second chance to
handle the exception, but if it doesn't the pop-up will show).

veh_excn provides two macros, `BEGIN_CATCH` and `END_CATCH`, which
will catch such exceptions in the entire process and die by
printing a message and calling `stg_exit(1)`.

Previously this code was handled using SEH (Structured Exception Handlers)
however each compiler and platform have different ways of dealing with SEH.

`MSVC` compilers have the keywords `__try`, `__catch` and `__except` to have the
compiler generate the appropriate SEH handler code for you.

`MinGW` compilers have no such keywords and require you to manually set the
SEH Handlers, however because SEH is implemented differently in x86 and x64
the methods to use them in GCC differs.

`x86`: SEH is based on the stack, the SEH handlers are available at `FS[0]`.
     On startup one would only need to add a new handler there. This has
     a number of issues such as hard to share handlers and it can be exploited.

`x64`: In order to fix the issues with the way SEH worked in x86, on x64 SEH handlers
     are statically compiled and added to the .pdata section by the compiler.
     Instead of being thread global they can now be Image global since you have to
     specify the `RVA` of the region of code that the handlers govern.

You can on x64 Dynamically allocate SEH handlers, but it seems that (based on
experimentation and it's very under-documented) that the dynamic calls cannot override
static SEH handlers in the .pdata section.

Because of this and because GHC no longer needs to support < windows XP, the better
alternative for handling errors would be using the in XP introduced VEH.

The bonus is because VEH (Vectored Exception Handler) are a runtime construct the API
is the same for both x86 and x64 (note that the Context object does contain CPU specific
structures) and the calls are the same cross compilers. Which means this file can be
simplified quite a bit.
Using VEH also means we don't have to worry about the dynamic code generated by GHCi.

Test Plan:
Prior to this diff the tests for `derefnull` and `divbyzero` seem to have been disabled for windows.
To reproduce the issue on x64:
1) open ghci
2) import GHC.Base
3) run: 1 `divInt` 0

which should lead to ghci crashing an a watson error box displaying.

After applying the patch, run:

make TEST="derefnull divbyzero"

on both x64 and x86 builds of ghc to verify fix.

Reviewers: simonmar, austin

Reviewed By: austin

Subscribers: thomie

Differential Revision: https://phabricator.haskell.org/D691

GHC Trac Issues: #6079
parent f6609b0e
/* -----------------------------------------------------------------------------
*
* (c) The GHC Team 1998-2000
*
* Hides indirection for EH handlers for different platforms
*
* ---------------------------------------------------------------------------*/
#ifndef EXCN_H
#define EXCN_H
#include "ghcconfig.h"
// On windows Excn provides two macros
// BEGIN_WINDOWS_VEH_HANDLER and END_WINDOWS_VEH_HANDLER, which
// will catch such exceptions in the entire process and die by
// printing a message and calling stg_exit(1).
//
// For other operating systems an empty macro is defined so
// that no #ifdefs are needed around the usage of these macros.
#if defined(mingw32_HOST_OS)
#include "win32/veh_excn.h"
#define BEGIN_WINDOWS_VEH_HANDLER __register_hs_exception_handler();
#define END_WINDOWS_VEH_HANDLER __unregister_hs_exception_handler();
#else
#define BEGIN_WINDOWS_VEH_HANDLER
#define END_WINDOWS_VEH_HANDLER
#endif /* mingw32_HOST_OS */
#endif /* EXCN_H */
......@@ -8,6 +8,7 @@
#define COMPILING_RTS_MAIN
#include "Excn.h"
#include "PosixSource.h"
#include "Rts.h"
#include "RtsAPI.h"
......@@ -15,105 +16,81 @@
#include "RtsUtils.h"
#include "Prelude.h"
#include "Task.h"
#if defined(mingw32_HOST_OS)
#include "win32/seh_excn.h"
#endif
#ifdef DEBUG
# include "Printer.h" /* for printing */
#endif
#ifdef HAVE_WINDOWS_H
# include <windows.h>
#endif
// Hack: we assume that we're building a batch-mode system unless
// INTERPRETER is set
#ifndef INTERPRETER /* Hack */
/* Annoying global vars for passing parameters to real_main() below
* This is to get around problem with Windows SEH, see hs_main(). */
static int progargc;
static char **progargv;
static StgClosure *progmain_closure; /* This will be ZCMain_main_closure */
static RtsConfig rtsconfig;
// The rts entry point from a compiled program using a Haskell main
// function. This gets called from a tiny main function generated by
// GHC and linked into each compiled Haskell program that uses a
// Haskell main function.
//
// We expect the caller to pass ZCMain_main_closure for
// main_closure. The reason we cannot refer to this symbol directly
// is because we're inside the rts and we do not know for sure that
// we'll be using a Haskell main function.
//
// NOTE: This function is marked as _noreturn_ in Main.h
/* Hack: we assume that we're building a batch-mode system unless
* INTERPRETER is set
*/
#ifndef INTERPRETER /* Hack */
static void real_main(void) GNUC3_ATTRIBUTE(__noreturn__);
static void real_main(void)
int hs_main ( int argc, char *argv[], // program args
StgClosure *main_closure, // closure for Main.main
RtsConfig rts_config) // RTS configuration
{
BEGIN_WINDOWS_VEH_HANDLER
int exit_status;
SchedulerStatus status;
hs_init_ghc(&progargc, &progargv, rtsconfig);
/* kick off the computation by creating the main thread with a pointer
to mainIO_closure representing the computation of the overall program;
then enter the scheduler with this thread and off we go;
the same for GranSim (we have only one instance of this code)
hs_init_ghc(&argc, &argv, rts_config);
in a parallel setup, where we have many instances of this code
running on different PEs, we should do this only for the main PE
(IAmMainThread is set in startupHaskell)
*/
// kick off the computation by creating the main thread with a pointer
// to mainIO_closure representing the computation of the overall program;
// then enter the scheduler with this thread and off we go;
//
// the same for GranSim (we have only one instance of this code)
//
// in a parallel setup, where we have many instances of this code
// running on different PEs, we should do this only for the main PE
// (IAmMainThread is set in startupHaskell)
/* ToDo: want to start with a larger stack size */
// ToDo: want to start with a larger stack size
{
Capability *cap = rts_lock();
rts_evalLazyIO(&cap,progmain_closure, NULL);
rts_evalLazyIO(&cap, main_closure, NULL);
status = rts_getSchedStatus(cap);
rts_unlock(cap);
}
/* check the status of the entire Haskell computation */
// check the status of the entire Haskell computation
switch (status) {
case Killed:
errorBelch("main thread exited (uncaught exception)");
exit_status = EXIT_KILLED;
break;
errorBelch("main thread exited (uncaught exception)");
exit_status = EXIT_KILLED;
break;
case Interrupted:
errorBelch("interrupted");
exit_status = EXIT_INTERRUPTED;
break;
errorBelch("interrupted");
exit_status = EXIT_INTERRUPTED;
break;
case HeapExhausted:
exit_status = EXIT_HEAPOVERFLOW;
break;
exit_status = EXIT_HEAPOVERFLOW;
break;
case Success:
exit_status = EXIT_SUCCESS;
break;
exit_status = EXIT_SUCCESS;
break;
default:
barf("main thread completed with invalid status");
barf("main thread completed with invalid status");
}
shutdownHaskellAndExit(exit_status, 0 /* !fastExit */);
}
/* The rts entry point from a compiled program using a Haskell main
* function. This gets called from a tiny main function generated by
* GHC and linked into each compiled Haskell program that uses a
* Haskell main function.
*
* We expect the caller to pass ZCMain_main_closure for
* main_closure. The reason we cannot refer to this symbol directly
* is because we're inside the rts and we do not know for sure that
* we'll be using a Haskell main function.
*/
int hs_main (int argc, char *argv[], // program args
StgClosure *main_closure, // closure for Main.main
RtsConfig rts_config) // RTS configuration
{
/* We do this dance with argc and argv as otherwise the SEH exception
stuff (the BEGIN/END CATCH below) on Windows gets confused */
progargc = argc;
progargv = argv;
progmain_closure = main_closure;
rtsconfig = rts_config;
END_WINDOWS_VEH_HANDLER
#if defined(mingw32_HOST_OS) && defined(i386_HOST_ARCH)
BEGIN_CATCH
#endif
real_main();
#if defined(mingw32_HOST_OS) && defined(i386_HOST_ARCH)
END_CATCH
#endif
shutdownHaskellAndExit(exit_status, 0 /* !fastExit */);
// No code beyond this point. Dead code elimination will remove it
}
# endif /* BATCH_MODE */
......@@ -177,6 +177,13 @@ endif
rts_dist_$1_CC_OPTS += -DRtsWay=\"rts_$1\"
# If we're compiling on windows, enforce that we only support XP+
# Adding this here means it doesn't have to be done in individual .c files
# and also centralizes the versioning.
ifeq "$$(TargetOS_CPP)" "mingw32"
rts_dist_$1_CC_OPTS += -DWINVER=0x0501
endif
ifneq "$$(UseSystemLibFFI)" "YES"
rts_dist_FFI_SO = rts/dist/build/lib$$(LIBFFI_NAME)$$(soext)
else
......@@ -313,13 +320,6 @@ endif
#-----------------------------------------------------------------------------
# Flags for compiling specific files
# If RtsMain.c is built with optimisation then the SEH exception stuff on
# Windows gets confused.
# This has to be in HC rather than CC opts, as otherwise there's a
# -optc-O2 that comes after it.
rts/RtsMain_HC_OPTS += -optc-O0
rts/RtsMessages_CC_OPTS += -DProjectVersion=\"$(ProjectVersion)\"
rts/RtsUtils_CC_OPTS += -DProjectVersion=\"$(ProjectVersion)\"
rts/Trace_CC_OPTS += -DProjectVersion=\"$(ProjectVersion)\"
......
......@@ -6,8 +6,6 @@
*
* ---------------------------------------------------------------------------*/
#define _WIN32_WINNT 0x0501
#include "Rts.h"
#include "sm/OSMem.h"
#include "RtsUtils.h"
......
......@@ -7,8 +7,6 @@
*
* --------------------------------------------------------------------------*/
#define _WIN32_WINNT 0x0501
#include "Rts.h"
#include <windows.h>
#if defined(THREADED_RTS)
......
......@@ -2,7 +2,6 @@
* RTS periodic timers.
*
*/
#define _WIN32_WINNT 0x0501
#include "Rts.h"
#include "Ticker.h"
......
#include "ghcconfig.h"
#include "seh_excn.h"
/*
* Exception / signal handlers.
*/
#if defined(mingw32_HOST_OS)
#if defined(i386_HOST_ARCH)
jmp_buf seh_unwind_to;
unsigned long seh_excn_code; /* variable used to communicate what kind of exception we've caught;nice. */
EXCEPTION_DISPOSITION
catchDivZero(struct _EXCEPTION_RECORD* rec,
void* arg1 __attribute__((unused)),
struct _CONTEXT* ctxt __attribute__((unused)),
void* arg2 __attribute__((unused)))
{
if ((rec->ExceptionFlags & EH_UNWINDING) != 0) {
// When the system unwinds the SEH stack after having handled an excn,
// return immediately.
return ExceptionContinueSearch;
}
switch (rec->ExceptionCode) {
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
case EXCEPTION_INT_DIVIDE_BY_ZERO:
seh_excn_code = 0;
longjmp(seh_unwind_to, rec->ExceptionCode);
return ExceptionContinueExecution;
case EXCEPTION_STACK_OVERFLOW:
seh_excn_code = 1;
longjmp(seh_unwind_to, rec->ExceptionCode);
return ExceptionContinueExecution;
case EXCEPTION_ACCESS_VIOLATION:
seh_excn_code = 2;
longjmp(seh_unwind_to, rec->ExceptionCode);
return ExceptionContinueExecution;
longjmp(seh_unwind_to, rec->ExceptionCode);
return ExceptionContinueExecution;
default: ;
}
return ExceptionContinueSearch;
}
#endif
#endif
#ifndef WIN32_SEH_EXCN_H
#define WIN32_SEH_EXCN_H
#include <stdio.h>
#include <stdlib.h>
#if defined(__MINGW32__)
/* Stuff needed to install and use SEH exception handlers */
#include <excpt.h>
#include <setjmp.h>
#include <windows.h>
#elif defined(_MSC_VER)
#include <windows.h>
#else
#include <signal.h>
#endif
/* Exception handling.
*
* On Win32, the default action for things like division by zero and
* segfaults is to pop up an annoying little dialog box.
*
* This is a pain when we are SSHed into a Windows machine, or when we
* want to debug a problem with gdb.
*
* seh_excn provides two macros, BEGIN_CATCH and END_CATCH, which
* will catch such exceptions in the code they bracket and die by
* printing a message and calling stg_exit(1).
*/
#define ON_DIV_ZERO fprintf(stdout,"divide by zero\n"); fflush(stdout);stg_exit(1)
#define ON_STACK_OVERFLOW fprintf(stdout,"C stack overflow in generated code\n"); fflush(stdout); stg_exit(1)
#define ON_SIGSEGV fprintf(stdout,"Segmentation fault/access violation in generated code\n"); fflush(stdout); stg_exit(1)
#if defined(__MINGW32__)
extern jmp_buf seh_unwind_to;
extern unsigned long seh_excn_code;
/*
* install an exception handler 'exHandler' which longjmp()s (via 'jumpBuf')
* to the code 'onExnCaught' when successfully catching an exception.
*
* Macro based on Andrew Begel's SEH support code posted to the mingw-users
* mailing list.
*/
#define TRY_BEGIN(jumpBuf, exHandler, onExcnCaught) \
do { \
int signal; \
if ((signal = setjmp(jumpBuf)) != 0) { \
onExcnCaught; \
} else { \
__try1(exHandler); \
} \
} while (0);
#define TRY_END() __except1
extern
EXCEPTION_DISPOSITION
catchDivZero(struct _EXCEPTION_RECORD*,
void*,
struct _CONTEXT*,
void*);
#define ON_EXCN \
if (seh_excn_code == 1) { \
ON_STACK_OVERFLOW; \
} else if ( seh_excn_code == 2 ) { \
ON_SIGSEGV; \
} else { \
ON_DIV_ZERO; \
}
#define BEGIN_CATCH TRY_BEGIN(seh_unwind_to, catchDivZero, ON_EXCN)
#define END_CATCH TRY_END()
#elif defined(_MSC_VER)
#define BEGIN_CATCH __try {
#define END_CATCH } __except ( ( ((GetExceptionCode() == EXCEPTION_FLT_DIVIDE_BY_ZERO) || (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) || (GetExceptionCode() == EXCEPTION_STACK_OVERFLOW) || (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION)) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH ) ) { \
switch ( (GetExceptionCode()) ) { \
case EXCEPTION_FLT_DIVIDE_BY_ZERO: \
case EXCEPTION_INT_DIVIDE_BY_ZERO:
ON_DIV_ZERO; break; \
case EXCEPTION_STACK_OVERFLOW: \
ON_STACK_OVERFLOW; break; \
case EXCEPTION_ACCESS_VIOLATION: \
ON_SIGSEGV; break; \
} \
}
#else
#error Cannot determine what sort of Windows system this is
#endif
#endif /* WIN32_SEH_EXCN_H */
/* -----------------------------------------------------------------------------
*
* (c) The GHC Team 1998-2000
*
* Error Handling implementations for windows
*
* ---------------------------------------------------------------------------*/
#include "Rts.h"
#include "ghcconfig.h"
#include "veh_excn.h"
#include <assert.h>
/////////////////////////////////
// Exception / signal handlers.
/////////////////////////////////
// Define some values for the ordering of VEH Handlers:
// - CALL_FIRST means call this exception handler first
// - CALL_LAST means call this exception handler last
#define CALL_FIRST 1
#define CALL_LAST 0
// this should be in <excpt.h>, but it's been removed from MinGW distributions
#ifndef EH_UNWINDING
#define EH_UNWINDING 0x02
#endif /* EH_UNWINDING */
// Registered exception handler
PVOID __hs_handle = NULL;
long WINAPI __hs_exception_handler(struct _EXCEPTION_POINTERS *exception_data)
{
long action = EXCEPTION_CONTINUE_SEARCH;
// When the system unwinds the VEH stack after having handled an excn,
// return immediately.
if ((exception_data->ExceptionRecord->ExceptionFlags & EH_UNWINDING) == 0)
{
// Error handling cases covered by this implementation.
switch (exception_data->ExceptionRecord->ExceptionCode) {
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
case EXCEPTION_INT_DIVIDE_BY_ZERO:
fprintf(stdout, "divide by zero\n");
action = EXCEPTION_CONTINUE_EXECUTION;
break;
case EXCEPTION_STACK_OVERFLOW:
fprintf(stdout, "C stack overflow in generated code\n");
action = EXCEPTION_CONTINUE_EXECUTION;
break;
case EXCEPTION_ACCESS_VIOLATION:
fprintf(stdout, "Segmentation fault/access violation in generated code\n");
action = EXCEPTION_CONTINUE_EXECUTION;
break;
default:;
}
// If an error has occurred and we've decided to continue execution
// then we've done so to prevent something else from handling the error.
// But the correct action is still to exit as fast as possible.
if (EXCEPTION_CONTINUE_EXECUTION == action)
{
fflush(stdout);
stg_exit(1);
}
}
return action;
}
void __register_hs_exception_handler( void )
{
// Allow the VEH handler to be registered only once.
if (NULL == __hs_handle)
{
__hs_handle = AddVectoredExceptionHandler(CALL_FIRST, __hs_exception_handler);
// should the handler not be registered this will return a null.
assert(__hs_handle);
}
else
{
errorBelch("There is no need to call __register_hs_exception_handler() twice, VEH handlers are global per process.");
}
}
void __unregister_hs_exception_handler( void )
{
if (__hs_handle != NULL)
{
// Should the return value be checked? we're terminating anyway.
RemoveVectoredExceptionHandler(__hs_handle);
__hs_handle = NULL;
}
else
{
errorBelch("__unregister_hs_exception_handler() called without having called __register_hs_exception_handler() first.");
}
}
/* -----------------------------------------------------------------------------
*
* (c) The GHC Team 1998-2000
*
* Header for windows Error Handling implementations
*
* ---------------------------------------------------------------------------*/
#ifndef WIN32_VEH_EXCN_H
#define WIN32_VEH_EXCN_H
#include <stdio.h>
#include <stdlib.h>
#include <Rts.h>
// Stuff needed to install and use VEH exception handlers
#include <excpt.h>
#include <windows.h>
// Exception handling.
//
// On Windows, the default action for things like division by zero and
// segfaults is to pop up a Dr. Watson error reporting dialog if the exception
// is unhandled by the user code.
//
// This is a pain when we are SSHed into a Windows machine, or when we
// want to debug a problem with gdb (gdb will get a first and second chance to
// handle the exception, but if it doesn't the pop-up will show).
//
//
// Previously this code was handled using SEH (Structured Exception Handlers)
// however each compiler and platform have different ways of dealing with SEH.
//
// MSVC compilers have the keywords __try, __catch and __except to have the
// compiler generate the appropriate SEH handler code for you.
//
// MinGW compilers have no such keywords and require you to manually set the
// SEH Handlers, however because SEH is implemented differently in x86 and x64
// the methods to use them in GCC differs.
//
// x86: SEH is based on the stack, the SEH handlers are available at FS[0].
// On startup one would only need to add a new handler there. This has
// a number of issues such as hard to share handlers and it can be exploited.
//
// x64: In order to fix the issues with the way SEH worked in x86, on x64 SEH handlers
// are statically compiled and added to the .pdata section by the compiler.
// Instead of being thread global they can now be Image global since you have to
// specify the RVA of the region of code that the handlers govern.
//
// You can on x64 Dynamically allocate SEH handlers, but it seems that (based on
// experimentation and it's very under-documented) that the dynamic calls cannot override
// static SEH handlers in the .pdata section.
//
// Because of this and because GHC no longer needs to support < windows XP, the better
// alternative for handling errors would be using the in XP introduced VEH.
//
// The bonus is because VEH (Vectored Exception Handler) are a runtime construct the API
// is the same for both x86 and x64 (note that the Context object does contain CPU specific
// structures) and the calls are the same cross compilers. Which means this file can be
// simplified quite a bit.
// Using VEH also means we don't have to worry about the dynamic code generated by GHCi.
// Prototype of the VEH callback function.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms681419(v=vs.85).aspx
//
long WINAPI __hs_exception_handler( struct _EXCEPTION_POINTERS *exception_data );
// prototypes to the functions doing the registration and unregistration of the VEH handlers
void __register_hs_exception_handler( void );
void __unregister_hs_exception_handler( void );
#endif /* WIN32_VEH_EXCN_H */
......@@ -7,8 +7,7 @@ test('testblockalloc',
# only GHCi triggers the bug, but we run the test all ways for completeness.
test('bug1010', normal, compile_and_run, ['+RTS -c -RTS'])
test('derefnull',
[when(opsys('mingw32'), expect_broken(6079)),
# LLVM Optimiser considers dereference of a null pointer
[# LLVM Optimiser considers dereference of a null pointer
# undefined and marks the code as unreachable which means
# that later optimisations remove it altogether.
omit_ways(['optllvm']),
......@@ -21,17 +20,24 @@ test('derefnull',
# SIGBUS on OX X (PPC and x86 only; amd64 gives SEGV)
when(platform('i386-apple-darwin'), exit_code(138)),
when(platform('powerpc-apple-darwin'), exit_code(138)),
when(opsys('mingw32'), exit_code(1))],
when(opsys('mingw32'), exit_code(1)),
# since these test are supposed to crash the
# profile report will be empty always.
# so disable the check for profiling
when(opsys('mingw32'), omit_ways(prof_ways))],
compile_and_run, [''])
test('divbyzero',
[when(opsys('mingw32'), expect_broken(6079)),
# SIGFPE on Linux
[# SIGFPE on Linux
exit_code(136),
# Apparently the output can be different on different
# Linux setups, so just ignore it. As long as we get
# the right exit code we're OK.
when(opsys('linux'), ignore_output),
when(opsys('mingw32'), exit_code(1))],
when(opsys('mingw32'), exit_code(1)),
# since these test are supposed to crash the
# profile report will be empty always.
# so disable the check for profiling
when(opsys('mingw32'), omit_ways(prof_ways))],
compile_and_run, [''])
test('outofmem', when(opsys('darwin'), skip),
......
Segmentation fault/access violation in generated code
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment