Repository navigation
Keep a pairing's unconfirmed long-term PSK out of the record saves made while it is being saved #56
Description
Problem
While a pairing's long-term PSK save is in flight, the in-memory record already holds the new PSK. Another save of that record made meanwhile, from set_approval() or refresh_record(), encodes that PSK too. If the pairing's save fails but that save lands, the PSK reaches disk without being confirmed, against pairing.md "server/pair-finalize". Only put_back()'s later write removes it, so if that write also fails, the next start references a PSK the client never received. The client's handshake under it then fails and falls back to the Sentinel, and with its approval cleared by the pairing, it must pair again.
Where
Pinned to origin/main (424352a):
store_long_term_psk()sets the record with the new PSK before its save:sendspin-server-cpp/src/server_core.cpp
Lines 1069 to 1070 in 424352a
this->records_.set(record); this->save_record(client_id, [this, id = client_id, done = std::move(done)](bool saved) { refresh_record()saves the record as it is in memory:sendspin-server-cpp/src/server_core.cpp
Lines 983 to 984 in 424352a
this->records_.set(updated); this->save_record(client.info.client_id); set_approval()the same:sendspin-server-cpp/src/server_core.cpp
Lines 1024 to 1030 in 424352a
this->records_.set(record); const auto unsaved = this->unsaved_records_.find(client_id); if (unsaved != this->unsaved_records_.end()) { unsaved->second.approval_set = true; } // pairing.md "Unpaired Access": approval SHOULD persist this->save_record(client_id); put_back(), the only write that removes the PSK again:sendspin-server-cpp/src/server_core.cpp
Lines 1117 to 1118 in 424352a
this->records_.set(restored); this->save_record(client_id);
Evidence
Confirmed by tracing; not reproduced. With a provider that fails chosen saves:
store_long_term_psk(a, psk)posts the PSK save. Before its outcome reaches core, the client connects under a new name (or the operator approves it), which posts a save that encodespsk.- The PSK save fails and the second save lands, so
pskis on disk. put_back()'s save fails.- The next start loads
awithpsk.
PR #53 (issue #13) retries a failed put_back() write before later record writes and at stop, which narrows this. It also adds a second way in: the second save fails, is kept for retry, and is retried onto disk before put_back()'s write runs. Both ways end the same.
Direction
A save made while the client's PSK save is pending encodes the record that handshakes reference: UnsavedRecord::before's long-term PSK, with the other fields from memory. The pairing's success path then saves the record once more, so a save queued behind the PSK save cannot drop the PSK, at the cost of one extra write per successful pairing. Making those saves non-retriable is not enough on its own: it leaves step 2 open, and shutdown() calls put_back() while the client is still in unsaved_records_.
Done when
- A save of a client's record made while its long-term PSK save is pending never encodes that PSK.
- A test with a scripted provider (PSK save refused, the name or approval save accepted,
put_back()'s save refused) shows the next load has no long-term PSK for the client. It fails before the change. - A Control row: a pairing whose save lands keeps its PSK on disk after a name or approval save queued behind it.
Activity
kahrendt commentedon Oct 15, 2026
Claimed 84ae70b6; branch issue-56-unconfirmed-psk
kahrendt commentedon Oct 15, 2026
Draft PR: #91
Keep a pairing's unconfirmed long-term PSK out of the record saves be…