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:
@@ -670,11 +670,11 @@ pub fn start_restore_worker() -> std::sync::Arc<()> {
|
||||
}
|
||||
|
||||
/// Point the libei injector at the running gamescope's EIS socket (it reads the relay file
|
||||
/// [`EI_SOCKET_FILE`]). Best-effort — video still works without it (input just won't reach the
|
||||
/// [`ei_socket_file`]). Best-effort — video still works without it (input just won't reach the
|
||||
/// session). Shared by the attach and host-managed-session paths.
|
||||
fn point_injector_at_eis() {
|
||||
match find_gamescope_eis_socket() {
|
||||
Some(sock) => match std::fs::write(EI_SOCKET_FILE, &sock) {
|
||||
Some(sock) => match std::fs::write(ei_socket_file(), &sock) {
|
||||
Ok(()) => {
|
||||
tracing::info!(socket = %sock, "gamescope: pointed injector at the session's EIS socket")
|
||||
}
|
||||
@@ -770,18 +770,31 @@ fn stop_session(unit_name: &str) {
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--user", "stop", unit_name])
|
||||
.status();
|
||||
let _ = std::fs::remove_file(EI_SOCKET_FILE);
|
||||
let _ = std::fs::remove_file(ei_socket_file());
|
||||
}
|
||||
|
||||
/// File where the wrapper below writes gamescope's `LIBEI_SOCKET` (its EIS server socket),
|
||||
/// read by the libei injector to drive input into the nested app. See [`crate::inject`].
|
||||
pub const EI_SOCKET_FILE: &str = "/tmp/punktfunk-gamescope-ei";
|
||||
/// File where the wrapper below writes gamescope's `LIBEI_SOCKET` (its EIS server socket), read by
|
||||
/// the libei injector to drive input into the nested app. See [`crate::inject`].
|
||||
///
|
||||
/// Placed under `$XDG_RUNTIME_DIR` (a per-user, 0700 directory) — NOT a world-writable `/tmp` —
|
||||
/// so a second unprivileged local user can neither read the relayed socket path nor pre-plant the
|
||||
/// file to redirect the host's injector to a rogue EIS server (which would let them keylog or deny
|
||||
/// the remote session's keyboard/mouse input; security-review 2026-06-28 #6). Falls back to `/tmp`
|
||||
/// only if `XDG_RUNTIME_DIR` is unset (gamescope itself requires it, so this is rare); the reader
|
||||
/// ([`crate::inject`]) additionally rejects a symlinked relay file as defense-in-depth.
|
||||
pub fn ei_socket_file() -> std::path::PathBuf {
|
||||
let runtime = crate::vdisplay::with_env_lock(|| std::env::var_os("XDG_RUNTIME_DIR"));
|
||||
match runtime {
|
||||
Some(rt) if !rt.is_empty() => std::path::PathBuf::from(rt).join("punktfunk-gamescope-ei"),
|
||||
_ => std::path::PathBuf::from("/tmp/punktfunk-gamescope-ei"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn `gamescope --backend headless -W w -H h -r hz -- <app>`. The app comes from
|
||||
/// `PUNKTFUNK_GAMESCOPE_APP` (default a no-op that just keeps gamescope alive — set it to a real
|
||||
/// game/GL app for actual content, e.g. `steam -gamepadui` for the SteamOS-like session).
|
||||
/// stdout/stderr go to `/tmp/punktfunk-gamescope.log`. The app is launched through a tiny shell
|
||||
/// wrapper that relays gamescope's `LIBEI_SOCKET` (set for its children) to [`EI_SOCKET_FILE`]
|
||||
/// wrapper that relays gamescope's `LIBEI_SOCKET` (set for its children) to [`ei_socket_file`]
|
||||
/// so the input injector can connect to gamescope's EIS server from outside.
|
||||
fn spawn(w: u32, h: u32, hz: u32, cmd: Option<&str>) -> Result<Child> {
|
||||
// A non-empty per-session command (set via `set_launch_command`) wins; else the
|
||||
@@ -791,10 +804,13 @@ fn spawn(w: u32, h: u32, hz: u32, cmd: Option<&str>) -> Result<Child> {
|
||||
let app = cmd
|
||||
.map(str::to_string)
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.or_else(|| std::env::var("PUNKTFUNK_GAMESCOPE_APP").ok())
|
||||
// Read the env fallback under the shared env lock so it can't race a concurrent session's
|
||||
// `set_var` of the same key (security-review 2026-06-28 #7).
|
||||
.or_else(|| crate::vdisplay::with_env_lock(|| std::env::var("PUNKTFUNK_GAMESCOPE_APP").ok()))
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "sleep infinity".to_string());
|
||||
let _ = std::fs::remove_file(EI_SOCKET_FILE); // stale socket path from a previous session
|
||||
let relay = ei_socket_file();
|
||||
let _ = std::fs::remove_file(&relay); // stale socket path from a previous session
|
||||
let mut cmd = Command::new("gamescope");
|
||||
cmd.args(["--backend", "headless"])
|
||||
.args(["-W", &w.to_string()])
|
||||
@@ -804,7 +820,10 @@ fn spawn(w: u32, h: u32, hz: u32, cmd: Option<&str>) -> Result<Child> {
|
||||
.args([
|
||||
"sh",
|
||||
"-c",
|
||||
&format!("printf %s \"$LIBEI_SOCKET\" > {EI_SOCKET_FILE}; exec \"$@\""),
|
||||
&format!(
|
||||
"printf %s \"$LIBEI_SOCKET\" > '{}'; exec \"$@\"",
|
||||
relay.display()
|
||||
),
|
||||
"sh",
|
||||
])
|
||||
.args(app.split_whitespace())
|
||||
@@ -997,7 +1016,7 @@ impl Drop for GamescopeProc {
|
||||
let _ = self.0.wait();
|
||||
// Clear the relayed EIS socket name so the host-lifetime injector can't reconnect to this
|
||||
// now-dead session's socket between sessions (the stale path is the "Connection refused").
|
||||
let _ = std::fs::remove_file(EI_SOCKET_FILE);
|
||||
let _ = std::fs::remove_file(ei_socket_file());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user