@@ -3,11 +3,75 @@ package chdbpurego
33import (
44 "os"
55 "os/exec"
6+ "syscall"
67 "unsafe"
78
89 "github.com/ebitengine/purego"
910)
1011
12+ // sigactionBufSize is large enough to hold a struct sigaction on every
13+ // platform we care about: 16 B on macOS, ~152 B on glibc/Linux.
14+ const sigactionBufSize = 256
15+
16+ // signalsToProtect lists the signals the Go runtime owns and must keep
17+ // owning. libchdb's chdb_set_signal_handlers_enabled(0) wipes the kernel's
18+ // handler list for the same set as a side effect; we save and restore these
19+ // around that call so Go's handlers survive.
20+ //
21+ // Signal numbers are NOT the same on Linux and macOS (e.g. SIGBUS is 10 on
22+ // Darwin but 7 on Linux; SIGURG is 16 on Darwin but 23 on Linux). Use the
23+ // syscall package's per-platform constants so the values resolve correctly
24+ // at compile time on each OS.
25+ var signalsToProtect = []int {
26+ int (syscall .SIGILL ),
27+ int (syscall .SIGABRT ),
28+ int (syscall .SIGFPE ),
29+ int (syscall .SIGBUS ),
30+ int (syscall .SIGSEGV ),
31+ int (syscall .SIGURG ), // Go uses SIGURG for async preemption
32+ }
33+
34+ // libcSigaction is the libc sigaction(2) function, resolved from whichever
35+ // libc the process was already linked against. We pass opaque buffers
36+ // rather than typed structs so the same code works for the differently-
37+ // laid-out struct sigaction on macOS vs glibc.
38+ var libcSigaction func (sig int , act , oact unsafe.Pointer ) int
39+
40+ func loadSigaction () {
41+ // Prefer the empty-path form: on Linux (glibc, musl) dlopen("") returns
42+ // a handle to the running process's symbol table, which always contains
43+ // sigaction because libc is already loaded by the Go runtime. No path
44+ // to maintain.
45+ libc , err := purego .Dlopen ("" , purego .RTLD_NOW )
46+ if err != nil {
47+ // macOS dyld interprets "" as a file lookup, not as "current
48+ // process", so the empty-path form fails. Fall back to libSystem,
49+ // which is always present and always contains sigaction.
50+ libc , err = purego .Dlopen ("/usr/lib/libSystem.B.dylib" , purego .RTLD_NOW )
51+ }
52+ if err != nil {
53+ panic ("chdb-purego: cannot resolve sigaction(2): " + err .Error ())
54+ }
55+ purego .RegisterLibFunc (& libcSigaction , libc , "sigaction" )
56+ }
57+
58+ // snapshotSignalHandlers stores the current sigaction state for the signals
59+ // we need to protect, in opaque buffers.
60+ func snapshotSignalHandlers () [][sigactionBufSize ]byte {
61+ saved := make ([][sigactionBufSize ]byte , len (signalsToProtect ))
62+ for i , sig := range signalsToProtect {
63+ libcSigaction (sig , nil , unsafe .Pointer (& saved [i ][0 ]))
64+ }
65+ return saved
66+ }
67+
68+ // restoreSignalHandlers writes a previously-snapshotted sigaction state back.
69+ func restoreSignalHandlers (saved [][sigactionBufSize ]byte ) {
70+ for i , sig := range signalsToProtect {
71+ libcSigaction (sig , unsafe .Pointer (& saved [i ][0 ]), nil )
72+ }
73+ }
74+
1175func findLibrary () string {
1276 // Env var
1377 if envPath := os .Getenv ("CHDB_LIB_PATH" ); envPath != "" {
@@ -66,6 +130,13 @@ var (
66130 chdbResultStorageRowsRead func (result * chdb_result ) uint64
67131 chdbResultStorageBytesRead func (result * chdb_result ) uint64
68132 chdbResultError func (result * chdb_result ) string
133+
134+ // Process-wide signal handler control. See issue #30: by default libchdb
135+ // installs its own SIGSEGV/SIGABRT/SIGBUS/SIGILL handlers when a
136+ // connection is opened, which overwrites the Go runtime's handlers and
137+ // breaks async preemption (SIGURG) and stack-growth (SIGSEGV) handling.
138+ chdbSetSignalHandlersEnabled func (enabled int )
139+ chdbResetSignalHandlers func ()
69140)
70141
71142func init () {
@@ -105,4 +176,59 @@ func init() {
105176 purego .RegisterLibFunc (& chdbResultStorageBytesRead , libchdb , "chdb_result_storage_bytes_read" )
106177 purego .RegisterLibFunc (& chdbResultError , libchdb , "chdb_result_error" )
107178
179+ // Signal handler protection (issue #30). The required API was added in
180+ // libchdb v26.x via chdb-core#11. Pre-check the symbol with Dlsym so
181+ // that older libchdb builds — which don't export
182+ // chdb_set_signal_handlers_enabled — degrade gracefully instead of
183+ // panicking inside RegisterLibFunc at init. On those builds the crash
184+ // from issue #30 stays unfixed, but the binding still loads.
185+ if sym , dlsymErr := purego .Dlsym (libchdb , "chdb_set_signal_handlers_enabled" ); dlsymErr == nil && sym != 0 {
186+ purego .RegisterLibFunc (& chdbSetSignalHandlersEnabled , libchdb , "chdb_set_signal_handlers_enabled" )
187+ purego .RegisterLibFunc (& chdbResetSignalHandlers , libchdb , "chdb_reset_signal_handlers" )
188+
189+ // Tell libchdb NOT to install its own signal handlers on the
190+ // first chdb_connect(). Without this, libchdb's ClickHouse
191+ // daemon code installs handlers for SIGSEGV / SIGABRT / SIGBUS
192+ // / SIGILL / SIGFPE the first time a connection is opened,
193+ // overwriting the handlers the Go runtime relies on for stack
194+ // growth and panic recovery. When a Go signal then lands in
195+ // libchdb's handler instead of the runtime's, the result is
196+ // the rare std::mutex::unlock crash described in issue #30 —
197+ // most often on macOS arm64 CI where signal pressure is
198+ // highest.
199+ //
200+ // Important: chdb_set_signal_handlers_enabled(0) doesn't only
201+ // set a flag — it also wipes the existing handlers in the same
202+ // set back to SIG_DFL (verified empirically; the chdb.h header
203+ // doesn't mention this side effect). Since the Go runtime has
204+ // already installed its handlers by the time this package's
205+ // init runs, we have to save Go's handlers, call the disable,
206+ // then restore them. After this dance the libchdb flag is set
207+ // (so no future connect installs handlers) and Go's handlers
208+ // remain in place.
209+ loadSigaction ()
210+ func () {
211+ defer guardSignalHandlers ()()
212+ chdbSetSignalHandlersEnabled (0 )
213+ }()
214+ }
215+ }
216+
217+ // signalProtectionAvailable reports whether the running libchdb exposes the
218+ // signal handler control API. When false, snapshotSignalHandlers and
219+ // restoreSignalHandlers must NOT be called (libcSigaction was not loaded).
220+ func signalProtectionAvailable () bool {
221+ return chdbSetSignalHandlersEnabled != nil
222+ }
223+
224+ // guardSignalHandlers returns a function suitable for `defer`-ing around a
225+ // call into libchdb. When signal protection is available, it captures the
226+ // current sigaction state now and the returned closure restores it. When
227+ // not available (old libchdb), both calls are no-ops.
228+ func guardSignalHandlers () func () {
229+ if ! signalProtectionAvailable () {
230+ return func () {}
231+ }
232+ saved := snapshotSignalHandlers ()
233+ return func () { restoreSignalHandlers (saved ) }
108234}
0 commit comments