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:
@@ -577,10 +577,11 @@ impl LibraryProvider for EpicProvider {
|
||||
if p.extension().and_then(|e| e.to_str()) != Some("item") {
|
||||
continue;
|
||||
}
|
||||
let Ok(text) = std::fs::read_to_string(&p) else {
|
||||
// `.item` manifests are small JSON; cap the read so a planted giant can't OOM the host.
|
||||
let Some(bytes) = read_capped(&p, 1024 * 1024) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
|
||||
let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(g) = epic_entry(&v, &art) {
|
||||
@@ -650,6 +651,23 @@ fn epic_entry(
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a launcher cache/manifest with a hard size cap, so a local unprivileged user can't plant a
|
||||
/// multi-GB file under the launcher's (Users-writable) data dir that OOMs the privileged host when
|
||||
/// it's loaded — then base64/JSON-decoded into further copies — during library enumeration
|
||||
/// (security-review 2026-06-28 S4). Returns `None` if missing, empty, or over `max`. Mirrors the
|
||||
/// Linux lutris-art reader's 1 MiB cap.
|
||||
#[cfg(windows)]
|
||||
fn read_capped(path: &Path, max: u64) -> Option<Vec<u8>> {
|
||||
let meta = std::fs::metadata(path).ok()?;
|
||||
if meta.len() == 0 || meta.len() > max {
|
||||
if meta.len() > max {
|
||||
tracing::warn!(path = %path.display(), len = meta.len(), max, "launcher cache exceeds size cap — skipping");
|
||||
}
|
||||
return None;
|
||||
}
|
||||
std::fs::read(path).ok()
|
||||
}
|
||||
|
||||
/// Best-effort parse of `catcache.bin` (base64-encoded JSON array of catalog items) into
|
||||
/// catalogItemId → [`Artwork`] from each item's `keyImages`. Empty map on any read/decode failure
|
||||
/// (the format is community-reverse-engineered + can lag a fresh install → titles just show no art).
|
||||
@@ -657,7 +675,8 @@ fn epic_entry(
|
||||
fn epic_art_index(catcache: &Path) -> std::collections::HashMap<String, Artwork> {
|
||||
use base64::Engine as _;
|
||||
let mut map = std::collections::HashMap::new();
|
||||
let Ok(raw) = std::fs::read(catcache) else {
|
||||
// 32 MiB cap: comfortably fits a real catalog cache, blocks a planted giant (S4).
|
||||
let Some(raw) = read_capped(catcache, 32 * 1024 * 1024) else {
|
||||
return map;
|
||||
};
|
||||
let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(raw) else {
|
||||
|
||||
Reference in New Issue
Block a user