Keep timeout-restore queues parked through the same-generation ready path

The deferral-buffer rework routes quarantine restores through a
same-generation ready transition that no longer rebuilds capability
state — but it drained both outbound queues unconditionally there,
bypassing the pending-convergence gate. Honor the restore reason in
that branch too: retrying deferred inbound ciphertext stays (it is
receive-side and safe under restored keys), while queue drains wait for
the convergence retry's establishment exactly as on the new-generation
path. Terminal restores keep draining immediately.

Also pre-drain the establishment's serialized forced announce in the
terminal-restore test so the tap only observes restore-driven traffic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-26 13:06:25 +02:00
co-authored by Claude Opus 4.8
parent b807b4da65
commit 4d2ade74dd
2 changed files with 20 additions and 1 deletions
+16 -1
View File
@@ -4892,8 +4892,23 @@ extension BLEService {
// A quarantined transport restored the same cryptographic // A quarantined transport restored the same cryptographic
// generation. Its capability proof and announce state never // generation. Its capability proof and announce state never
// became stale; only work queued while outbound keys were paused // became stale; only work queued while outbound keys were paused
// needs one idempotent ready transition. // needs one idempotent ready transition. Retrying the bounded
// early-ciphertext queue is receive-side and therefore always
// safe under the restored keys.
noisePacketHandler.handleSessionAuthenticated(normalizedPeerID) noisePacketHandler.handleSessionAuthenticated(normalizedPeerID)
#if DEBUG
_test_onPrivateMediaSessionReconciled?(normalizedPeerID)
#endif
if deferOutboundUntilConvergence {
// Timeout-restore: the counterpart may have completed the
// replacement handshake and discarded these keys, so
// encrypting the parked queues here would lose them silently.
// The restore's mandatory convergence retry or any later
// handshake the reconnect policy initiates re-enters this
// transition with a fresh generation and drains them under
// keys both sides hold.
return
}
sendPendingMessagesAfterHandshake(for: normalizedPeerID) sendPendingMessagesAfterHandshake(for: normalizedPeerID)
sendPendingNoisePayloadsAfterHandshake(for: normalizedPeerID) sendPendingNoisePayloadsAfterHandshake(for: normalizedPeerID)
return return
+4
View File
@@ -666,6 +666,10 @@ struct BLEServiceCoreTests {
) )
await ble._test_drainNoiseMessagePipeline() await ble._test_drainNoiseMessagePipeline()
#expect(ble.canDeliverSecurely(to: alicePeerID)) #expect(ble.canDeliverSecurely(to: alicePeerID))
// The establishment transition enqueues its forced announce as a
// separate serialized phase; drain again so it lands before the tap
// and cannot masquerade as restore-driven announce traffic below.
await ble._test_drainNoiseMessagePipeline()
let outbound = OutboundPacketTap() let outbound = OutboundPacketTap()
ble._test_onOutboundPacket = outbound.record ble._test_onOutboundPacket = outbound.record