Summary
Calling a synchronous BLE operation (blim.characteristic(svc, uuid).read()) from inside a Lua subscription callback crashes the whole process with SIGSEGV instead of raising a Lua error. The crash is not in the user script — it is memory corruption of the shared lua_State, surfacing later in golua's error path.
Environment
- blim @
61d4c17 (embedded blim.lua), darwin/arm64
- go 1.26.1, golua
v0.0.0-20250217091409-248753f411c4, srgg/go-ble v0.0.0-20251019021511-162008c817dc
Reproduction
blim.subscribe({{ service = "ff30", chars = { "ff32" } }}, function(data)
-- any synchronous BLE op inside a notification callback:
local char = blim.characteristic("ff50", "ff55")
if char then char.read() end -- <- SIGSEGV on first notification
end)
Real-world case: a UI redraw function that performs a characteristic read was invoked from a state-change notification callback. Crash is deterministic on the first firing.
Stack trace (relevant goroutine)
SIGSEGV addr=0x184, during cgo execution
goroutine 57:
golua lua.(*State).StackTrace -> _Cfunc_lua_getinfo <- crashes collecting traceback
golua lua.(*State).callEx (lua.go:267) <- error path of the callback Call
internal/lua.(*LuaAPI).callLuaCallback (api.go:1089)
internal/lua.(*LuaEngine).DoWithState (lua_engine.go:208)
internal/lua.(*LuaAPI).executeSubscription (api.go:904)
go-ble.(*BLEConnection).runSubscription (connection_subscription.go:491)
Analysis
The lua_State is single and guarded by DoWithState. While a subscription callback runs, the state is held by the subscription goroutine. A synchronous read() issued from that context dispatches a second BLE operation whose completion/dispatch path touches the same lua_State from another goroutine. Concurrent access corrupts the Lua stack; the callback's Call then errors, and StackTrace/lua_getinfo segfaults walking the corrupted stack (addr=0x184 — small offset from NULL, i.e. a broken CallInfo). Note the existing inCallback guard protects the debug-hook/blim.sleep path, but synchronous device operations have no equivalent guard.
Expected behavior
At minimum, deterministic failure instead of process death — e.g. read()/write()/write_receive() invoked while inCallback > 0 should raise a clean Lua error:
"BLE operations are not allowed inside subscription callbacks; defer to the main loop".
(Actually supporting nested sync ops via queueing/deferral would be a separate, larger feature.)
Workaround
Perform BLE operations only from the main loop; subscription callbacks should only set flags/cache values (blim.sleep in the loop releases the state so queued callbacks run).
Summary
Calling a synchronous BLE operation (
blim.characteristic(svc, uuid).read()) from inside a Lua subscription callback crashes the whole process with SIGSEGV instead of raising a Lua error. The crash is not in the user script — it is memory corruption of the sharedlua_State, surfacing later in golua's error path.Environment
61d4c17(embedded blim.lua), darwin/arm64v0.0.0-20250217091409-248753f411c4, srgg/go-blev0.0.0-20251019021511-162008c817dcReproduction
Real-world case: a UI redraw function that performs a characteristic read was invoked from a state-change notification callback. Crash is deterministic on the first firing.
Stack trace (relevant goroutine)
Analysis
The
lua_Stateis single and guarded byDoWithState. While a subscription callback runs, the state is held by the subscription goroutine. A synchronousread()issued from that context dispatches a second BLE operation whose completion/dispatch path touches the samelua_Statefrom another goroutine. Concurrent access corrupts the Lua stack; the callback'sCallthen errors, andStackTrace/lua_getinfosegfaults walking the corrupted stack (addr=0x184— small offset from NULL, i.e. a broken CallInfo). Note the existinginCallbackguard protects the debug-hook/blim.sleeppath, but synchronous device operations have no equivalent guard.Expected behavior
At minimum, deterministic failure instead of process death — e.g.
read()/write()/write_receive()invoked whileinCallback > 0should raise a clean Lua error:"BLE operations are not allowed inside subscription callbacks; defer to the main loop".(Actually supporting nested sync ops via queueing/deferral would be a separate, larger feature.)
Workaround
Perform BLE operations only from the main loop; subscription callbacks should only set flags/cache values (
blim.sleepin the loop releases the state so queued callbacks run).