Skip to content

Commit

Permalink
runtime: handle Go calls C calls Go panic correctly on windows/386
Browse files Browse the repository at this point in the history
32-bit Windows uses "structured exception handling" (SEH) to
handle hardware faults: that there is a per-thread linked list
of fault handlers maintained in user space instead of
something like Unix's signal handlers. The structures in the
linked list are required to live on the OS stack, and the
usual discipline is that the function that pushes a record
(allocated from the current stack frame) onto the list pops
that record before returning. Not to pop the entry before
returning creates a dangling pointer error: the list head
points to a stack frame that no longer exists.

Go pushes an SEH record in the top frame of every OS thread,
and that record suffices for all Go execution on that thread,
at least until cgo gets involved.

If we call into C using cgo, that called C code may push its
own SEH records, but by the convention it must pop them before
returning back to the Go code. We assume it does, and that's
fine.

If the C code calls back into Go, we want the Go SEH handler
to become active again, not whatever C has set up. So
runtime.callbackasm1, which handles a call from C back into
Go, pushes a new SEH record before calling the Go code and
pops it when the Go code returns. That's also fine.

It can happen that when Go calls C calls Go like this, the
inner Go code panics. We allow a defer in the outer Go to
recover the panic, effectively wiping not only the inner Go
frames but also the C calls. This sequence was not popping the
SEH stack up to what it was before the cgo calls, so it was
creating the dangling pointer warned about above. When
eventually the m stack was used enough to overwrite the
dangling SEH records, the SEH chain was lost, and any future
panic would not end up in Go's handler.

The bug in TestCallbackPanic and friends was thus creating a
situation where TestSetPanicOnFault - which causes a hardware
fault - would not find the Go fault handler and instead crash
the binary.

Add checks to TestCallbackPanicLocked to diagnose the mistake
in that test instead of leaving a bad state for another test
case to stumble over.

Fix bug by restoring SEH chain during deferred "endcgo"
cleanup.

This bug is likely present in Go 1.2.1, but since it depends
on Go calling C calling Go, with the inner Go panicking and
the outer Go recovering the panic, it seems not important
enough to bother fixing before Go 1.3. Certainly no one has
complained.

Fixes #7470.

LGTM=alex.brainman
R=golang-codereviews, alex.brainman
CC=golang-codereviews, iant, khr
https://golang.org/cl/71440043
  • Loading branch information
rsc committed Mar 5, 2014
1 parent 4e7049b commit 1249d3a
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 33 deletions.
12 changes: 12 additions & 0 deletions src/pkg/runtime/cgocall.c
Expand Up @@ -98,6 +98,7 @@ void
runtime·cgocall(void (*fn)(void*), void *arg)
{
Defer d;
SEHUnwind sehunwind;

if(m->racecall) {
runtime·asmcgocall(fn, arg);
Expand Down Expand Up @@ -130,6 +131,14 @@ runtime·cgocall(void (*fn)(void*), void *arg)
d.argp = (void*)-1; // unused because unlockm never recovers
d.special = true;
g->defer = &d;

// Record current SEH for restoration during endcgo.
// This matters most when the execution stops due to panic
// and the called C code isn't given a chance to clean up
// the SEHs it has pushed.
sehunwind.seh = runtime·getseh();
sehunwind.link = m->sehunwind;
m->sehunwind = &sehunwind;

m->ncgo++;

Expand Down Expand Up @@ -166,6 +175,9 @@ endcgo(void)
m->cgomal = nil;
}

runtime·setseh(m->sehunwind->seh);
m->sehunwind = m->sehunwind->link;

if(raceenabled)
runtime·raceacquire(&cgosync);
}
Expand Down
4 changes: 4 additions & 0 deletions src/pkg/runtime/export_test.go
Expand Up @@ -90,3 +90,7 @@ var MemclrBytes = memclrBytes
func gogoBytes() int32

var GogoBytes = gogoBytes

func getseh_go() uintptr

var GetSEH = getseh_go
16 changes: 4 additions & 12 deletions src/pkg/runtime/proc.c
Expand Up @@ -601,13 +601,11 @@ runtime·starttheworld(void)
void
runtime·mstart(void)
{
#ifdef GOOS_windows
#ifdef GOARCH_386
#ifdef GOOSARCH_windows_386
// It is used by windows-386 only. Unfortunately, seh needs
// to be located on os stack, and mstart runs on os stack
// for both m0 and m.
SEH seh;
#endif
#endif

if(g != m->g0)
Expand All @@ -619,10 +617,8 @@ runtime·mstart(void)
runtime·gosave(&m->g0->sched);
m->g0->sched.pc = (uintptr)-1; // make sure it is never used
m->g0->stackguard = m->g0->stackguard0; // cgo sets only stackguard0, copy it to stackguard
#ifdef GOOS_windows
#ifdef GOARCH_386
#ifdef GOOSARCH_windows_386
m->seh = &seh;
#endif
#endif
runtime·asminit();
runtime·minit();
Expand Down Expand Up @@ -775,14 +771,12 @@ runtime·needm(byte x)
g->stackguard = (uintptr)(&x - 32*1024);
g->stackguard0 = g->stackguard;

#ifdef GOOS_windows
#ifdef GOARCH_386
#ifdef GOOSARCH_windows_386
// On windows/386, we need to put an SEH frame (two words)
// somewhere on the current stack. We are called from cgocallback_gofunc
// and we know that it will leave two unused words below m->curg->sched.sp.
// Use those.
m->seh = (SEH*)((uintptr*)&x + 1);
#endif
#endif

// Initialize this thread to use the m.
Expand Down Expand Up @@ -862,10 +856,8 @@ runtime·dropm(void)
// Undo whatever initialization minit did during needm.
runtime·unminit();

#ifdef GOOS_windows
#ifdef GOARCH_386
#ifdef GOOSARCH_windows_386
m->seh = nil; // reset dangling typed pointer
#endif
#endif

// Clear m and g, and return m to the extra list.
Expand Down
30 changes: 30 additions & 0 deletions src/pkg/runtime/runtime.h
Expand Up @@ -86,6 +86,7 @@ typedef struct Complex64 Complex64;
typedef struct Complex128 Complex128;
typedef struct LibCall LibCall;
typedef struct SEH SEH;
typedef struct SEHUnwind SEHUnwind;
typedef struct WinCallbackContext WinCallbackContext;
typedef struct Timers Timers;
typedef struct Timer Timer;
Expand Down Expand Up @@ -241,11 +242,19 @@ struct LibCall
uintptr r2;
uintptr err; // error number
};

struct SEH
{
void* prev;
void* handler;
};

struct SEHUnwind
{
SEHUnwind* link;
SEH* seh;
};

// describes how to handle callback
struct WinCallbackContext
{
Expand Down Expand Up @@ -295,6 +304,16 @@ struct G
uintptr racectx;
uintptr end[];
};

// Define a symbol for windows/386 because that is the only
// system with SEH handling, and we end up checking that
// repeatedly.
#ifdef GOOS_windows
#ifdef GOARCH_386
#define GOOSARCH_windows_386
#endif
#endif

struct M
{
G* g0; // goroutine with scheduling stack
Expand Down Expand Up @@ -378,6 +397,7 @@ struct M
byte* errstr;
#endif
SEH* seh;
SEHUnwind* sehunwind;
uintptr end[];
};

Expand Down Expand Up @@ -947,6 +967,16 @@ void* runtime·funcdata(Func*, int32);
int32 runtime·setmaxthreads(int32);
G* runtime·timejump(void);

// On Windows 386, we have functions for saving and restoring
// the SEH values; elsewhere #define them away.
#ifdef GOOSARCH_windows_386
SEH* runtime·getseh(void);
void runtime·setseh(SEH*);
#else
#define runtime·getseh() nil
#define runtime·setseh(x) do{}while(0)
#endif

#pragma varargck argpos runtime·printf 1
#pragma varargck type "c" int32
#pragma varargck type "d" int32
Expand Down
4 changes: 4 additions & 0 deletions src/pkg/runtime/runtime1.goc
Expand Up @@ -43,6 +43,10 @@ func gogoBytes() (x int32) {
x = RuntimeGogoBytes;
}

func getseh_go() (x uintptr) {
x = (uintptr)runtime·getseh();
}

func typestring(e Eface) (s String) {
s = *e.type->string;
}
Expand Down
10 changes: 10 additions & 0 deletions src/pkg/runtime/sys_windows_386.s
Expand Up @@ -390,3 +390,13 @@ TEXT runtime·usleep2(SB),NOSPLIT,$20
CALL AX
MOVL BP, SP
RET

TEXT runtime·getseh(SB),NOSPLIT,$0
MOVL 0(FS), AX
RET

TEXT runtime·setseh(SB),NOSPLIT,$0
MOVL seh+0(FP), AX
MOVL AX, 0(FS)
RET

57 changes: 36 additions & 21 deletions src/pkg/runtime/syscall_windows_test.go
Expand Up @@ -177,11 +177,25 @@ func TestCallbackGC(t *testing.T) {
nestedCall(t, runtime.GC)
}

func TestCallbackPanic(t *testing.T) {
// Make sure panic during callback unwinds properly.
if runtime.LockedOSThread() {
t.Fatal("locked OS thread on entry to TestCallbackPanic")
// NOTE: TestCallbackPanicLocked must precede the other TestCallbackPanic variants.
// The SEH logic is testing that SEH is properly restored during the panic.
// The bug we're looking for (issue 7470) used to leave SEH in the wrong place,
// but future panics would leave it in that same wrong place. So if one of the other
// tests runs first, TestCallbackPanicLocked will see SEH not changing and
// incorrectly infer that it is being restored properly.
// The SEH checks are only safe (not racy) with the OS thread locked.
//
// The fallback is that even if this test doesn't notice, TestSetPanicOnFault will
// crash if it runs on the same thread after one of these tests.

func TestCallbackPanicLocked(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if !runtime.LockedOSThread() {
t.Fatal("runtime.LockOSThread didn't")
}
oldSEH := runtime.GetSEH()
defer func() {
s := recover()
if s == nil {
Expand All @@ -190,27 +204,21 @@ func TestCallbackPanic(t *testing.T) {
if s.(string) != "callback panic" {
t.Fatal("wrong panic:", s)
}
if runtime.LockedOSThread() {
t.Fatal("locked OS thread on exit from TestCallbackPanic")
if !runtime.LockedOSThread() {
t.Fatal("lost lock on OS thread after panic")
}
if newSEH := runtime.GetSEH(); oldSEH != newSEH {
t.Fatalf("SEH not restored after panic: %#x became %#x", oldSEH, newSEH)
}
}()
nestedCall(t, func() { panic("callback panic") })
panic("nestedCall returned")
}

func TestCallbackPanicLoop(t *testing.T) {
// Make sure we don't blow out m->g0 stack.
for i := 0; i < 100000; i++ {
TestCallbackPanic(t)
}
}

func TestCallbackPanicLocked(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()

if !runtime.LockedOSThread() {
t.Fatal("runtime.LockOSThread didn't")
func TestCallbackPanic(t *testing.T) {
// Make sure panic during callback unwinds properly.
if runtime.LockedOSThread() {
t.Fatal("locked OS thread on entry to TestCallbackPanic")
}
defer func() {
s := recover()
Expand All @@ -220,14 +228,21 @@ func TestCallbackPanicLocked(t *testing.T) {
if s.(string) != "callback panic" {
t.Fatal("wrong panic:", s)
}
if !runtime.LockedOSThread() {
t.Fatal("lost lock on OS thread after panic")
if runtime.LockedOSThread() {
t.Fatal("locked OS thread on exit from TestCallbackPanic")
}
}()
nestedCall(t, func() { panic("callback panic") })
panic("nestedCall returned")
}

func TestCallbackPanicLoop(t *testing.T) {
// Make sure we don't blow out m->g0 stack.
for i := 0; i < 100000; i++ {
TestCallbackPanic(t)
}
}

func TestBlockingCallback(t *testing.T) {
c := make(chan int)
go func() {
Expand Down

0 comments on commit 1249d3a

Please sign in to comment.