fix(host/security): close audit findings S1,#1,#4,#10,#12,#7,#6,S2-S6 (Linux/cross-platform)
Remediations from design/security-review-2026-06-28.md verified on Linux (cargo check/clippy/test green; Windows-gated paths verify in CI): - S1 [HIGH]: bump quinn-proto 0.11.14 -> 0.11.15 (RUSTSEC-2026-0185, pre-auth out-of-order STREAM reassembly memory exhaustion on the always-on default QUIC listener). - #1 [HIGH]: remove the unauthenticated nvhttp `GET /pin` endpoint; the GameStream PIN is delivered ONLY via the bearer-gated mgmt API, so a network client can no longer submit its own displayed PIN and self-pair. - #4 [HIGH->MED]: gate the unauthenticated RTSP/UDP media plane on a paired `/launch` and bind it to the launching client's source IP (threaded through the HTTPS handler), so an unpaired peer can neither start capture on an idle host nor ride a paired client's active launch. - #12: bound concurrent parked pairing waiters (MAX_PARKED_WAITERS) so a pre-auth peer can't pin unbounded 300s handshakes. +regression test. - #10: throttle the per-packet ENet control GCM-decrypt-failed warn (exponential backoff) so a junk flood can't spam the log. - #7 [MED->LOW]: serialize all process-global env mutation on the session-setup path under a new vdisplay::ENV_LOCK (apply_session_env / apply_input_env / the launch-cmd set_var / the gamescope env read), so concurrent native sessions can't race set_var/getenv (data-race UB -> host-wide DoS). Full per-session SessionContext threading remains a follow-up for cross-session value confusion. - #6 [MED]: move the gamescope EIS socket relay from world-writable /tmp to $XDG_RUNTIME_DIR (per-user 0700) and reject a symlinked relay file, so a local user can't intercept (keylog) or deny the remote session's input. - S2: a malformed client Opus mic frame now drops that frame instead of tearing down the shared host-lifetime virtual mic (cross-session DoS). - S3: track held buttons/keys in capped HashSets (was unbounded Vec with O(n) scans) so a paired client can't grow per-session input state. - S5: reject fps==0/absurd at the open_video chokepoint (covers Hello, ANNOUNCE, Reconfigure) so the encoder time_base/pts math can't div-by-0. - S6: bound the shared mic mpsc (drop-newest when full). - S4: cap Epic launcher-cache reads (catcache.bin/.item) so a planted giant can't OOM the host during library enumeration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,14 @@ use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
/// Out-of-band PIN delivery. Moonlight generates + displays a PIN; the user submits it
|
||||
/// (via the management API's `POST /api/v1/pair/pin` or nvhttp's `GET /pin?pin=NNNN`).
|
||||
/// `getservercert` parks until a PIN arrives.
|
||||
/// Out-of-band PIN delivery. Moonlight generates + displays a PIN; the operator submits it
|
||||
/// via the bearer-authenticated management API (`POST /api/v1/pair/pin`) only — there is no
|
||||
/// unauthenticated nvhttp delivery path (a network client must never be able to submit its
|
||||
/// own PIN; security-review 2026-06-28 #1). `getservercert` parks until a PIN arrives.
|
||||
/// Max pairing handshakes parked in [`PinGate::take`] at once (each holds a slot for up to
|
||||
/// 300s), bounding a pre-auth waiter flood. Real pairing is one operator-driven client at a time.
|
||||
const MAX_PARKED_WAITERS: usize = 4;
|
||||
|
||||
pub struct PinGate {
|
||||
pin: Mutex<Option<String>>,
|
||||
notify: Notify,
|
||||
@@ -48,7 +53,20 @@ impl PinGate {
|
||||
}
|
||||
|
||||
async fn take(&self, timeout: Duration) -> Option<String> {
|
||||
self.waiters.fetch_add(1, Ordering::SeqCst);
|
||||
// Bound the number of pairing handshakes parked at once: each `getservercert` is
|
||||
// pre-auth and parks for up to 300s, so without a cap an unpaired LAN peer could pin
|
||||
// unbounded tasks + keep `awaiting_pin` asserted (security-review 2026-06-28 #12).
|
||||
// Reserve a slot atomically; refuse (treated as "no PIN") once the cap is reached.
|
||||
if self
|
||||
.waiters
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| {
|
||||
(n < MAX_PARKED_WAITERS).then_some(n + 1)
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("pairing: too many handshakes awaiting a PIN — refusing");
|
||||
return None;
|
||||
}
|
||||
// Decrement on every exit path (PIN delivered, timeout, or future cancellation).
|
||||
struct WaiterGuard<'a>(&'a AtomicUsize);
|
||||
impl Drop for WaiterGuard<'_> {
|
||||
@@ -117,7 +135,8 @@ impl Pairing {
|
||||
|
||||
tracing::info!(
|
||||
uniqueid,
|
||||
"pairing phase 1 (getservercert) — awaiting PIN: submit `GET /pin?pin=NNNN`"
|
||||
"pairing phase 1 (getservercert) — awaiting PIN: deliver it via the management \
|
||||
API `POST /api/v1/pair/pin` (operator reads the PIN off the Moonlight client)"
|
||||
);
|
||||
let pin = self
|
||||
.pin
|
||||
@@ -304,4 +323,28 @@ mod tests {
|
||||
assert_eq!(pairing.pin.take(Duration::from_millis(10)).await, None);
|
||||
assert!(!pairing.pin.awaiting_pin());
|
||||
}
|
||||
|
||||
/// A pre-auth peer flood can park at most `MAX_PARKED_WAITERS` pairing handshakes; the next
|
||||
/// `take` is refused immediately (returns `None` without parking), bounding the 300s-waiter DoS
|
||||
/// (security-review 2026-06-28 #12).
|
||||
#[tokio::test]
|
||||
async fn pin_gate_caps_parked_waiters() {
|
||||
let pairing = Arc::new(Pairing::new());
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..MAX_PARKED_WAITERS {
|
||||
let p = pairing.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
p.pin.take(Duration::from_secs(5)).await
|
||||
}));
|
||||
}
|
||||
// Wait until all the slots are taken.
|
||||
while pairing.pin.waiters.load(Ordering::SeqCst) < MAX_PARKED_WAITERS {
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
// One more is refused right away (no parking), even with a long timeout.
|
||||
assert_eq!(pairing.pin.take(Duration::from_secs(5)).await, None);
|
||||
for h in handles {
|
||||
h.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user