Sunshine version: 2025.924.154138 (Fedora/Nobara 43 package)
OS: Linux (Nobara 43), NVIDIA, KMS capture
Client: moonlight-web-stream (Rust moonlight-common), but the bug is client-agnostic
Summary
nvhttp.cpp stores pending pair sessions in map_id_sess keyed by client uniqueid, inserted with map_id_sess.emplace(...). emplace never overwrites an existing entry, and entries are not reliably cleaned up when a pairing attempt is abandoned (client disconnect/timeout) or fails. Once a stale entry exists with last_phase != NONE, every later getservercert from the same uniqueid silently reuses the poisoned session:
- New attempt:
getservercert → emplace fails (key exists) → the old session object is reused, only its salt/response handle are updated.
- User submits the PIN →
pin() → getservercert(sess, ...) → if (sess.last_phase != PAIR_PHASE::NONE) fail_pair("Out of order call to getservercert").
- The held
/pair connection is closed without a usable response; the client reports a generic pairing failure.
From that point, all pairing attempts for that client ID fail the same way until Sunshine is restarted. Nothing in the web UI or logs (at info level) explains why, so users retry in a loop.
A second aggravating detail: pin() completes std::begin(map_id_sess)->second — the first session in the map — so with more than one pending session the PIN can be applied to the wrong one, which both fails the intended attempt ("pin was incorrect" on the client) and advances/poisons another session.
Reproduction
- Client A sends
getservercert for uniqueid X, then dies/never gets a PIN (or a PIN meant for a different attempt is submitted — anything that leaves the session with last_phase != NONE).
- Client A (same uniqueid X) starts a fresh attempt:
getservercert.
- Enter the correct PIN in the web UI.
- Pairing fails; server-side debug shows
Out of order call to getservercert. Repeat 2–3 forever; only a Sunshine restart recovers.
Observed live over several attempts (timestamps, socat captures of the /pair exchanges, and the client-side IncompleteMessage errors available on request).
Suggested fix
In the getservercert phrase handler, replace-or-reset the session for that uniqueid instead of emplace (e.g. insert_or_assign with a fresh pair_session_t, after completing/aborting any held response on the old one). Keying the PIN submission to the salt (or rejecting pin() when multiple sessions are pending) would also fix the wrong-session case.
Minor related observation
While capturing the exchanges, /unpair requests were answered with two back-to-back HTTP responses on one connection (HTTP/1.1 200 OK with body <root status_code="404"/>, immediately followed by HTTP/1.1 404 NOT FOUND with the same body), which strict HTTP clients treat as a framing error. Happy to file separately if preferred.
Sunshine version: 2025.924.154138 (Fedora/Nobara 43 package)
OS: Linux (Nobara 43), NVIDIA, KMS capture
Client: moonlight-web-stream (Rust moonlight-common), but the bug is client-agnostic
Summary
nvhttp.cppstores pending pair sessions inmap_id_sesskeyed by clientuniqueid, inserted withmap_id_sess.emplace(...).emplacenever overwrites an existing entry, and entries are not reliably cleaned up when a pairing attempt is abandoned (client disconnect/timeout) or fails. Once a stale entry exists withlast_phase != NONE, every latergetservercertfrom the sameuniqueidsilently reuses the poisoned session:getservercert→emplacefails (key exists) → the old session object is reused, only its salt/response handle are updated.pin()→getservercert(sess, ...)→if (sess.last_phase != PAIR_PHASE::NONE) fail_pair("Out of order call to getservercert")./pairconnection is closed without a usable response; the client reports a generic pairing failure.From that point, all pairing attempts for that client ID fail the same way until Sunshine is restarted. Nothing in the web UI or logs (at info level) explains why, so users retry in a loop.
A second aggravating detail:
pin()completesstd::begin(map_id_sess)->second— the first session in the map — so with more than one pending session the PIN can be applied to the wrong one, which both fails the intended attempt ("pin was incorrect" on the client) and advances/poisons another session.Reproduction
getservercertfor uniqueid X, then dies/never gets a PIN (or a PIN meant for a different attempt is submitted — anything that leaves the session withlast_phase != NONE).getservercert.Out of order call to getservercert. Repeat 2–3 forever; only a Sunshine restart recovers.Observed live over several attempts (timestamps, socat captures of the
/pairexchanges, and the client-sideIncompleteMessageerrors available on request).Suggested fix
In the
getservercertphrase handler, replace-or-reset the session for that uniqueid instead ofemplace(e.g.insert_or_assignwith a freshpair_session_t, after completing/aborting any held response on the old one). Keying the PIN submission to the salt (or rejectingpin()when multiple sessions are pending) would also fix the wrong-session case.Minor related observation
While capturing the exchanges,
/unpairrequests were answered with two back-to-back HTTP responses on one connection (HTTP/1.1 200 OKwith body<root status_code="404"/>, immediately followed byHTTP/1.1 404 NOT FOUNDwith the same body), which strict HTTP clients treat as a framing error. Happy to file separately if preferred.