aboutsummaryrefslogtreecommitdiff
path: root/src/runtime
diff options
context:
space:
mode:
Diffstat (limited to 'src/runtime')
-rw-r--r--src/runtime/cgocall.go4
-rw-r--r--src/runtime/crash_cgo_test.go13
-rw-r--r--src/runtime/runtime2.go2
-rw-r--r--src/runtime/testdata/testprogcgo/callback_pprof.go138
4 files changed, 155 insertions, 2 deletions
diff --git a/src/runtime/cgocall.go b/src/runtime/cgocall.go
index 326674cd2e..b046ab960f 100644
--- a/src/runtime/cgocall.go
+++ b/src/runtime/cgocall.go
@@ -355,7 +355,9 @@ func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) {
gp.m.incgo = true
unlockOSThread()
- if gp.m.isextra {
+ if gp.m.isextra && gp.m.ncgo == 0 {
+ // There are no active cgocalls above this frame (ncgo == 0),
+ // thus there can't be more Go frames above this frame.
gp.m.isExtraInC = true
}
diff --git a/src/runtime/crash_cgo_test.go b/src/runtime/crash_cgo_test.go
index e9b449ab88..a7321f49a5 100644
--- a/src/runtime/crash_cgo_test.go
+++ b/src/runtime/crash_cgo_test.go
@@ -72,6 +72,19 @@ func TestCgoCallbackGC(t *testing.T) {
}
}
+func TestCgoCallbackPprof(t *testing.T) {
+ t.Parallel()
+ switch runtime.GOOS {
+ case "plan9", "windows":
+ t.Skipf("no pthreads on %s", runtime.GOOS)
+ }
+
+ got := runTestProg(t, "testprogcgo", "CgoCallbackPprof")
+ if want := "OK\n"; got != want {
+ t.Fatalf("expected %q, but got:\n%s", want, got)
+ }
+}
+
func TestCgoExternalThreadPanic(t *testing.T) {
t.Parallel()
if runtime.GOOS == "plan9" {
diff --git a/src/runtime/runtime2.go b/src/runtime/runtime2.go
index 0c70d2cc81..6b9f49d503 100644
--- a/src/runtime/runtime2.go
+++ b/src/runtime/runtime2.go
@@ -554,7 +554,7 @@ type m struct {
printlock int8
incgo bool // m is executing a cgo call
isextra bool // m is an extra m
- isExtraInC bool // m is an extra m that is not executing Go code
+ isExtraInC bool // m is an extra m that does not have any Go frames
isExtraInSig bool // m is an extra m in a signal handler
freeWait atomic.Uint32 // Whether it is safe to free g0 and delete m (one of freeMRef, freeMStack, freeMWait)
needextram bool
diff --git a/src/runtime/testdata/testprogcgo/callback_pprof.go b/src/runtime/testdata/testprogcgo/callback_pprof.go
new file mode 100644
index 0000000000..cd235d0341
--- /dev/null
+++ b/src/runtime/testdata/testprogcgo/callback_pprof.go
@@ -0,0 +1,138 @@
+// Copyright 2025 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.
+
+//go:build !plan9 && !windows
+
+package main
+
+// Regression test for https://go.dev/issue/72870. Go code called from C should
+// never be reported as external code.
+
+/*
+#include <pthread.h>
+
+void go_callback1();
+void go_callback2();
+
+static void *callback_pprof_thread(void *arg) {
+ go_callback1();
+ return 0;
+}
+
+static void c_callback(void) {
+ go_callback2();
+}
+
+static void start_callback_pprof_thread() {
+ pthread_t th;
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_create(&th, &attr, callback_pprof_thread, 0);
+ // Don't join, caller will watch pprof.
+}
+*/
+import "C"
+
+import (
+ "bytes"
+ "fmt"
+ "internal/profile"
+ "os"
+ "runtime/pprof"
+ "time"
+)
+
+func init() {
+ register("CgoCallbackPprof", CgoCallbackPprof)
+}
+
+func CgoCallbackPprof() {
+ C.start_callback_pprof_thread()
+
+ var buf bytes.Buffer
+ if err := pprof.StartCPUProfile(&buf); err != nil {
+ fmt.Printf("Error starting CPU profile: %v\n", err)
+ os.Exit(1)
+ }
+ time.Sleep(1 * time.Second)
+ pprof.StopCPUProfile()
+
+ p, err := profile.Parse(&buf)
+ if err != nil {
+ fmt.Printf("Error parsing profile: %v\n", err)
+ os.Exit(1)
+ }
+
+ foundCallee := false
+ for _, s := range p.Sample {
+ funcs := flattenFrames(s)
+ if len(funcs) == 0 {
+ continue
+ }
+
+ leaf := funcs[0]
+ if leaf.Name != "main.go_callback1_callee" {
+ continue
+ }
+ foundCallee = true
+
+ if len(funcs) < 2 {
+ fmt.Printf("Profile: %s\n", p)
+ frames := make([]string, len(funcs))
+ for i := range funcs {
+ frames[i] = funcs[i].Name
+ }
+ fmt.Printf("FAIL: main.go_callback1_callee sample missing caller in frames %v\n", frames)
+ os.Exit(1)
+ }
+
+ if funcs[1].Name != "main.go_callback1" {
+ // In https://go.dev/issue/72870, this will be runtime._ExternalCode.
+ fmt.Printf("Profile: %s\n", p)
+ frames := make([]string, len(funcs))
+ for i := range funcs {
+ frames[i] = funcs[i].Name
+ }
+ fmt.Printf("FAIL: main.go_callback1_callee sample caller got %s want main.go_callback1 in frames %v\n", funcs[1].Name, frames)
+ os.Exit(1)
+ }
+ }
+
+ if !foundCallee {
+ fmt.Printf("Missing main.go_callback1_callee sample in profile %s\n", p)
+ os.Exit(1)
+ }
+
+ fmt.Printf("OK\n")
+}
+
+// Return the frame functions in s, regardless of inlining.
+func flattenFrames(s *profile.Sample) []*profile.Function {
+ ret := make([]*profile.Function, 0, len(s.Location))
+ for _, loc := range s.Location {
+ for _, line := range loc.Line {
+ ret = append(ret, line.Function)
+ }
+ }
+ return ret
+}
+
+//export go_callback1
+func go_callback1() {
+ // This is a separate function just to ensure we have another Go
+ // function as the caller in the profile.
+ go_callback1_callee()
+}
+
+func go_callback1_callee() {
+ C.c_callback()
+
+ // Spin for CPU samples.
+ for {
+ }
+}
+
+//export go_callback2
+func go_callback2() {
+}