]> Cypherpunks repositories - gostls13.git/commitdiff
runtime: acquire/release C TSAN lock when calling cgo symbolizer/tracebacker
authorMichael Pratt <mpratt@google.com>
Fri, 30 May 2025 21:05:41 +0000 (17:05 -0400)
committerGopher Robot <gobot@golang.org>
Thu, 25 Sep 2025 17:22:54 +0000 (10:22 -0700)
When calling into C via cmd/cgo, the generated code calls
_cgo_tsan_acquire / _cgo_tsan_release around the C call to report a
dummy lock to the C/C++ TSAN runtime. This is necessary because the
C/C++ TSAN runtime does not understand synchronization within Go and
would otherwise report false positive race reports. See the comment in
cmd/cgo/out.go for more details.

Various C functions in runtime/cgo also contain manual calls to
_cgo_tsan_acquire/release where necessary to suppress race reports.

However, the cgo symbolizer and cgo traceback functions called from
callCgoSymbolizer and cgoContextPCs, respectively, do not have any
instrumentation [1]. They call directly into user C functions with no
TSAN instrumentation.

This means they have an opportunity to report false race conditions. The
most direct way is via their argument. Both are passed a pointer to a
struct stored on the Go stack, and both write to fields of the struct.
If two calls are passed the same pointer from different threads, the C
TSAN runtime will think this is a race.

This is simple to achieve for the cgo symbolizer function, which the
new regression test does. callCgoSymbolizer is called on the standard
goroutine stack, so the argument is a pointer into the goroutine stack.
If the goroutine moves Ms between two calls, it will look like a race.

On the other hand, cgoContextPCs is called on the system stack. Each M
has a unique system stack, so for it to pass the same argument pointer
on different threads would require the first M to exit, free its stack,
and the same region of address space to be used as the stack for a new
M. Theoretically possible, but quite unlikely.

Both of these are addressed by providing a C wrapper in runtime/cgo that
calls _cgo_tsan_acquire/_cgo_tsan_release around calls to the symbolizer
and traceback functions.

There is a lot of room for future cleanup here. Most runtime/cgo
functions have manual instrumentation in their C implementation. That
could be removed in favor of instrumentation in the runtime. We could
even theoretically remove the instrumentation from cmd/cgo and move it
to cgocall. None of these are necessary, but may make things more
consistent and easier to follow.

[1] Note that the cgo traceback function called from the signal handler
via x_cgo_callers _does_ have manual instrumentation.

Fixes #73949.

Cq-Include-Trybots: luci.golang.try:gotip-freebsd-amd64,gotip-linux-amd64-longtest,gotip-windows-amd64-longtest
Change-Id: I6a6a636c9daa38f7fd00694af76b75cb93ba1886
Reviewed-on: https://go-review.googlesource.com/c/go/+/677955
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>

13 files changed:
src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/main.go [new file with mode: 0644]
src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/tracebackctxt_c.c [new file with mode: 0644]
src/cmd/cgo/internal/testsanitizers/tsan_test.go
src/runtime/cgo.go
src/runtime/cgo/callbacks.go
src/runtime/cgo/gcc_context.c
src/runtime/cgo/gcc_libinit.c
src/runtime/cgo/gcc_libinit_windows.c
src/runtime/cgo/libcgo.h
src/runtime/symtab.go
src/runtime/testdata/testprog/setcgotraceback.go [new file with mode: 0644]
src/runtime/traceback.go
src/runtime/traceback_test.go

diff --git a/src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/main.go b/src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/main.go
new file mode 100644 (file)
index 0000000..998a08c
--- /dev/null
@@ -0,0 +1,78 @@
+// 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.
+
+package main
+
+/*
+// Defined in tracebackctxt_c.c.
+extern void C1(void);
+extern void C2(void);
+extern void tcContext(void*);
+extern void tcTraceback(void*);
+extern void tcSymbolizer(void*);
+*/
+import "C"
+
+import (
+       "fmt"
+       "runtime"
+       "sync"
+       "unsafe"
+)
+
+// Regression test for https://go.dev/issue/73949. TSAN should not report races
+// on writes to the argument passed to the symbolizer function.
+//
+// Triggering this race requires calls to the symbolizer function with the same
+// argument pointer on multiple threads. The runtime passes a stack variable to
+// this function, so that means we need to get a single goroutine to execute on
+// two threads, calling the symbolizer function on each.
+//
+// runtime.CallersFrames / Next will call the symbolizer function (if there are
+// C frames). So the approach here is, with GOMAXPROCS=2, have 2 goroutines
+// that use CallersFrames over and over, both frequently calling Gosched in an
+// attempt to get picked up by the other P.
+
+var tracebackOK bool
+
+func main() {
+       runtime.GOMAXPROCS(2)
+       runtime.SetCgoTraceback(0, unsafe.Pointer(C.tcTraceback), unsafe.Pointer(C.tcContext), unsafe.Pointer(C.tcSymbolizer))
+       C.C1()
+       if tracebackOK {
+               fmt.Println("OK")
+       }
+}
+
+//export G1
+func G1() {
+       C.C2()
+}
+
+//export G2
+func G2() {
+       pc := make([]uintptr, 32)
+       n := runtime.Callers(0, pc)
+
+       var wg sync.WaitGroup
+       for range 2 {
+               wg.Go(func() {
+                       for range 1000 {
+                               cf := runtime.CallersFrames(pc[:n])
+                               var frames []runtime.Frame
+                               for {
+                                       frame, more := cf.Next()
+                                       frames = append(frames, frame)
+                                       if !more {
+                                               break
+                                       }
+                               }
+                               runtime.Gosched()
+                       }
+               })
+       }
+       wg.Wait()
+
+       tracebackOK = true
+}
diff --git a/src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/tracebackctxt_c.c b/src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/tracebackctxt_c.c
new file mode 100644 (file)
index 0000000..9ddaa4a
--- /dev/null
@@ -0,0 +1,70 @@
+// 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.
+
+// The C definitions for tracebackctxt.go. That file uses //export so
+// it can't put function definitions in the "C" import comment.
+
+#include <stdint.h>
+#include <stdio.h>
+
+// Functions exported from Go.
+extern void G1(void);
+extern void G2(void);
+
+void C1() {
+       G1();
+}
+
+void C2() {
+       G2();
+}
+
+struct cgoContextArg {
+       uintptr_t context;
+};
+
+struct cgoTracebackArg {
+       uintptr_t  context;
+       uintptr_t  sigContext;
+       uintptr_t* buf;
+       uintptr_t  max;
+};
+
+struct cgoSymbolizerArg {
+       uintptr_t   pc;
+       const char* file;
+       uintptr_t   lineno;
+       const char* func;
+       uintptr_t   entry;
+       uintptr_t   more;
+       uintptr_t   data;
+};
+
+void tcContext(void* parg) {
+       struct cgoContextArg* arg = (struct cgoContextArg*)(parg);
+       if (arg->context == 0) {
+               arg->context = 1;
+       }
+}
+
+void tcTraceback(void* parg) {
+       int base, i;
+       struct cgoTracebackArg* arg = (struct cgoTracebackArg*)(parg);
+       if (arg->max < 1) {
+               return;
+       }
+       arg->buf[0] = 6; // Chosen by fair dice roll.
+}
+
+void tcSymbolizer(void *parg) {
+       struct cgoSymbolizerArg* arg = (struct cgoSymbolizerArg*)(parg);
+       if (arg->pc == 0) {
+               return;
+       }
+       // Report two lines per PC returned by traceback, to test more handling.
+       arg->more = arg->file == NULL;
+       arg->file = "tracebackctxt.go";
+       arg->func = "cFunction";
+       arg->lineno = arg->pc + (arg->more << 16);
+}
index 265c5e3605275571e44f538c5ae55c3d1a98e45b..589db2e6bc4e34abc844ae4ab57a5a41981fbf05 100644 (file)
@@ -56,6 +56,7 @@ func TestTSAN(t *testing.T) {
                {src: "tsan13.go", needsRuntime: true},
                {src: "tsan14.go", needsRuntime: true},
                {src: "tsan15.go", needsRuntime: true},
+               {src: "tsan_tracebackctxt", needsRuntime: true}, // Subdirectory
        }
        for _, tc := range cases {
                tc := tc
@@ -67,7 +68,7 @@ func TestTSAN(t *testing.T) {
                        defer dir.RemoveAll(t)
 
                        outPath := dir.Join(name)
-                       mustRun(t, config.goCmd("build", "-o", outPath, srcPath(tc.src)))
+                       mustRun(t, config.goCmd("build", "-o", outPath, "./"+srcPath(tc.src)))
 
                        cmdArgs := []string{outPath}
                        if goos == "linux" {
index eca905bad95158f7c267f4e4bfa600f31a2dcffe..60f2403d739f348703951c03f043fecf0acfc8ab 100644 (file)
@@ -15,7 +15,9 @@ import "unsafe"
 //go:linkname _cgo_sys_thread_create _cgo_sys_thread_create
 //go:linkname _cgo_notify_runtime_init_done _cgo_notify_runtime_init_done
 //go:linkname _cgo_callers _cgo_callers
-//go:linkname _cgo_set_context_function _cgo_set_context_function
+//go:linkname _cgo_set_traceback_functions _cgo_set_traceback_functions
+//go:linkname _cgo_call_traceback_function _cgo_call_traceback_function
+//go:linkname _cgo_call_symbolizer_function _cgo_call_symbolizer_function
 //go:linkname _cgo_yield _cgo_yield
 //go:linkname _cgo_pthread_key_created _cgo_pthread_key_created
 //go:linkname _cgo_bindm _cgo_bindm
@@ -27,7 +29,9 @@ var (
        _cgo_sys_thread_create        unsafe.Pointer
        _cgo_notify_runtime_init_done unsafe.Pointer
        _cgo_callers                  unsafe.Pointer
-       _cgo_set_context_function     unsafe.Pointer
+       _cgo_set_traceback_functions  unsafe.Pointer
+       _cgo_call_traceback_function  unsafe.Pointer
+       _cgo_call_symbolizer_function unsafe.Pointer
        _cgo_yield                    unsafe.Pointer
        _cgo_pthread_key_created      unsafe.Pointer
        _cgo_bindm                    unsafe.Pointer
index 3c246a88b6ca9163ca07e46696502f85de3b0f2b..986f61914f2670555610ee8166fc535220cf02a6 100644 (file)
@@ -121,13 +121,30 @@ var _cgo_bindm = &x_cgo_bindm
 var x_cgo_notify_runtime_init_done byte
 var _cgo_notify_runtime_init_done = &x_cgo_notify_runtime_init_done
 
-// Sets the traceback context function. See runtime.SetCgoTraceback.
-
-//go:cgo_import_static x_cgo_set_context_function
-//go:linkname x_cgo_set_context_function x_cgo_set_context_function
-//go:linkname _cgo_set_context_function _cgo_set_context_function
-var x_cgo_set_context_function byte
-var _cgo_set_context_function = &x_cgo_set_context_function
+// Sets the traceback, context, and symbolizer functions. See
+// runtime.SetCgoTraceback.
+
+//go:cgo_import_static x_cgo_set_traceback_functions
+//go:linkname x_cgo_set_traceback_functions x_cgo_set_traceback_functions
+//go:linkname _cgo_set_traceback_functions _cgo_set_traceback_functions
+var x_cgo_set_traceback_functions byte
+var _cgo_set_traceback_functions = &x_cgo_set_traceback_functions
+
+// Call the traceback function registered with x_cgo_set_traceback_functions.
+
+//go:cgo_import_static x_cgo_call_traceback_function
+//go:linkname x_cgo_call_traceback_function x_cgo_call_traceback_function
+//go:linkname _cgo_call_traceback_function _cgo_call_traceback_function
+var x_cgo_call_traceback_function byte
+var _cgo_call_traceback_function = &x_cgo_call_traceback_function
+
+// Call the symbolizer function registered with x_cgo_set_symbolizer_functions.
+
+//go:cgo_import_static x_cgo_call_symbolizer_function
+//go:linkname x_cgo_call_symbolizer_function x_cgo_call_symbolizer_function
+//go:linkname _cgo_call_symbolizer_function _cgo_call_symbolizer_function
+var x_cgo_call_symbolizer_function byte
+var _cgo_call_symbolizer_function = &x_cgo_call_symbolizer_function
 
 // Calls a libc function to execute background work injected via libc
 // interceptors, such as processing pending signals under the thread
index ad586928219609e55aca205e03494e48511bc920..b647c99a982539199f5c1e0f16bb143cfef67165 100644 (file)
@@ -8,11 +8,11 @@
 
 // Releases the cgo traceback context.
 void _cgo_release_context(uintptr_t ctxt) {
-       void (*pfn)(struct context_arg*);
+       void (*pfn)(struct cgoContextArg*);
 
        pfn = _cgo_get_context_function();
        if (ctxt != 0 && pfn != nil) {
-               struct context_arg arg;
+               struct cgoContextArg arg;
 
                arg.Context = ctxt;
                (*pfn)(&arg);
index e9b0a3f769b756de1bee51b33ae5f72295319928..05998fadf814562443366cfe1414e835c4c68b6e 100644 (file)
@@ -32,8 +32,14 @@ static void pthread_key_destructor(void* g);
 uintptr_t x_cgo_pthread_key_created;
 void (*x_crosscall2_ptr)(void (*fn)(void *), void *, int, size_t);
 
+// The traceback function, used when tracing C calls.
+static void (*cgo_traceback_function)(struct cgoTracebackArg*);
+
 // The context function, used when tracing back C calls into Go.
-static void (*cgo_context_function)(struct context_arg*);
+static void (*cgo_context_function)(struct cgoContextArg*);
+
+// The symbolizer function, used when symbolizing C frames.
+static void (*cgo_symbolizer_function)(struct cgoSymbolizerArg*);
 
 void
 x_cgo_sys_thread_create(void* (*func)(void*), void* arg) {
@@ -52,7 +58,7 @@ x_cgo_sys_thread_create(void* (*func)(void*), void* arg) {
 
 uintptr_t
 _cgo_wait_runtime_init_done(void) {
-       void (*pfn)(struct context_arg*);
+       void (*pfn)(struct cgoContextArg*);
        int done;
 
        pfn = __atomic_load_n(&cgo_context_function, __ATOMIC_CONSUME);
@@ -70,7 +76,6 @@ _cgo_wait_runtime_init_done(void) {
                        x_cgo_pthread_key_created = 1;
                }
 
-
                // TODO(iant): For the case of a new C thread calling into Go, such
                // as when using -buildmode=c-archive, we know that Go runtime
                // initialization is complete but we do not know that all Go init
@@ -87,7 +92,7 @@ _cgo_wait_runtime_init_done(void) {
        }
 
        if (pfn != nil) {
-               struct context_arg arg;
+               struct cgoContextArg arg;
 
                arg.Context = 0;
                (*pfn)(&arg);
@@ -138,17 +143,71 @@ x_cgo_notify_runtime_init_done(void* dummy __attribute__ ((unused))) {
        pthread_mutex_unlock(&runtime_init_mu);
 }
 
-// Sets the context function to call to record the traceback context
-// when calling a Go function from C code. Called from runtime.SetCgoTraceback.
-void x_cgo_set_context_function(void (*context)(struct context_arg*)) {
-       __atomic_store_n(&cgo_context_function, context, __ATOMIC_RELEASE);
+// Sets the traceback, context, and symbolizer functions. Called from
+// runtime.SetCgoTraceback.
+void x_cgo_set_traceback_functions(struct cgoSetTracebackFunctionsArg* arg) {
+       __atomic_store_n(&cgo_traceback_function, arg->Traceback, __ATOMIC_RELEASE);
+       __atomic_store_n(&cgo_context_function, arg->Context, __ATOMIC_RELEASE);
+       __atomic_store_n(&cgo_symbolizer_function, arg->Symbolizer, __ATOMIC_RELEASE);
+}
+
+// Gets the traceback function to call to trace C calls.
+void (*(_cgo_get_traceback_function(void)))(struct cgoTracebackArg*) {
+       return __atomic_load_n(&cgo_traceback_function, __ATOMIC_CONSUME);
+}
+
+// Call the traceback function registered with x_cgo_set_traceback_functions.
+//
+// The traceback function is an arbitrary user C function which may be built
+// with TSAN, and thus must be wrapped with TSAN acquire/release calls. For
+// normal cgo calls, cmd/cgo automatically inserts TSAN acquire/release calls.
+// Since the traceback, context, and symbolizer functions are registered at
+// startup and called via the runtime, they do not get automatic TSAN
+// acquire/release calls.
+//
+// The only purpose of this wrapper is to perform TSAN acquire/release.
+// Alternatively, if the runtime arranged to safely call TSAN acquire/release,
+// it could perform the call directly.
+void x_cgo_call_traceback_function(struct cgoTracebackArg* arg) {
+       void (*pfn)(struct cgoTracebackArg*);
+
+       pfn = _cgo_get_traceback_function();
+       if (pfn == nil) {
+               return;
+       }
+
+       _cgo_tsan_acquire();
+       (*pfn)(arg);
+       _cgo_tsan_release();
 }
 
-// Gets the context function.
-void (*(_cgo_get_context_function(void)))(struct context_arg*) {
+// Gets the context function to call to record the traceback context
+// when calling a Go function from C code.
+void (*(_cgo_get_context_function(void)))(struct cgoContextArg*) {
        return __atomic_load_n(&cgo_context_function, __ATOMIC_CONSUME);
 }
 
+// Gets the symbolizer function to call to symbolize C frames.
+void (*(_cgo_get_symbolizer_function(void)))(struct cgoSymbolizerArg*) {
+       return __atomic_load_n(&cgo_symbolizer_function, __ATOMIC_CONSUME);
+}
+
+// Call the symbolizer function registered with x_cgo_set_traceback_functions.
+//
+// See comment on x_cgo_call_traceback_function.
+void x_cgo_call_symbolizer_function(struct cgoSymbolizerArg* arg) {
+       void (*pfn)(struct cgoSymbolizerArg*);
+
+       pfn = _cgo_get_symbolizer_function();
+       if (pfn == nil) {
+               return;
+       }
+
+       _cgo_tsan_acquire();
+       (*pfn)(arg);
+       _cgo_tsan_release();
+}
+
 // _cgo_try_pthread_create retries pthread_create if it fails with
 // EAGAIN.
 int
index 9275185d6efc9a840db0b6bb211c9553cccf26f1..926f9168434638b38af612841db6ad34373f2dab 100644 (file)
@@ -32,6 +32,7 @@ static CRITICAL_SECTION runtime_init_cs;
 static HANDLE runtime_init_wait;
 static int runtime_init_done;
 
+// No pthreads on Windows, these are always zero.
 uintptr_t x_cgo_pthread_key_created;
 void (*x_crosscall2_ptr)(void (*fn)(void *), void *, int, size_t);
 
@@ -81,7 +82,7 @@ _cgo_is_runtime_initialized() {
 
 uintptr_t
 _cgo_wait_runtime_init_done(void) {
-       void (*pfn)(struct context_arg*);
+       void (*pfn)(struct cgoContextArg*);
 
         _cgo_maybe_run_preinit();
        while (!_cgo_is_runtime_initialized()) {
@@ -89,7 +90,7 @@ _cgo_wait_runtime_init_done(void) {
        }
        pfn = _cgo_get_context_function();
        if (pfn != nil) {
-               struct context_arg arg;
+               struct cgoContextArg arg;
 
                arg.Context = 0;
                (*pfn)(&arg);
@@ -118,20 +119,54 @@ x_cgo_notify_runtime_init_done(void* dummy) {
        }
 }
 
+// The traceback function, used when tracing C calls.
+static void (*cgo_traceback_function)(struct cgoTracebackArg*);
+
 // The context function, used when tracing back C calls into Go.
-static void (*cgo_context_function)(struct context_arg*);
+static void (*cgo_context_function)(struct cgoContextArg*);
+
+// The symbolizer function, used when symbolizing C frames.
+static void (*cgo_symbolizer_function)(struct cgoSymbolizerArg*);
+
+// Sets the traceback, context, and symbolizer functions. Called from
+// runtime.SetCgoTraceback.
+void x_cgo_set_traceback_functions(struct cgoSetTracebackFunctionsArg* arg) {
+       EnterCriticalSection(&runtime_init_cs);
+       cgo_traceback_function = arg->Traceback;
+       cgo_context_function = arg->Context;
+       cgo_symbolizer_function = arg->Symbolizer;
+       LeaveCriticalSection(&runtime_init_cs);
+}
+
+// Gets the traceback function to call to trace C calls.
+void (*(_cgo_get_traceback_function(void)))(struct cgoTracebackArg*) {
+       void (*ret)(struct cgoTracebackArg*);
 
-// Sets the context function to call to record the traceback context
-// when calling a Go function from C code. Called from runtime.SetCgoTraceback.
-void x_cgo_set_context_function(void (*context)(struct context_arg*)) {
        EnterCriticalSection(&runtime_init_cs);
-       cgo_context_function = context;
+       ret = cgo_traceback_function;
        LeaveCriticalSection(&runtime_init_cs);
+       return ret;
+}
+
+// Call the traceback function registered with x_cgo_set_traceback_functions.
+//
+// On other platforms, this coordinates with C/C++ TSAN. On Windows, there is
+// no C/C++ TSAN.
+void x_cgo_call_traceback_function(struct cgoTracebackArg* arg) {
+       void (*pfn)(struct cgoTracebackArg*);
+
+       pfn = _cgo_get_traceback_function();
+       if (pfn == nil) {
+               return;
+       }
+
+       (*pfn)(arg);
 }
 
-// Gets the context function.
-void (*(_cgo_get_context_function(void)))(struct context_arg*) {
-       void (*ret)(struct context_arg*);
+// Gets the context function to call to record the traceback context
+// when calling a Go function from C code.
+void (*(_cgo_get_context_function(void)))(struct cgoContextArg*) {
+       void (*ret)(struct cgoContextArg*);
 
        EnterCriticalSection(&runtime_init_cs);
        ret = cgo_context_function;
@@ -139,6 +174,31 @@ void (*(_cgo_get_context_function(void)))(struct context_arg*) {
        return ret;
 }
 
+// Gets the symbolizer function to call to symbolize C frames.
+void (*(_cgo_get_symbolizer_function(void)))(struct cgoSymbolizerArg*) {
+       void (*ret)(struct cgoSymbolizerArg*);
+
+       EnterCriticalSection(&runtime_init_cs);
+       ret = cgo_symbolizer_function;
+       LeaveCriticalSection(&runtime_init_cs);
+       return ret;
+}
+
+// Call the symbolizer function registered with x_cgo_set_symbolizer_functions.
+//
+// On other platforms, this coordinates with C/C++ TSAN. On Windows, there is
+// no C/C++ TSAN.
+void x_cgo_call_symbolizer_function(struct cgoSymbolizerArg* arg) {
+       void (*pfn)(struct cgoSymbolizerArg*);
+
+       pfn = _cgo_get_symbolizer_function();
+       if (pfn == nil) {
+               return;
+       }
+
+       (*pfn)(arg);
+}
+
 void _cgo_beginthread(unsigned long (__stdcall *func)(void*), void* arg) {
        int tries;
        HANDLE thandle;
index 26da68fadb67d0dca4ff4858fb2a59ea58f7acc8..aa0b57d6d7a04c5fd98f1006b728cfb4009d1fd2 100644 (file)
@@ -89,15 +89,7 @@ void darwin_arm_init_thread_exception_port(void);
 void darwin_arm_init_mach_exception_handler(void);
 
 /*
- * The cgo context function. See runtime.SetCgoTraceback.
- */
-struct context_arg {
-       uintptr_t Context;
-};
-extern void (*(_cgo_get_context_function(void)))(struct context_arg*);
-
-/*
- * The argument for the cgo traceback callback. See runtime.SetCgoTraceback.
+ * The cgo traceback callback. See runtime.SetCgoTraceback.
  */
 struct cgoTracebackArg {
        uintptr_t  Context;
@@ -105,6 +97,38 @@ struct cgoTracebackArg {
        uintptr_t* Buf;
        uintptr_t  Max;
 };
+extern void (*(_cgo_get_traceback_function(void)))(struct cgoTracebackArg*);
+
+/*
+ * The cgo context callback. See runtime.SetCgoTraceback.
+ */
+struct cgoContextArg {
+       uintptr_t Context;
+};
+extern void (*(_cgo_get_context_function(void)))(struct cgoContextArg*);
+
+/*
+ * The argument for the cgo symbolizer callback. See runtime.SetCgoTraceback.
+ */
+struct cgoSymbolizerArg {
+       uintptr_t   PC;
+       const char* File;
+       uintptr_t   Lineno;
+       const char* Func;
+       uintptr_t   Entry;
+       uintptr_t   More;
+       uintptr_t   Data;
+};
+extern void (*(_cgo_get_symbolizer_function(void)))(struct cgoSymbolizerArg*);
+
+/*
+ * The argument for x_cgo_set_traceback_functions. See runtime.SetCgoTraceback.
+ */
+struct cgoSetTracebackFunctionsArg {
+       void (*Traceback)(struct cgoTracebackArg*);
+       void (*Context)(struct cgoContextArg*);
+       void (*Symbolizer)(struct cgoSymbolizerArg*);
+};
 
 /*
  * TSAN support.  This is only useful when building with
@@ -121,11 +145,21 @@ struct cgoTracebackArg {
 
 #ifdef CGO_TSAN
 
+// _cgo_tsan_acquire tells C/C++ TSAN that we are acquiring a dummy lock. We
+// call this when calling from Go to C. This is necessary because TSAN cannot
+// see the synchronization in Go. Note that C/C++ code built with TSAN is not
+// the same as the Go race detector.
+//
+// cmd/cgo generates calls to _cgo_tsan_acquire and _cgo_tsan_release. For
+// other cgo calls, manual calls are required.
+//
 // These must match the definitions in yesTsanProlog in cmd/cgo/out.go.
 // In general we should call _cgo_tsan_acquire when we enter C code,
 // and call _cgo_tsan_release when we return to Go code.
+//
 // This is only necessary when calling code that might be instrumented
 // by TSAN, which mostly means system library calls that TSAN intercepts.
+//
 // See the comment in cmd/cgo/out.go for more details.
 
 long long _cgo_sync __attribute__ ((common));
index 56f2a00d76ef862e0237e2b4144c4d5716cb395d..62ad8d13611150edfda08fb1b962233c793a6877 100644 (file)
@@ -108,7 +108,7 @@ func (ci *Frames) Next() (frame Frame, more bool) {
                }
                funcInfo := findfunc(pc)
                if !funcInfo.valid() {
-                       if cgoSymbolizer != nil {
+                       if cgoSymbolizerAvailable() {
                                // Pre-expand cgo frames. We could do this
                                // incrementally, too, but there's no way to
                                // avoid allocation in this case anyway.
@@ -295,6 +295,8 @@ func runtime_expandFinalInlineFrame(stk []uintptr) []uintptr {
 // expandCgoFrames expands frame information for pc, known to be
 // a non-Go function, using the cgoSymbolizer hook. expandCgoFrames
 // returns nil if pc could not be expanded.
+//
+// Preconditions: cgoSymbolizerAvailable returns true.
 func expandCgoFrames(pc uintptr) []Frame {
        arg := cgoSymbolizerArg{pc: pc}
        callCgoSymbolizer(&arg)
diff --git a/src/runtime/testdata/testprog/setcgotraceback.go b/src/runtime/testdata/testprog/setcgotraceback.go
new file mode 100644 (file)
index 0000000..de00502
--- /dev/null
@@ -0,0 +1,45 @@
+// 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.
+
+package main
+
+import (
+       "fmt"
+       "internal/abi"
+       "runtime"
+       "unsafe"
+)
+
+func init() {
+       register("SetCgoTracebackNoCgo", SetCgoTracebackNoCgo)
+}
+
+func cgoTraceback() {
+       panic("unexpectedly reached cgo traceback function")
+}
+
+func cgoContext() {
+       panic("unexpectedly reached cgo context function")
+}
+
+func cgoSymbolizer() {
+       panic("unexpectedly reached cgo symbolizer function")
+}
+
+// SetCgoTraceback is a no-op in non-cgo binaries.
+func SetCgoTracebackNoCgo() {
+       traceback := unsafe.Pointer(abi.FuncPCABIInternal(cgoTraceback))
+       context := unsafe.Pointer(abi.FuncPCABIInternal(cgoContext))
+       symbolizer := unsafe.Pointer(abi.FuncPCABIInternal(cgoSymbolizer))
+       runtime.SetCgoTraceback(0, traceback, context, symbolizer)
+
+       // In a cgo binary, runtime.(*Frames).Next calls the cgo symbolizer for
+       // any non-Go frames. Pass in a bogus frame to verify that Next does
+       // not attempt to call the cgo symbolizer, which would crash in a
+       // non-cgo binary like this one.
+       frames := runtime.CallersFrames([]uintptr{0x12345678})
+       frames.Next()
+
+       fmt.Println("OK")
+}
index 00eac5920139d55ec5966b163be6de2d08adabe0..949d48c79a6df5ef61d099034141ce706c8f28db 100644 (file)
@@ -591,7 +591,7 @@ func (u *unwinder) symPC() uintptr {
 // If the current frame is not a cgo frame or if there's no registered cgo
 // unwinder, it returns 0.
 func (u *unwinder) cgoCallers(pcBuf []uintptr) int {
-       if cgoTraceback == nil || u.frame.fn.funcID != abi.FuncID_cgocallback || u.cgoCtxt < 0 {
+       if !cgoTracebackAvailable() || u.frame.fn.funcID != abi.FuncID_cgocallback || u.cgoCtxt < 0 {
                // We don't have a cgo unwinder (typical case), or we do but we're not
                // in a cgo frame or we're out of cgo context.
                return 0
@@ -1014,7 +1014,7 @@ func traceback2(u *unwinder, showRuntime bool, skip, max int) (n, lastN int) {
                        anySymbolized := false
                        stop := false
                        for _, pc := range cgoBuf[:cgoN] {
-                               if cgoSymbolizer == nil {
+                               if !cgoSymbolizerAvailable() {
                                        if pr, stop := commitFrame(); stop {
                                                break
                                        } else if pr {
@@ -1573,10 +1573,18 @@ func SetCgoTraceback(version int, traceback, context, symbolizer unsafe.Pointer)
        cgoContext = context
        cgoSymbolizer = symbolizer
 
-       // The context function is called when a C function calls a Go
-       // function. As such it is only called by C code in runtime/cgo.
-       if _cgo_set_context_function != nil {
-               cgocall(_cgo_set_context_function, context)
+       if _cgo_set_traceback_functions != nil {
+               type cgoSetTracebackFunctionsArg struct {
+                       traceback  unsafe.Pointer
+                       context    unsafe.Pointer
+                       symbolizer unsafe.Pointer
+               }
+               arg := cgoSetTracebackFunctionsArg{
+                       traceback:  traceback,
+                       context:    context,
+                       symbolizer: symbolizer,
+               }
+               cgocall(_cgo_set_traceback_functions, noescape(unsafe.Pointer(&arg)))
        }
 }
 
@@ -1584,6 +1592,18 @@ var cgoTraceback unsafe.Pointer
 var cgoContext unsafe.Pointer
 var cgoSymbolizer unsafe.Pointer
 
+func cgoTracebackAvailable() bool {
+       // - The traceback function must be registered via SetCgoTraceback.
+       // - This must be a cgo binary (providing _cgo_call_traceback_function).
+       return cgoTraceback != nil && _cgo_call_traceback_function != nil
+}
+
+func cgoSymbolizerAvailable() bool {
+       // - The symbolizer function must be registered via SetCgoTraceback.
+       // - This must be a cgo binary (providing _cgo_call_symbolizer_function).
+       return cgoSymbolizer != nil && _cgo_call_symbolizer_function != nil
+}
+
 // cgoTracebackArg is the type passed to cgoTraceback.
 type cgoTracebackArg struct {
        context    uintptr
@@ -1610,7 +1630,7 @@ type cgoSymbolizerArg struct {
 
 // printCgoTraceback prints a traceback of callers.
 func printCgoTraceback(callers *cgoCallers) {
-       if cgoSymbolizer == nil {
+       if !cgoSymbolizerAvailable() {
                for _, c := range callers {
                        if c == 0 {
                                break
@@ -1635,6 +1655,8 @@ func printCgoTraceback(callers *cgoCallers) {
 // printOneCgoTraceback prints the traceback of a single cgo caller.
 // This can print more than one line because of inlining.
 // It returns the "stop" result of commitFrame.
+//
+// Preconditions: cgoSymbolizerAvailable returns true.
 func printOneCgoTraceback(pc uintptr, commitFrame func() (pr, stop bool), arg *cgoSymbolizerArg) bool {
        arg.pc = pc
        for {
@@ -1665,6 +1687,8 @@ func printOneCgoTraceback(pc uintptr, commitFrame func() (pr, stop bool), arg *c
 }
 
 // callCgoSymbolizer calls the cgoSymbolizer function.
+//
+// Preconditions: cgoSymbolizerAvailable returns true.
 func callCgoSymbolizer(arg *cgoSymbolizerArg) {
        call := cgocall
        if panicking.Load() > 0 || getg().m.curg != getg() {
@@ -1678,14 +1702,13 @@ func callCgoSymbolizer(arg *cgoSymbolizerArg) {
        if asanenabled {
                asanwrite(unsafe.Pointer(arg), unsafe.Sizeof(cgoSymbolizerArg{}))
        }
-       call(cgoSymbolizer, noescape(unsafe.Pointer(arg)))
+       call(_cgo_call_symbolizer_function, noescape(unsafe.Pointer(arg)))
 }
 
 // cgoContextPCs gets the PC values from a cgo traceback.
+//
+// Preconditions: cgoTracebackAvailable returns true.
 func cgoContextPCs(ctxt uintptr, buf []uintptr) {
-       if cgoTraceback == nil {
-               return
-       }
        call := cgocall
        if panicking.Load() > 0 || getg().m.curg != getg() {
                // We do not want to call into the scheduler when panicking
@@ -1703,5 +1726,5 @@ func cgoContextPCs(ctxt uintptr, buf []uintptr) {
        if asanenabled {
                asanwrite(unsafe.Pointer(&arg), unsafe.Sizeof(arg))
        }
-       call(cgoTraceback, noescape(unsafe.Pointer(&arg)))
+       call(_cgo_call_traceback_function, noescape(unsafe.Pointer(&arg)))
 }
index 8cbccac673ac6ce56d638e5279c2a5a39a11ba2e..1dac91311ca9a34ac0c0b52df40ccadbc96b4efb 100644 (file)
@@ -8,6 +8,9 @@ import (
        "bytes"
        "fmt"
        "internal/abi"
+       "internal/asan"
+       "internal/msan"
+       "internal/race"
        "internal/testenv"
        "regexp"
        "runtime"
@@ -867,3 +870,15 @@ func TestTracebackGeneric(t *testing.T) {
                }
        }
 }
+
+func TestSetCgoTracebackNoCgo(t *testing.T) {
+       if asan.Enabled || msan.Enabled || race.Enabled {
+               t.Skip("skipped test: sanitizer builds use cgo")
+       }
+
+       output := runTestProg(t, "testprog", "SetCgoTracebackNoCgo")
+       want := "OK\n"
+       if output != want {
+               t.Fatalf("want %s, got %s\n", want, output)
+       }
+}