//! Deriving pipewire-pulse's own PID — pure, no PipeWire and no `/proc` I/O. //! //! The owner bridge's key 4 is `application.process.id`. For a stream created //! by a **Pulse-emulated** client that PID is *pipewire-pulse's own*, shared //! verbatim across every unrelated Pulse app, so bridging on it would fuse //! every Pulse module into one tainted owner (design v3.4 §5.2 correction 5, //! §6.1.2). The engine therefore needs to know that one PID so it can refuse //! to bridge on it — and **every** way of deriving it can fail, in which case //! the safe answer is `None`: key 4 becomes unusable (coarser, never wrong). //! //! The derivation is split into two pure stages so the I/O — reading //! `/proc//comm` — stays in the adapter: //! //! 1. [`candidate`] finds the PID that *looks* like pulse from the graph //! alone: the `pipewire.sec.pid` value shared across multiple Clients. //! Native PipeWire clients carry their own distinct PID; only the //! Pulse shim repeats one value, so a repeated value is the signal. //! 2. [`validate`] confirms that candidate against the `comm` the adapter //! read from `/proc`. This is what closes **PID reuse**: a recycled PID //! that coincidentally repeats in the graph is rejected because //! `/proc//comm` now names a different process. //! //! Any failure at either stage — no repeated value, two repeated values, //! the property missing, `/proc` gone, a `comm` mismatch — yields `None`. use crate::host::taint::snapshot::ClientSnapshot; use std::collections::BTreeMap; /// The kernel `comm` of the pipewire-pulse process. `comm` is truncated to /// 15 bytes by the kernel; `pipewire-pulse` is 14 bytes, so it is exact — /// and exact is the only safe match, since a prefix match would accept a /// recycled PID belonging to e.g. `pipewire-pulseX`. const PULSE_COMM: &str = "pipewire-pulse"; /// Stage 1: the PID that looks like pipewire-pulse from the client graph. /// /// Returns `Some(pid)` only when **exactly one** `pipewire.sec.pid` value is /// shared by two or more clients. Rationale, matched to the failure matrix: /// /// - **consistent** — one value repeats, the rest (native clients) are /// distinct ⇒ that value. /// - **inconsistent** — two or more values each repeat ⇒ we cannot tell which /// is pulse ⇒ `None`. /// - **missing property** — the Pulse clients carry no `sec_pid` ⇒ nothing /// repeats ⇒ `None`. /// /// A count threshold of two is deliberate: a single client carrying a PID is /// indistinguishable from a lone native app, and pulse always mints many. pub fn candidate(clients: &[ClientSnapshot]) -> Option { let mut counts: BTreeMap = BTreeMap::new(); for client in clients { if let Some(pid) = client.sec_pid { *counts.entry(pid).or_insert(0) += 1; } } // Every PID seen on 2+ clients is a pulse candidate. If there is exactly // one such PID we trust it; zero or several ⇒ fail closed. let mut repeated = counts.iter().filter(|&(_, &n)| n >= 2).map(|(&pid, _)| pid); let first = repeated.next()?; if repeated.next().is_some() { // Ambiguous: more than one value repeats. return None; } Some(first) } /// Stage 2: confirm the candidate against the `comm` read from /// `/proc//comm`. /// /// `comm` is `None` when the adapter's read failed — the `/proc` entry is /// gone (the process exited between derivation and probe) — which is itself a /// reason to fail closed. A present-but-different `comm` is the **PID reuse** /// guard: the number is live but now belongs to someone else. pub fn validate(candidate: u32, comm: Option<&str>) -> Option { match comm { Some(PULSE_COMM) => Some(candidate), _ => None, } } /// The two stages composed, for callers that already hold the probed `comm`. /// The model keeps them separate (it recomputes the candidate as clients /// churn, and only re-probes when the candidate *changes*), so this is a /// convenience for tests and for the fully-resolved path. pub fn derive(clients: &[ClientSnapshot], comm_of: impl Fn(u32) -> Option) -> Option { let candidate = candidate(clients)?; validate(candidate, comm_of(candidate).as_deref()) }