Skip to content

aioze client: ctypes callbacks not kept referenced — SIGSEGV during cgo when Go logs after connect returns #202

Description

@devjotaduo

Summary

neonize/aioze/client.py passes the ctypes callback wrappers to Neonize() inline, without keeping any Python reference to them:

err = await self.__client.Neonize(
    ...,
    func_string(self.__onQr),
    func_string(self.__onLoginStatus),
    func_callback_bytes(self.event.execute),
    func_callback_bytes2(log_whatsmeow),
    (ctypes.c_char * len(self.event.list_func)).from_buffer(d),
    ...
)

The thunks stay alive only because they are arguments of the in-flight FFI call. The moment Neonize() returns (e.g. after Stop()), they become garbage — but whatsmeow goroutines on the Go side (e.g. FrameSocket.readPump emitting the final "Error sending close to websocket" log line) still invoke the log/event callbacks for a short while after disconnect. Once the GC collects a thunk, the next call from Go lands on a dead function pointer and the whole host process dies with SIGSEGV during cgo execution.

Note the sync client already has a partial mitigation for exactly this (client.py: "Keep the ctypes callback wrappers and the subscriber buffer referenced for the whole lifetime of the connection", stored in self._connect_refs) — but the aioze client has none. And self._connect_refs still dies with the client object, so a host that drops the client object right after stopping it keeps a smaller version of the same window.

Crash log (production)

SIGSEGV: segmentation violation
PC=0x7f047522c0d2 m=9 sigcode=128 addr=0x0
signal arrived during cgo execution

goroutine 14 gp=0xc0003ba540 m=9 mp=0xc000436808 [syscall]:
runtime.cgocall(0x7f041d14dd00, 0xc000074e28)
github.com/krypton-byte/neonize/utils._Cfunc_call_c_func_callback_bytes2(0x7f047522c0a0, 0xc00003e280, 0x46)
	_cgo_gotypes.go:54 +0x45
github.com/krypton-byte/neonize/utils.(*stdoutLogger).outputf(...)
	goneonize/utils/log.go:90 +0x22c
github.com/krypton-byte/neonize/utils.(*stdoutLogger).Debugf(...)
	goneonize/utils/log.go:96 +0x38
go.mau.fi/whatsmeow/socket.(*FrameSocket).readPump.func1()
	whatsmeow/socket/framesocket.go:206 +0x7c
go.mau.fi/whatsmeow/socket.(*FrameSocket).readPump(...)
	whatsmeow/socket/framesocket.go:216 +0x262

The crashing PC is the (freed) callback pointer itself. Trigger sequence in our host: agent teardown → Stop()connect() returns → client object destroyed → GC frees the thunks → readPump logs its shutdown line → SIGSEGV.

Fix we are running

Keep every thunk handed to Go alive forever in a module-level registry (_LIVE_CONNECT_REFS.append(...) before calling Neonize()). The leak is a few small objects per client construction, and it is immune to both the call returning and the client object being destroyed. Running in production since 2026-07-18; the previously 100%-reproducible teardown crash is gone (survived dozens of client recycles).

Patch: devjotaduo@272f97c (branch qwenpaw-0.4.3.1). Happy to open a PR if you want it — the same treatment probably belongs in the sync client too (registry instead of instance attribute).

Related: filed separately, a race in goneonize Stop() × FetchMe that crashes/wedges never-paired devices.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions