Summary
The ~40 c.bl.* blockchain/hardware methods on the mobile Client (in mobile/blockchain.go) are not gated by beginOp(), so they can run concurrently with Shutdown() and dereference a torn-down client → Go nil-pointer SIGSEGV → SIGABRT (hard crash on Android).
PR #241 added the beginOp/inflight-drain/ErrClientClosed lifecycle gate but only applied it to ConnectToBlox and Ping. Every blockchain read (BloxFreeSpace, GetFolderSize, GetDatastoreSize, ListActivePlugins, GetClusterInfo, PoolList, AccountExists, …) still does ctx := context.TODO(); return c.bl.X(ctx, c.bloxPid, …) with no closed-check, so they race Shutdown's c.h.Close() / c.ds.Close().
Impact (downstream)
In the fx-components box app, one shared native client is reused across bloxes. Re-setting-up the currently-selected blox runs initFula = logout + Shutdown + newClient while the still-mounted Blox screen keeps polling BloxFreeSpace/GetFolderSize/ListActivePlugins. The in-flight read nil-derefs the closing client. On-device logcat:
ReactNativeJS: Error listActivePlugins: ... all dials failed
ReactNative: Previous Fula client shutdown successfully.
ReactNative: Creating a new Fula instance / Fula initialized
E Go: panic: runtime error: invalid memory address or nil pointer dereference
E Go: [signal SIGSEGV: segmentation violation code=0x1 addr=0xb8 ...]
F libc: Fatal signal 6 (SIGABRT) ... land.fx.blox
Reproduced deterministically in a unit test: calling BloxFreeSpace() on a client after Shutdown() panics with nil pointer dereference ... BloxFreeSpace(0x0?) — identical to the field crash.
Fix
Gate all 44 c.bl.* methods through the existing beginOp():
ctx, done, err := c.beginOp()
if err != nil {
return nil, err // ErrClientClosed once Shutdown has started
}
defer done()
return c.bl.X(ctx, c.bloxPid, ...)
This (1) rejects calls that start after Shutdown with ErrClientClosed instead of nil-dereferencing, and (2) registers each call in inflight so Shutdown waits for in-flight calls to finish (and opCancel cancels them) before freeing the host/datastore. Two methods need bespoke handling: ReplicateInPool (returns bare []byte, so returns an error-JSON on closed) and ChatWithAI (gates the setup call but keeps context.TODO() for the streaming buffer that outlives the call).
Tests
mobile/client_shutdown_test.go: TestBlockchainMethodsAfterShutdownReturnErrClosed — table test asserting BloxFreeSpace/GetFolderSize/GetDatastoreSize/ListActivePlugins/GetClusterInfo/PoolList/AccountExists/ChatWithAI all return ErrClientClosed after Shutdown. Verified to panic (reproduce the crash) when the gate is removed, and pass with it.
Summary
The ~40
c.bl.*blockchain/hardware methods on the mobileClient(inmobile/blockchain.go) are not gated bybeginOp(), so they can run concurrently withShutdown()and dereference a torn-down client → Go nil-pointer SIGSEGV → SIGABRT (hard crash on Android).PR #241 added the
beginOp/inflight-drain/ErrClientClosedlifecycle gate but only applied it toConnectToBloxandPing. Every blockchain read (BloxFreeSpace,GetFolderSize,GetDatastoreSize,ListActivePlugins,GetClusterInfo,PoolList,AccountExists, …) still doesctx := context.TODO(); return c.bl.X(ctx, c.bloxPid, …)with no closed-check, so they raceShutdown'sc.h.Close()/c.ds.Close().Impact (downstream)
In the
fx-componentsbox app, one shared native client is reused across bloxes. Re-setting-up the currently-selected blox runsinitFula= logout +Shutdown+newClientwhile the still-mounted Blox screen keeps pollingBloxFreeSpace/GetFolderSize/ListActivePlugins. The in-flight read nil-derefs the closing client. On-device logcat:Reproduced deterministically in a unit test: calling
BloxFreeSpace()on a client afterShutdown()panics withnil pointer dereference ... BloxFreeSpace(0x0?)— identical to the field crash.Fix
Gate all 44
c.bl.*methods through the existingbeginOp():This (1) rejects calls that start after
ShutdownwithErrClientClosedinstead of nil-dereferencing, and (2) registers each call ininflightsoShutdownwaits for in-flight calls to finish (andopCancelcancels them) before freeing the host/datastore. Two methods need bespoke handling:ReplicateInPool(returns bare[]byte, so returns an error-JSON on closed) andChatWithAI(gates the setup call but keepscontext.TODO()for the streaming buffer that outlives the call).Tests
mobile/client_shutdown_test.go:TestBlockchainMethodsAfterShutdownReturnErrClosed— table test assertingBloxFreeSpace/GetFolderSize/GetDatastoreSize/ListActivePlugins/GetClusterInfo/PoolList/AccountExists/ChatWithAIall returnErrClientClosedafterShutdown. Verified to panic (reproduce the crash) when the gate is removed, and pass with it.