// Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // TODO: This test could be implemented on all (most?) UNIXes if we // added syscall.Tgkill more widely. // We skip all of these tests under race mode because our test thread // spends all of its time in the race runtime, which isn't a safe // point. //go:build (amd64 || arm64 || loong64 || ppc64le) && linux && !race package runtime_test import ( "fmt" "internal/abi" "internal/asan" "internal/msan" "math" "os" "regexp" "runtime" "runtime/debug" "sync/atomic" "syscall" "testing" ) func startDebugCallWorker(t *testing.T) (g *runtime.G, after func()) { // This can deadlock if run under a debugger because it // depends on catching SIGTRAP, which is usually swallowed by // a debugger. skipUnderDebugger(t) // asan/msan instrumentation interferes with tests since we might // inject debugCallV2 while in the asan/msan runtime. This is a // problem for doing things like running the GC or taking stack // traces. Not sure why this is happening yet, but skip for now. if msan.Enabled || asan.Enabled { t.Skip("debugCallV2 is injected erroneously during asan/msan runtime calls; skipping") } // This can deadlock if there aren't enough threads or if a GC // tries to interrupt an atomic loop (see issue #10958). Execute // an extra GC to ensure even the sweep phase is done (out of // caution to prevent #49370 from happening). // TODO(mknyszek): This extra GC cycle is likely unnecessary // because preemption (which may happen during the sweep phase) // isn't much of an issue anymore thanks to asynchronous preemption. // The biggest risk is having a write barrier in the debug call // injection test code fire, because it runs in a signal handler // and may not have a P. // // We use 8 Ps so there's room for the debug call worker, // something that's trying to preempt the call worker, and the // goroutine that's trying to stop the call worker. ogomaxprocs := runtime.GOMAXPROCS(8) ogcpercent := debug.SetGCPercent(-1) runtime.GC() // ready is a buffered channel so debugCallWorker won't block // on sending to it. This makes it less likely we'll catch // debugCallWorker while it's in the runtime. ready := make(chan *runtime.G, 1) var stop uint32 done := make(chan error) go debugCallWorker(ready, &stop, done) g = <-ready return g, func() { atomic.StoreUint32(&stop, 1) err := <-done if err != nil { t.Fatal(err) } runtime.GOMAXPROCS(ogomaxprocs) debug.SetGCPercent(ogcpercent) } } func debugCallWorker(ready chan<- *runtime.G, stop *uint32, done chan<- error) { runtime.LockOSThread() defer runtime.UnlockOSThread() ready <- runtime.Getg() x := 2 debugCallWorker2(stop, &x) if x != 1 { done <- fmt.Errorf("want x = 2, got %d; register pointer not adjusted?", x) } close(done) } // Don't inline this function, since we want to test adjusting // pointers in the arguments. // //go:noinline func debugCallWorker2(stop *uint32, x *int) { for atomic.LoadUint32(stop) == 0 { // Strongly encourage x to live in a register so we // can test pointer register adjustment. *x++ } *x = 1 } func debugCallTKill(tid int) error { return syscall.Tgkill(syscall.Getpid(), tid, syscall.SIGTRAP) } // skipUnderDebugger skips the current test when running under a // debugger (specifically if this process has a tracer). This is // Linux-specific. func skipUnderDebugger(t *testing.T) { pid := syscall.Getpid() status, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) if err != nil { t.Logf("couldn't get proc tracer: %s", err) return } re := regexp.MustCompile(`TracerPid:\s+([0-9]+)`) sub := re.FindSubmatch(status) if sub == nil { t.Logf("couldn't find proc tracer PID") return } if string(sub[1]) == "0" { return } t.Skip("test will deadlock under a debugger") } func TestDebugCall(t *testing.T) { g, after := startDebugCallWorker(t) defer after() type stackArgs struct { x0 int x1 float64 y0Ret int y1Ret float64 } // Inject a call into the debugCallWorker goroutine and test // basic argument and result passing. fn := func(x int, y float64) (y0Ret int, y1Ret float64) { return x + 1, y + 1.0 } var args *stackArgs var regs abi.RegArgs intRegs := regs.Ints[:] floatRegs := regs.Floats[:] fval := float64(42.0) if len(intRegs) > 0 { intRegs[0] = 42 floatRegs[0] = math.Float64bits(fval) } else { args = &stackArgs{ x0: 42, x1: 42.0, } } if _, err := runtime.InjectDebugCall(g, fn, ®s, args, debugCallTKill, false); err != nil { t.Fatal(err) } var result0 int var result1 float64 if len(intRegs) > 0 { result0 = int(intRegs[0]) result1 = math.Float64frombits(floatRegs[0]) } else { result0 = args.y0Ret result1 = args.y1Ret } if result0 != 43 { t.Errorf("want 43, got %d", result0) } if result1 != fval+1 { t.Errorf("want 43, got %f", result1) } } func TestDebugCallLarge(t *testing.T) { g, after := startDebugCallWorker(t) defer after() // Inject a call with a large call frame. const N = 128 var args struct { in [N]int out [N]int } fn := func(in [N]int) (out [N]int) { for i := range in { out[i] = in[i] + 1 } return } var want [N]int for i := range args.in { args.in[i] = i want[i] = i + 1 } if _, err := runtime.InjectDebugCall(g, fn, nil, &args, debugCallTKill, false); err != nil { t.Fatal(err) } if want != args.out { t.Fatalf("want %v, got %v", want, args.out) } } func TestDebugCallGC(t *testing.T) { g, after := startDebugCallWorker(t) defer after() // Inject a call that performs a GC. if _, err := runtime.InjectDebugCall(g, runtime.GC, nil, nil, debugCallTKill, false); err != nil { t.Fatal(err) } } func TestDebugCallGrowStack(t *testing.T) { g, after := startDebugCallWorker(t) defer after() // Inject a call that grows the stack. debugCallWorker checks // for stack pointer breakage. if _, err := runtime.InjectDebugCall(g, func() { growStack(nil) }, nil, nil, debugCallTKill, false); err != nil { t.Fatal(err) } } //go:nosplit func debugCallUnsafePointWorker(gpp **runtime.G, ready, stop *uint32) { // The nosplit causes this function to not contain safe-points // except at calls. runtime.LockOSThread() defer runtime.UnlockOSThread() *gpp = runtime.Getg() for atomic.LoadUint32(stop) == 0 { atomic.StoreUint32(ready, 1) } } func TestDebugCallUnsafePoint(t *testing.T) { skipUnderDebugger(t) // This can deadlock if there aren't enough threads or if a GC // tries to interrupt an atomic loop (see issue #10958). defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(8)) // InjectDebugCall cannot be executed while a GC is actively in // progress. Wait until the current GC is done, and turn it off. // // See #49370. runtime.GC() defer debug.SetGCPercent(debug.SetGCPercent(-1)) // Test that the runtime refuses call injection at unsafe points. var g *runtime.G var ready, stop uint32 defer atomic.StoreUint32(&stop, 1) go debugCallUnsafePointWorker(&g, &ready, &stop) for atomic.LoadUint32(&ready) == 0 { runtime.Gosched() } _, err := runtime.InjectDebugCall(g, func() {}, nil, nil, debugCallTKill, true) if msg := "call not at safe point"; err == nil || err.Error() != msg { t.Fatalf("want %q, got %s", msg, err) } } func TestDebugCallPanic(t *testing.T) { skipUnderDebugger(t) // This can deadlock if there aren't enough threads. defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(8)) // InjectDebugCall cannot be executed while a GC is actively in // progress. Wait until the current GC is done, and turn it off. // // See #10958 and #49370. defer debug.SetGCPercent(debug.SetGCPercent(-1)) // TODO(mknyszek): This extra GC cycle is likely unnecessary // because preemption (which may happen during the sweep phase) // isn't much of an issue anymore thanks to asynchronous preemption. // The biggest risk is having a write barrier in the debug call // injection test code fire, because it runs in a signal handler // and may not have a P. runtime.GC() ready := make(chan *runtime.G) var stop uint32 defer atomic.StoreUint32(&stop, 1) go func() { runtime.LockOSThread() defer runtime.UnlockOSThread() ready <- runtime.Getg() for atomic.LoadUint32(&stop) == 0 { } }() g := <-ready p, err := runtime.InjectDebugCall(g, func() { panic("test") }, nil, nil, debugCallTKill, false) if err != nil { t.Fatal(err) } if ps, ok := p.(string); !ok || ps != "test" { t.Fatalf("wanted panic %v, got %v", "test", p) } }