Skip to content

Keep a pairing's unconfirmed long-term PSK out of the record saves made while it is being saved #56

Closed

Description

@kahrendt

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:
    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:
    this->records_.set(updated);
    this->save_record(client.info.client_id);
  • set_approval() the same:
    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:
    this->records_.set(restored);
    this->save_record(client_id);

Evidence

Confirmed by tracing; not reproduced. With a provider that fails chosen saves:

  1. 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 encodes psk.
  2. The PSK save fails and the second save lands, so psk is on disk.
  3. put_back()'s save fails.
  4. The next start loads a with psk.

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

  1. self-assigned this
    on Oct 15, 2026
  2. kahrendt commented on Oct 15, 2026

    @kahrendt
    ContributorAuthor

    Claimed 84ae70b6; branch issue-56-unconfirmed-psk

  3. kahrendt commented on Oct 15, 2026

    @kahrendt
    ContributorAuthor

    Draft PR: #91

  4. added a commit that references this issue on Oct 15, 2026
    9331eb7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions