fix(lite): refuse backend calls after a retired bridge is shut down; make the shutdown flag atomic
Concurrency audit (lite): LiteClientBridge::shutdownCalled_ was a plain bool, set by retire()/destruction on one thread and read nowhere by the detached sync/console/broadcast threads. A retired controller's in-flight execute() (send/save/sync) therefore still ran litelib_* against the PROCESS-GLOBAL client, which may already belong to a NEW controller after a server switch. - shutdownCalled_ is now std::atomic<bool>; shutdown() uses exchange() so exactly one caller tears the client down (idempotent) and the flag is published for the detached threads. - execute() and initializeNew/NewFromPhrase/Existing refuse once the bridge is shut down (shutDownResult), so a stale command can't reach the global client. Gating only — doSend/doShield, the JSON send payload, amounts/memo/recipients, persistAfterBroadcast, encrypt/unlock/export and api_.execute argument passing are byte-identical when the bridge is live. Test: testLiteClientBridgeRefusesAfterShutdown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12155,6 +12155,26 @@ void testLiteClientBridgeMutationEpoch()
|
||||
}
|
||||
}
|
||||
|
||||
// A retired bridge is shut down; every later backend call must be refused (litelib_* would otherwise run
|
||||
// on the process-global client, which may belong to a NEW controller by then). shutdown() is idempotent.
|
||||
void testLiteClientBridgeRefusesAfterShutdown()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
auto bridge = LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi());
|
||||
EXPECT_FALSE(bridge.isShutdown());
|
||||
EXPECT_TRUE(bridge.execute("balance", "").ok);
|
||||
bridge.shutdown();
|
||||
bridge.shutdown(); // idempotent (exchange): second call is a no-op
|
||||
EXPECT_TRUE(bridge.isShutdown());
|
||||
const auto before = bridge.mutationEpoch();
|
||||
const auto s = bridge.execute("send", "[]");
|
||||
EXPECT_FALSE(s.ok);
|
||||
EXPECT_FALSE(bridge.execute("save", "").ok);
|
||||
EXPECT_FALSE(bridge.initializeExisting(false, "https://x").ok);
|
||||
EXPECT_FALSE(bridge.initializeNew(false, "https://x").ok);
|
||||
EXPECT_TRUE(bridge.mutationEpoch() == before); // a refused send never reached the backend or bumped the epoch
|
||||
}
|
||||
|
||||
// M2b-3 hardening: the backend `sync` is a blocking, uninterruptible full scan. Destroying the
|
||||
// controller while a sync is in flight must NOT hang (the sync thread is detached, not joined).
|
||||
void testLiteWalletControllerShutdownDoesNotHangDuringSync()
|
||||
@@ -19514,6 +19534,7 @@ int main()
|
||||
testLiteWalletControllerWorkerProducesModel();
|
||||
testLiteWalletControllerWorkerChangeGate();
|
||||
testLiteClientBridgeMutationEpoch();
|
||||
testLiteClientBridgeRefusesAfterShutdown();
|
||||
testLiteWalletControllerShutdownDoesNotHangDuringSync();
|
||||
testSerialJobLane();
|
||||
testLiteGatewayRefreshPolicyHelpers();
|
||||
|
||||
Reference in New Issue
Block a user