Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,8 @@ Downgrading to a version that predates the counter lease after running a leased
The pre-wire gate is a point-in-time check, not a lock held across the flush, and this specifically affects **DM sessions**: `needs_pre_wire_flush()` inspects pending reservations once, and `flush()` skips any session entry that is currently checked out by a concurrent `load_session` call (`SessionEntry::CheckedOut`), still returning `Ok` for the entries it did persist. If another task checks out the same session between this send's lock release and its flush, that session's reservation can remain pending even though the flush "succeeded" — the caller proceeds to write its stanza regardless. This is a property of `SignalStoreCache::flush` itself, not specific to retries; it affects any pre-wire-gated DM send that races a concurrent operation on the same session. Sender-key entries have no analogous checked-out state — `get_sender_key` clones an `Arc` without removing the cached record, so every dirty sender-key entry is included in a flush's batch — so group/status sends are not exposed to this race.
</Warning>

Deleting a session or sender-key record — an identity change, a session reset, a rotated sender key — creates a tombstone rather than clearing the record's pending gate immediately. If a durability gate was open on the record at delete time, `SignalStoreCache` keeps it open until the backend's `delete_session`/`delete_sender_key` call actually succeeds; a failed delete leaves `needs_pre_wire_flush()` returning `true` and is retried on the next flush. Earlier, the gate was released as soon as the tombstone was applied to the in-memory cache, so a failed delete or a crash in that window could let ciphertext reach the wire while the pre-delete chain state was still loadable from the backend, re-deriving already-used key material on reload. A lossy `clear()` still drops a pending tombstone gate — the tombstone is discarded from the in-memory cache without issuing the backend delete, so the old chain state may remain in the backend.

### Clean reload vs. crash recovery

Fast-forwarding past a lease's reserved ceiling on every reload is the safe default, but it's also overly conservative for the common case: a clean reconnect or a same-process store re-creation never actually risked losing an in-flight send, yet unconditionally fast-forwarding still burned a full unused batch every time. 32 clean reconnects could push a sender-key chain 2,048 iterations ahead and get rejected once a peer who missed the intervening messages hit `MAX_FORWARD_JUMPS` (2,000).
Expand Down