Skip to content

Commit a46b1ad

Browse files
committed
runtime: allow update of system stack bounds on callback from C thread
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: I7841b056acea1111bdae3b718345a3bd3961b4a8 Reviewed-on: https://go-review.googlesource.com/c/go/+/525455 Run-TryBot: Michael Pratt <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 5eb382f commit a46b1ad

File tree

5 files changed

+216
-24
lines changed

5 files changed

+216
-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

+13
Original file line numberDiff line numberDiff line change
@@ -869,3 +869,16 @@ 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", "openbsd": // no makecontext
877+
t.Skipf("skipping test on %s", runtime.GOOS)
878+
}
879+
got := runTestProg(t, "testprogcgo", "StackSwitchCallback")
880+
want := "OK\n"
881+
if got != want {
882+
t.Errorf("expected %q, got %v", want, got)
883+
}
884+
}

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,81 @@
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 && !openbsd
6+
7+
#include <assert.h>
8+
#include <pthread.h>
9+
#include <stddef.h>
10+
#include <stdio.h>
11+
#include <stdlib.h>
12+
#include <ucontext.h>
13+
14+
// Use a stack size larger than the 32kb estimate in
15+
// runtime.callbackUpdateSystemStack. This ensures that a second stack
16+
// allocation won't accidentally count as in bounds of the first stack
17+
#define STACK_SIZE (64ull << 10)
18+
19+
static ucontext_t uctx_save, uctx_switch;
20+
21+
extern void stackSwitchCallback(void);
22+
23+
static void *stackSwitchThread(void *arg) {
24+
// Simple test: callback works from the normal system stack.
25+
stackSwitchCallback();
26+
27+
// Next, verify that switching stacks doesn't break callbacks.
28+
29+
char *stack1 = malloc(STACK_SIZE);
30+
if (stack1 == NULL) {
31+
perror("malloc");
32+
exit(1);
33+
}
34+
35+
// Allocate the second stack before freeing the first to ensure we don't get
36+
// the same address from malloc.
37+
char *stack2 = malloc(STACK_SIZE);
38+
if (stack1 == NULL) {
39+
perror("malloc");
40+
exit(1);
41+
}
42+
43+
if (getcontext(&uctx_switch) == -1) {
44+
perror("getcontext");
45+
exit(1);
46+
}
47+
uctx_switch.uc_stack.ss_sp = stack1;
48+
uctx_switch.uc_stack.ss_size = STACK_SIZE;
49+
uctx_switch.uc_link = &uctx_save;
50+
makecontext(&uctx_switch, stackSwitchCallback, 0);
51+
52+
if (swapcontext(&uctx_save, &uctx_switch) == -1) {
53+
perror("swapcontext");
54+
exit(1);
55+
}
56+
57+
if (getcontext(&uctx_switch) == -1) {
58+
perror("getcontext");
59+
exit(1);
60+
}
61+
uctx_switch.uc_stack.ss_sp = stack2;
62+
uctx_switch.uc_stack.ss_size = STACK_SIZE;
63+
uctx_switch.uc_link = &uctx_save;
64+
makecontext(&uctx_switch, stackSwitchCallback, 0);
65+
66+
if (swapcontext(&uctx_save, &uctx_switch) == -1) {
67+
perror("swapcontext");
68+
exit(1);
69+
}
70+
71+
free(stack1);
72+
free(stack2);
73+
74+
return NULL;
75+
}
76+
77+
void callStackSwitchCallbackFromThread(void) {
78+
pthread_t thread;
79+
assert(pthread_create(&thread, NULL, stackSwitchThread, NULL) == 0);
80+
assert(pthread_join(thread, NULL) == 0);
81+
}
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 && !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)