From d7abfe4f0dc91568648a66495b9f5d7ebc0f22b5 Mon Sep 17 00:00:00 2001 From: Michael Pratt Date: Fri, 30 May 2025 17:05:41 -0400 Subject: [PATCH] runtime: acquire/release C TSAN lock when calling cgo symbolizer/tracebacker 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 Auto-Submit: Michael Pratt Reviewed-by: Ian Lance Taylor LUCI-TryBot-Result: Go LUCI --- .../testdata/tsan_tracebackctxt/main.go | 78 ++++++++++++++++++ .../tsan_tracebackctxt/tracebackctxt_c.c | 70 ++++++++++++++++ .../cgo/internal/testsanitizers/tsan_test.go | 3 +- src/runtime/cgo.go | 8 +- src/runtime/cgo/callbacks.go | 31 +++++-- src/runtime/cgo/gcc_context.c | 4 +- src/runtime/cgo/gcc_libinit.c | 79 +++++++++++++++--- src/runtime/cgo/gcc_libinit_windows.c | 80 ++++++++++++++++--- src/runtime/cgo/libcgo.h | 52 +++++++++--- src/runtime/symtab.go | 4 +- .../testdata/testprog/setcgotraceback.go | 45 +++++++++++ src/runtime/traceback.go | 47 ++++++++--- src/runtime/traceback_test.go | 15 ++++ 13 files changed, 462 insertions(+), 54 deletions(-) create mode 100644 src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/main.go create mode 100644 src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/tracebackctxt_c.c create mode 100644 src/runtime/testdata/testprog/setcgotraceback.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 index 0000000000..998a08ca53 --- /dev/null +++ b/src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/main.go @@ -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 index 0000000000..9ddaa4aaf2 --- /dev/null +++ b/src/cmd/cgo/internal/testsanitizers/testdata/tsan_tracebackctxt/tracebackctxt_c.c @@ -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 +#include + +// 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); +} diff --git a/src/cmd/cgo/internal/testsanitizers/tsan_test.go b/src/cmd/cgo/internal/testsanitizers/tsan_test.go index 265c5e3605..589db2e6bc 100644 --- a/src/cmd/cgo/internal/testsanitizers/tsan_test.go +++ b/src/cmd/cgo/internal/testsanitizers/tsan_test.go @@ -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" { diff --git a/src/runtime/cgo.go b/src/runtime/cgo.go index eca905bad9..60f2403d73 100644 --- a/src/runtime/cgo.go +++ b/src/runtime/cgo.go @@ -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 diff --git a/src/runtime/cgo/callbacks.go b/src/runtime/cgo/callbacks.go index 3c246a88b6..986f61914f 100644 --- a/src/runtime/cgo/callbacks.go +++ b/src/runtime/cgo/callbacks.go @@ -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 diff --git a/src/runtime/cgo/gcc_context.c b/src/runtime/cgo/gcc_context.c index ad58692821..b647c99a98 100644 --- a/src/runtime/cgo/gcc_context.c +++ b/src/runtime/cgo/gcc_context.c @@ -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); diff --git a/src/runtime/cgo/gcc_libinit.c b/src/runtime/cgo/gcc_libinit.c index e9b0a3f769..05998fadf8 100644 --- a/src/runtime/cgo/gcc_libinit.c +++ b/src/runtime/cgo/gcc_libinit.c @@ -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 diff --git a/src/runtime/cgo/gcc_libinit_windows.c b/src/runtime/cgo/gcc_libinit_windows.c index 9275185d6e..926f916843 100644 --- a/src/runtime/cgo/gcc_libinit_windows.c +++ b/src/runtime/cgo/gcc_libinit_windows.c @@ -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; diff --git a/src/runtime/cgo/libcgo.h b/src/runtime/cgo/libcgo.h index 26da68fadb..aa0b57d6d7 100644 --- a/src/runtime/cgo/libcgo.h +++ b/src/runtime/cgo/libcgo.h @@ -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)); diff --git a/src/runtime/symtab.go b/src/runtime/symtab.go index 56f2a00d76..62ad8d1361 100644 --- a/src/runtime/symtab.go +++ b/src/runtime/symtab.go @@ -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 index 0000000000..de005027ec --- /dev/null +++ b/src/runtime/testdata/testprog/setcgotraceback.go @@ -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") +} diff --git a/src/runtime/traceback.go b/src/runtime/traceback.go index 00eac59201..949d48c79a 100644 --- a/src/runtime/traceback.go +++ b/src/runtime/traceback.go @@ -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))) } diff --git a/src/runtime/traceback_test.go b/src/runtime/traceback_test.go index 8cbccac673..1dac91311c 100644 --- a/src/runtime/traceback_test.go +++ b/src/runtime/traceback_test.go @@ -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) + } +} -- 2.52.0