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 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-06-12 04:01:44 +02:00
parent bf71cacb9a
commit d815ea3462
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270

View File

@ -5,6 +5,7 @@ package sds
/*
#include <libsds.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
// 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)