Skip to content

Commit 4f9fe6d

Browse files
prattmicgopherbot
authored andcommitted
runtime: allow update of system stack bounds on callback from C thread
[This is a redo of CL 525455 with the test fixed on darwin by defining _XOPEN_SOURCE, and disabled with android, musl, and openbsd, which do not provide getcontext.] Since CL 495855, Ms are cached for C threads calling into Go, including the stack bounds of the system stack. Some C libraries (e.g., coroutine libraries) do manual stack management and may change stacks between calls to Go on the same thread. Changing the stack if there is more Go up the stack would be problematic. But if the calls are completely independent there is no particular reason for Go to care about the changing stack boundary. Thus, this CL allows the stack bounds to change in such cases. The primary downside here (besides additional complexity) is that normal systems that do not manipulate the stack may not notice unintentional stack corruption as quickly as before. Note that callbackUpdateSystemStack is written to be usable for the initial setup in needm as well as updating the stack in cgocallbackg. Fixes #62440. For #62130. Change-Id: I0fe0134f865932bbaff1fc0da377c35c013bd768 Reviewed-on: https://go-review.googlesource.com/c/go/+/527715 Run-TryBot: Michael Pratt <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Michael Pratt <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent 2399302 commit 4f9fe6d

File tree

5 files changed

+243
-24
lines changed

5 files changed

+243
-24
lines changed

src/runtime/cgocall.go

+70
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,73 @@ func cgocall(fn, arg unsafe.Pointer) int32 {
206206
return errno
207207
}
208208

209+
// Set or reset the system stack bounds for a callback on sp.
210+
//
211+
// Must be nosplit because it is called by needm prior to fully initializing
212+
// the M.
213+
//
214+
//go:nosplit
215+
func callbackUpdateSystemStack(mp *m, sp uintptr, signal bool) {
216+
g0 := mp.g0
217+
if sp > g0.stack.lo && sp <= g0.stack.hi {
218+
// Stack already in bounds, nothing to do.
219+
return
220+
}
221+
222+
if mp.ncgo > 0 {
223+
// ncgo > 0 indicates that this M was in Go further up the stack
224+
// (it called C and is now receiving a callback). It is not
225+
// safe for the C call to change the stack out from under us.
226+
227+
// Note that this case isn't possible for signal == true, as
228+
// that is always passing a new M from needm.
229+
230+
// Stack is bogus, but reset the bounds anyway so we can print.
231+
hi := g0.stack.hi
232+
lo := g0.stack.lo
233+
g0.stack.hi = sp + 1024
234+
g0.stack.lo = sp - 32*1024
235+
g0.stackguard0 = g0.stack.lo + stackGuard
236+
237+
print("M ", mp.id, " procid ", mp.procid, " runtime: cgocallback with sp=", hex(sp), " out of bounds [", hex(lo), ", ", hex(hi), "]")
238+
print("\n")
239+
exit(2)
240+
}
241+
242+
// This M does not have Go further up the stack. However, it may have
243+
// previously called into Go, initializing the stack bounds. Between
244+
// that call returning and now the stack may have changed (perhaps the
245+
// C thread is running a coroutine library). We need to update the
246+
// stack bounds for this case.
247+
//
248+
// Set the stack bounds to match the current stack. If we don't
249+
// actually know how big the stack is, like we don't know how big any
250+
// scheduling stack is, but we assume there's at least 32 kB. If we
251+
// can get a more accurate stack bound from pthread, use that, provided
252+
// it actually contains SP..
253+
g0.stack.hi = sp + 1024
254+
g0.stack.lo = sp - 32*1024
255+
if !signal && _cgo_getstackbound != nil {
256+
// Don't adjust if called from the signal handler.
257+
// We are on the signal stack, not the pthread stack.
258+
// (We could get the stack bounds from sigaltstack, but
259+
// we're getting out of the signal handler very soon
260+
// anyway. Not worth it.)
261+
var bounds [2]uintptr
262+
asmcgocall(_cgo_getstackbound, unsafe.Pointer(&bounds))
263+
// getstackbound is an unsupported no-op on Windows.
264+
//
265+
// Don't use these bounds if they don't contain SP. Perhaps we
266+
// were called by something not using the standard thread
267+
// stack.
268+
if bounds[0] != 0 && sp > bounds[0] && sp <= bounds[1] {
269+
g0.stack.lo = bounds[0]
270+
g0.stack.hi = bounds[1]
271+
}
272+
}
273+
g0.stackguard0 = g0.stack.lo + stackGuard
274+
}
275+
209276
// Call from C back to Go. fn must point to an ABIInternal Go entry-point.
210277
//
211278
//go:nosplit
@@ -216,6 +283,9 @@ func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) {
216283
exit(2)
217284
}
218285

286+
sp := gp.m.g0.sched.sp // system sp saved by cgocallback.
287+
callbackUpdateSystemStack(gp.m, sp, false)
288+
219289
// The call from C is on gp.m's g0 stack, so we must ensure
220290
// that we stay on that M. We have to do this before calling
221291
// exitsyscall, since it would otherwise be free to move us to

src/runtime/crash_cgo_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -869,3 +869,20 @@ func TestEnsureBindM(t *testing.T) {
869869
t.Errorf("expected %q, got %v", want, got)
870870
}
871871
}
872+
873+
func TestStackSwitchCallback(t *testing.T) {
874+
t.Parallel()
875+
switch runtime.GOOS {
876+
case "windows", "plan9", "android", "openbsd": // no getcontext
877+
t.Skipf("skipping test on %s", runtime.GOOS)
878+
}
879+
got := runTestProg(t, "testprogcgo", "StackSwitchCallback")
880+
skip := "SKIP\n"
881+
if got == skip {
882+
t.Skip("skipping on musl/bionic libc")
883+
}
884+
want := "OK\n"
885+
if got != want {
886+
t.Errorf("expected %q, got %v", want, got)
887+
}
888+
}

src/runtime/proc.go

+9-24
Original file line numberDiff line numberDiff line change
@@ -2036,30 +2036,10 @@ func needm(signal bool) {
20362036
osSetupTLS(mp)
20372037

20382038
// Install g (= m->g0) and set the stack bounds
2039-
// to match the current stack. If we don't actually know
2040-
// how big the stack is, like we don't know how big any
2041-
// scheduling stack is, but we assume there's at least 32 kB.
2042-
// If we can get a more accurate stack bound from pthread,
2043-
// use that.
2039+
// to match the current stack.
20442040
setg(mp.g0)
2045-
gp := getg()
2046-
gp.stack.hi = getcallersp() + 1024
2047-
gp.stack.lo = getcallersp() - 32*1024
2048-
if !signal && _cgo_getstackbound != nil {
2049-
// Don't adjust if called from the signal handler.
2050-
// We are on the signal stack, not the pthread stack.
2051-
// (We could get the stack bounds from sigaltstack, but
2052-
// we're getting out of the signal handler very soon
2053-
// anyway. Not worth it.)
2054-
var bounds [2]uintptr
2055-
asmcgocall(_cgo_getstackbound, unsafe.Pointer(&bounds))
2056-
// getstackbound is an unsupported no-op on Windows.
2057-
if bounds[0] != 0 {
2058-
gp.stack.lo = bounds[0]
2059-
gp.stack.hi = bounds[1]
2060-
}
2061-
}
2062-
gp.stackguard0 = gp.stack.lo + stackGuard
2041+
sp := getcallersp()
2042+
callbackUpdateSystemStack(mp, sp, signal)
20632043

20642044
// Should mark we are already in Go now.
20652045
// Otherwise, we may call needm again when we get a signal, before cgocallbackg1,
@@ -2176,9 +2156,14 @@ func oneNewExtraM() {
21762156
// So that the destructor would invoke dropm while the non-Go thread is exiting.
21772157
// This is much faster since it avoids expensive signal-related syscalls.
21782158
//
2179-
// NOTE: this always runs without a P, so, nowritebarrierrec required.
2159+
// This always runs without a P, so //go:nowritebarrierrec is required.
2160+
//
2161+
// This may run with a different stack than was recorded in g0 (there is no
2162+
// call to callbackUpdateSystemStack prior to dropm), so this must be
2163+
// //go:nosplit to avoid the stack bounds check.
21802164
//
21812165
//go:nowritebarrierrec
2166+
//go:nosplit
21822167
func dropm() {
21832168
// Clear m and g, and return m to the extra list.
21842169
// After the call to setg we can only call nosplit functions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2023 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+
//go:build unix && !android && !openbsd
6+
7+
// Required for darwin ucontext.
8+
#define _XOPEN_SOURCE
9+
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
10+
11+
#include <assert.h>
12+
#include <pthread.h>
13+
#include <stddef.h>
14+
#include <stdio.h>
15+
#include <stdlib.h>
16+
#include <ucontext.h>
17+
18+
// musl libc does not provide getcontext, etc. Skip the test there.
19+
//
20+
// musl libc doesn't provide any direct detection mechanism. So assume any
21+
// non-glibc linux is using musl.
22+
//
23+
// Note that bionic does not provide getcontext either, but that is skipped via
24+
// the android build tag.
25+
#if defined(__linux__) && !defined(__GLIBC__)
26+
#define MUSL 1
27+
#endif
28+
#if defined(MUSL)
29+
void callStackSwitchCallbackFromThread(void) {
30+
printf("SKIP\n");
31+
exit(0);
32+
}
33+
#else
34+
35+
// Use a stack size larger than the 32kb estimate in
36+
// runtime.callbackUpdateSystemStack. This ensures that a second stack
37+
// allocation won't accidentally count as in bounds of the first stack
38+
#define STACK_SIZE (64ull << 10)
39+
40+
static ucontext_t uctx_save, uctx_switch;
41+
42+
extern void stackSwitchCallback(void);
43+
44+
static void *stackSwitchThread(void *arg) {
45+
// Simple test: callback works from the normal system stack.
46+
stackSwitchCallback();
47+
48+
// Next, verify that switching stacks doesn't break callbacks.
49+
50+
char *stack1 = malloc(STACK_SIZE);
51+
if (stack1 == NULL) {
52+
perror("malloc");
53+
exit(1);
54+
}
55+
56+
// Allocate the second stack before freeing the first to ensure we don't get
57+
// the same address from malloc.
58+
char *stack2 = malloc(STACK_SIZE);
59+
if (stack1 == NULL) {
60+
perror("malloc");
61+
exit(1);
62+
}
63+
64+
if (getcontext(&uctx_switch) == -1) {
65+
perror("getcontext");
66+
exit(1);
67+
}
68+
uctx_switch.uc_stack.ss_sp = stack1;
69+
uctx_switch.uc_stack.ss_size = STACK_SIZE;
70+
uctx_switch.uc_link = &uctx_save;
71+
makecontext(&uctx_switch, stackSwitchCallback, 0);
72+
73+
if (swapcontext(&uctx_save, &uctx_switch) == -1) {
74+
perror("swapcontext");
75+
exit(1);
76+
}
77+
78+
if (getcontext(&uctx_switch) == -1) {
79+
perror("getcontext");
80+
exit(1);
81+
}
82+
uctx_switch.uc_stack.ss_sp = stack2;
83+
uctx_switch.uc_stack.ss_size = STACK_SIZE;
84+
uctx_switch.uc_link = &uctx_save;
85+
makecontext(&uctx_switch, stackSwitchCallback, 0);
86+
87+
if (swapcontext(&uctx_save, &uctx_switch) == -1) {
88+
perror("swapcontext");
89+
exit(1);
90+
}
91+
92+
free(stack1);
93+
free(stack2);
94+
95+
return NULL;
96+
}
97+
98+
void callStackSwitchCallbackFromThread(void) {
99+
pthread_t thread;
100+
assert(pthread_create(&thread, NULL, stackSwitchThread, NULL) == 0);
101+
assert(pthread_join(thread, NULL) == 0);
102+
}
103+
104+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2023 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+
//go:build unix && !android && !openbsd
6+
7+
package main
8+
9+
/*
10+
void callStackSwitchCallbackFromThread(void);
11+
*/
12+
import "C"
13+
14+
import (
15+
"fmt"
16+
"runtime/debug"
17+
)
18+
19+
func init() {
20+
register("StackSwitchCallback", StackSwitchCallback)
21+
}
22+
23+
//export stackSwitchCallback
24+
func stackSwitchCallback() {
25+
// We want to trigger a bounds check on the g0 stack. To do this, we
26+
// need to call a splittable function through systemstack().
27+
// SetGCPercent contains such a systemstack call.
28+
gogc := debug.SetGCPercent(100)
29+
debug.SetGCPercent(gogc)
30+
}
31+
32+
33+
// Regression test for https://go.dev/issue/62440. It should be possible for C
34+
// threads to call into Go from different stacks without crashing due to g0
35+
// stack bounds checks.
36+
//
37+
// N.B. This is only OK for threads created in C. Threads with Go frames up the
38+
// stack must not change the stack out from under us.
39+
func StackSwitchCallback() {
40+
C.callStackSwitchCallbackFromThread();
41+
42+
fmt.Printf("OK\n")
43+
}

0 commit comments

Comments
 (0)