From d815ea346218eb2cd4094948b90c190209b7a3cb Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Fri, 12 Jun 2026 04:01:44 +0200 Subject: [PATCH] fix: pass WaitGroup as cgo.Handle, not a raw stack pointer sdsCall stored &wg (a pointer to a stack sync.WaitGroup) in C memory (SdsResp.ffiWg) and the result callback dereferenced it from the nim-ffi thread while the caller was parked in wg.Wait(). That violates the cgo pointer rules: the GC can relocate the parked goroutine's stack, leaving the C-held pointer dangling, which corrupts memory under GC pressure. Pass the WaitGroup as a runtime/cgo.Handle (a stable uintptr token) instead, and resolve it back to *sync.WaitGroup in the callback. Co-Authored-By: Claude Opus 4.8 --- sds/sds.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/sds/sds.go b/sds/sds.go index d646b9c..d3ec50d 100644 --- a/sds/sds.go +++ b/sds/sds.go @@ -5,6 +5,7 @@ package sds /* #include #include + #include #include // Event callback shared by all ReliabilityManager instances; userData carries @@ -17,12 +18,12 @@ package sds typedef struct { int ret; - char* msg; // owned copy of the CBOR payload (freed by freeResp) + char* msg; // owned copy of the CBOR payload (freed by freeResp) size_t len; - void* ffiWg; // *sync.WaitGroup the caller blocks on + uintptr_t ffiWg; // cgo.Handle for the *sync.WaitGroup the caller blocks on } SdsResp; - static void* allocResp(void* wg) { + static void* allocResp(uintptr_t wg) { SdsResp* r = calloc(1, sizeof(SdsResp)); r->ffiWg = wg; return r; @@ -113,6 +114,7 @@ import ( "crypto/rand" "encoding/hex" "errors" + "runtime/cgo" "sync" "unsafe" @@ -151,7 +153,11 @@ func SdsGoCallback(ret C.int, msg *C.char, len C.size_t, resp unsafe.Pointer) { // Copy the CBOR payload into resp-owned memory; the libsds buffer is only // valid during this callback. C.setRespMsg(resp, msg, len) - wg := (*sync.WaitGroup)(m.ffiWg) + // ffiWg is a cgo.Handle, not a raw Go pointer: the callback fires on the + // nim-ffi thread while the caller is parked in wg.Wait(), and a raw + // pointer to the caller's (stack) WaitGroup can dangle when GC relocates + // that stack. The handle resolves to a stable *sync.WaitGroup. + wg := cgo.Handle(m.ffiWg).Value().(*sync.WaitGroup) wg.Done() } } @@ -159,8 +165,14 @@ func SdsGoCallback(ret C.int, msg *C.char, len C.size_t, resp unsafe.Pointer) { // call runs a libsds request that delivers its CBOR result through SdsGoCallback, // blocks until the callback fires, and returns the (copied) result bytes. func sdsCall(invoke func(resp unsafe.Pointer)) (int, []byte) { - wg := sync.WaitGroup{} - resp := C.allocResp(unsafe.Pointer(&wg)) + wg := &sync.WaitGroup{} + // Pass the WaitGroup as a cgo.Handle (a stable uintptr token) rather than a + // raw &wg: storing a Go pointer in C memory and dereferencing it from the + // callback thread violates the cgo pointer rules and crashes under GC. + h := cgo.NewHandle(wg) + defer h.Delete() + + resp := C.allocResp(C.uintptr_t(h)) defer C.freeResp(resp) wg.Add(1)