Skip to content

SIGSEGV when a synchronous BLE read is called from inside a subscription callback #3

Description

@srgg

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).

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