Skip to content

Commit 1b49a86

Browse files
committed
runtime/cgo: catch EXC_BAD_ACCESS on darwin/arm
The Go builders (and standard development cycle) for programs on iOS require running the programs under lldb. Unfortunately lldb intercepts SIGSEGV and will not give it back. https://llvm.org/bugs/show_bug.cgi?id=22868 We get around this by never letting lldb see the SIGSEGV. On darwin, Unix signals are emulated on top of mach exceptions. The debugger registers a task-level mach exception handler. We register a thread-level exception handler which acts as a faux signal handler. The thread-level handler gets precedence over the task-level handler, so we can turn the exception EXC_BAD_ACCESS into a panic before lldb can see it. Fixes #10043 Change-Id: I64d7c310dfa7ecf60eb1e59f094966520d473335 Reviewed-on: https://go-review.googlesource.com/7072 Reviewed-by: Minux Ma <[email protected]> Run-TryBot: David Crawshaw <[email protected]>
1 parent 1f3fe91 commit 1b49a86

7 files changed

+303
-0
lines changed

src/runtime/cgo/gcc_darwin_arm.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ threadentry(void *v)
8484
ts = *(ThreadStart*)v;
8585
free(v);
8686

87+
darwin_arm_init_thread_exception_port();
88+
8789
crosscall_arm1(ts.fn, setg_gcc, (void*)ts.g);
8890
return nil;
8991
}
@@ -145,5 +147,7 @@ x_cgo_init(G *g, void (*setg)(void*), void **tlsg, void **tlsbase)
145147
// yes, tlsbase from mrc might not be correctly aligned.
146148
inittls(tlsg, (void**)((uintptr)tlsbase & ~3));
147149

150+
darwin_arm_init_mach_exception_handler();
151+
darwin_arm_init_thread_exception_port();
148152
init_working_dir();
149153
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright 2015 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Emulation of the Unix signal SIGSEGV.
6+
//
7+
// On iOS, Go tests and apps under development are run by lldb.
8+
// The debugger uses a task-level exception handler to intercept signals.
9+
// Despite having a 'handle' mechanism like gdb, lldb will not allow a
10+
// SIGSEGV to pass to the running program. For Go, this means we cannot
11+
// generate a panic, which cannot be recovered, and so tests fail.
12+
//
13+
// We work around this by registering a thread-level mach exception handler
14+
// and intercepting EXC_BAD_ACCESS. The kernel offers thread handlers a
15+
// chance to resolve exceptions before the task handler, so we can generate
16+
// the panic and avoid lldb's SIGSEGV handler.
17+
//
18+
// If you want to debug a segfault under lldb, compile the standard library
19+
// with the build tag lldb:
20+
//
21+
// go test -tags lldb -installsuffix lldb
22+
23+
// +build darwin,arm,!lldb
24+
25+
// TODO(crawshaw): darwin,arm64,!lldb
26+
27+
#include <limits.h>
28+
#include <pthread.h>
29+
#include <stdio.h>
30+
#include <signal.h>
31+
#include <stdlib.h>
32+
#include <unistd.h>
33+
34+
#include <mach/arm/thread_status.h>
35+
#include <mach/exception_types.h>
36+
#include <mach/mach.h>
37+
#include <mach/mach_init.h>
38+
#include <mach/mach_port.h>
39+
#include <mach/thread_act.h>
40+
#include <mach/thread_status.h>
41+
42+
#include "libcgo.h"
43+
44+
uintptr_t x_cgo_panicmem;
45+
46+
static pthread_mutex_t mach_exception_handler_port_set_mu;
47+
static mach_port_t mach_exception_handler_port_set = MACH_PORT_NULL;
48+
49+
kern_return_t
50+
catch_exception_raise(
51+
mach_port_t exception_port,
52+
mach_port_t thread,
53+
mach_port_t task,
54+
exception_type_t exception,
55+
exception_data_t code_vector,
56+
mach_msg_type_number_t code_count)
57+
{
58+
kern_return_t ret;
59+
arm_unified_thread_state_t thread_state;
60+
mach_msg_type_number_t state_count = ARM_UNIFIED_THREAD_STATE_COUNT;
61+
62+
// Returning KERN_SUCCESS intercepts the exception.
63+
//
64+
// Returning KERN_FAILURE lets the exception fall through to the
65+
// next handler, which is the standard signal emulation code
66+
// registered on the task port.
67+
68+
if (exception != EXC_BAD_ACCESS) {
69+
return KERN_FAILURE;
70+
}
71+
72+
ret = thread_get_state(thread, ARM_UNIFIED_THREAD_STATE, (thread_state_t)&thread_state, &state_count);
73+
if (ret) {
74+
fprintf(stderr, "runtime/cgo: thread_get_state failed: %d\n", ret);
75+
abort();
76+
}
77+
78+
// Bounce call to sigpanic through asm that makes it look like
79+
// we call sigpanic directly from the faulting code.
80+
thread_state.ts_32.__r[1] = thread_state.ts_32.__lr;
81+
thread_state.ts_32.__r[2] = thread_state.ts_32.__pc;
82+
thread_state.ts_32.__pc = x_cgo_panicmem;
83+
84+
ret = thread_set_state(thread, ARM_UNIFIED_THREAD_STATE, (thread_state_t)&thread_state, state_count);
85+
if (ret) {
86+
fprintf(stderr, "runtime/cgo: thread_set_state failed: %d\n", ret);
87+
abort();
88+
}
89+
90+
return KERN_SUCCESS;
91+
}
92+
93+
void
94+
darwin_arm_init_thread_exception_port()
95+
{
96+
// Called by each new OS thread to bind its EXC_BAD_ACCESS exception
97+
// to mach_exception_handler_port_set.
98+
int ret;
99+
mach_port_t port = MACH_PORT_NULL;
100+
101+
ret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
102+
if (ret) {
103+
fprintf(stderr, "runtime/cgo: mach_port_allocate failed: %d\n", ret);
104+
abort();
105+
}
106+
ret = mach_port_insert_right(
107+
mach_task_self(),
108+
port,
109+
port,
110+
MACH_MSG_TYPE_MAKE_SEND);
111+
if (ret) {
112+
fprintf(stderr, "runtime/cgo: mach_port_insert_right failed: %d\n", ret);
113+
abort();
114+
}
115+
116+
ret = thread_set_exception_ports(
117+
mach_thread_self(),
118+
EXC_MASK_BAD_ACCESS,
119+
port,
120+
EXCEPTION_DEFAULT,
121+
THREAD_STATE_NONE);
122+
if (ret) {
123+
fprintf(stderr, "runtime/cgo: thread_set_exception_ports failed: %d\n", ret);
124+
abort();
125+
}
126+
127+
ret = pthread_mutex_lock(&mach_exception_handler_port_set_mu);
128+
if (ret) {
129+
fprintf(stderr, "runtime/cgo: pthread_mutex_lock failed: %d\n", ret);
130+
abort();
131+
}
132+
ret = mach_port_move_member(
133+
mach_task_self(),
134+
port,
135+
mach_exception_handler_port_set);
136+
if (ret) {
137+
fprintf(stderr, "runtime/cgo: mach_port_move_member failed: %d\n", ret);
138+
abort();
139+
}
140+
ret = pthread_mutex_unlock(&mach_exception_handler_port_set_mu);
141+
if (ret) {
142+
fprintf(stderr, "runtime/cgo: pthread_mutex_unlock failed: %d\n", ret);
143+
abort();
144+
}
145+
}
146+
147+
static void*
148+
mach_exception_handler(void *port)
149+
{
150+
// Calls catch_exception_raise.
151+
extern boolean_t exc_server();
152+
mach_msg_server(exc_server, 2048, (mach_port_t)port, 0);
153+
abort(); // never returns
154+
}
155+
156+
void
157+
darwin_arm_init_mach_exception_handler()
158+
{
159+
pthread_mutex_init(&mach_exception_handler_port_set_mu, NULL);
160+
161+
// Called once per process to initialize a mach port server, listening
162+
// for EXC_BAD_ACCESS thread exceptions.
163+
int ret;
164+
pthread_t thr = NULL;
165+
pthread_attr_t attr;
166+
167+
ret = mach_port_allocate(
168+
mach_task_self(),
169+
MACH_PORT_RIGHT_PORT_SET,
170+
&mach_exception_handler_port_set);
171+
if (ret) {
172+
fprintf(stderr, "runtime/cgo: mach_port_allocate failed for port_set: %d\n", ret);
173+
abort();
174+
}
175+
176+
// Start a thread to handle exceptions.
177+
pthread_attr_init(&attr);
178+
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
179+
ret = pthread_create(&thr, &attr, mach_exception_handler, (void*)mach_exception_handler_port_set);
180+
if (ret) {
181+
fprintf(stderr, "runtime/cgo: pthread_create failed: %d\n", ret);
182+
abort();
183+
}
184+
pthread_attr_destroy(&attr);
185+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2015 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build darwin
6+
// +build arm arm64
7+
// +build lldb
8+
9+
#include <stdint.h>
10+
11+
uintptr_t x_cgo_panicmem;
12+
13+
void darwin_arm_init_thread_exception_port() {}
14+
void darwin_arm_init_mach_exception_handler() {}

src/runtime/cgo/libcgo.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,13 @@ void crosscall_386(void (*fn)(void));
6363
* Prints error then calls abort. For linux and android.
6464
*/
6565
void fatalf(const char* format, ...);
66+
67+
/*
68+
* Registers the current mach thread port for EXC_BAD_ACCESS processing.
69+
*/
70+
void darwin_arm_init_thread_exception_port(void);
71+
72+
/*
73+
* Starts a mach message server processing EXC_BAD_ACCESS.
74+
*/
75+
void darwin_arm_init_mach_exception_handler(void);

src/runtime/cgo/signal_darwin_arm.s

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2015 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
#include "textflag.h"
6+
7+
// panicmem is the entrypoint for SIGSEGV as intercepted via a
8+
// mach thread port as EXC_BAD_ACCESS. As the segfault may have happened
9+
// in C code, we first need to load_g then call panicmem.
10+
//
11+
// R1 - LR at moment of fault
12+
// R2 - PC at moment of fault
13+
TEXT ·panicmem(SB),NOSPLIT,$-4
14+
// If in external C code, we need to load the g register.
15+
BL runtime·load_g(SB)
16+
CMP $0, g
17+
BNE ongothread
18+
19+
// On a foreign thread. We call badsignal, which will, if all
20+
// goes according to plan, not return.
21+
SUB $4, R13
22+
MOVW $11, R1
23+
MOVW $11, R2
24+
MOVM.DB.W [R1,R2], (R13)
25+
// TODO: badsignal should not return, but it does. Issue #10139.
26+
//BL runtime·badsignal(SB)
27+
MOVW $139, R1
28+
MOVW R1, 4(R13)
29+
B runtime·exit(SB)
30+
31+
ongothread:
32+
// Trigger a SIGSEGV panic.
33+
//
34+
// The goal is to arrange the stack so it looks like the runtime
35+
// function sigpanic was called from the PC that faulted. It has
36+
// to be sigpanic, as the stack unwinding code in traceback.go
37+
// looks explicitly for it.
38+
//
39+
// To do this we call into runtime·setsigsegv, which sets the
40+
// appropriate state inside the g object. We give it the faulting
41+
// PC on the stack, then put it in the LR before calling sigpanic.
42+
MOVM.DB.W [R1,R2], (R13)
43+
BL runtime·setsigsegv(SB)
44+
MOVM.IA.W (R13), [R1,R2]
45+
46+
SUB $4, R13
47+
MOVW R1, 0(R13)
48+
MOVW R2, R14
49+
B runtime·sigpanic(SB)

src/runtime/cgo/signal_darwin_armx.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2015 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build darwin
6+
// +build arm arm64
7+
8+
package cgo
9+
10+
import "unsafe"
11+
12+
//go:cgo_import_static x_cgo_panicmem
13+
//go:linkname x_cgo_panicmem x_cgo_panicmem
14+
var x_cgo_panicmem uintptr
15+
16+
// TODO(crawshaw): move this into x_cgo_init, it will not run until
17+
// runtime has finished loading, which may be after its use.
18+
func init() {
19+
x_cgo_panicmem = funcPC(panicmem)
20+
}
21+
22+
func funcPC(f interface{}) uintptr {
23+
var ptrSize = unsafe.Sizeof(uintptr(0))
24+
return **(**uintptr)(add(unsafe.Pointer(&f), ptrSize))
25+
}
26+
27+
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
28+
return unsafe.Pointer(uintptr(p) + x)
29+
}
30+
31+
func panicmem()

src/runtime/sigpanic_unix.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,13 @@ func sigpanic() {
4141
}
4242
panic(errorString(sigtable[g.sig].name))
4343
}
44+
45+
// setsigsegv is used on darwin/arm{,64} to fake a segmentation fault.
46+
//go:nosplit
47+
func setsigsegv(pc uintptr) {
48+
g := getg()
49+
g.sig = _SIGSEGV
50+
g.sigpc = pc
51+
g.sigcode0 = _SEGV_MAPERR
52+
g.sigcode1 = 0 // TODO: emulate si_addr
53+
}

0 commit comments

Comments
 (0)