host/taint: owner key 4 falls back to the Client's pipewire.sec.pid
Native PipeWire clients put no application.process.id on their nodes — only client.id. keys_of read node properties alone, so those nodes had no key 4, were therefore unbounded, and propagate_unresolved_owner excluded them the moment any tainted reader existed anywhere on the machine. Measured: an untagged mpv was eligible alone, and became unresolved-owner the instant peerspeak played audio. Since peerspeak playing audio is the only situation in which this feature runs, that amounted to "native PipeWire apps are never shareable". The tainted reader that armed it was sunshine, which is itself bounded — so this is the bounded-reader arm, not the keyless-reader case §6.1.1 narrates. The pid is one hop away, on the node's Client, already in the snapshot. RISK, and the guard on it: every Pulse-emulated Client carries pipewire-pulse's own PID as sec_pid — measured, 15 unrelated Clients sharing 2528 on this host. An unguarded fallback would fuse all of them into one owner. Exception 1 therefore applies to the fallback exactly as it does to the node's own property, so the fallback strictly *adds* correct bounding rather than trading it. Ambiguous client ids yield no fallback pid: inventing an owner key is the one direction that can reduce taint, so a coin toss is the wrong guess. The client index is threaded through a new OwnerCtx rather than a sixth positional Option<u32>, and evaluate() builds one and shares it, so the components and the key index cannot disagree about who is bounded. Round 10, R10-3. 6 new rows; 3 mutations verified — removing the fallback, dropping the pulse-pid exception (11 rows die), and resolving an ambiguous client id instead of dropping it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,10 @@ pub struct Graph {
|
||||
/// (GStreamer opens one per stream) pass clients explicitly instead.
|
||||
client_by_app: BTreeMap<u32, GlobalId>,
|
||||
client_by_module: BTreeMap<u64, GlobalId>,
|
||||
/// Native (non-Pulse-emulated) clients, whose `pipewire.sec.pid` is the
|
||||
/// app's **own** pid rather than pipewire-pulse's. See
|
||||
/// [`Graph::native_client_node`].
|
||||
native_client_by_app: BTreeMap<u32, GlobalId>,
|
||||
session_client: Option<GlobalId>,
|
||||
}
|
||||
|
||||
@@ -84,6 +88,35 @@ impl Graph {
|
||||
id
|
||||
}
|
||||
|
||||
/// A **native PipeWire** client's stream: `client.id` on the node, **no
|
||||
/// `application.process.id`**, and the app's real pid only on the Client
|
||||
/// as `pipewire.sec.pid`.
|
||||
///
|
||||
/// ⚠️ This is what an ordinary app actually looks like when it does not go
|
||||
/// through pipewire-pulse — measured for mpv on its default ao and for
|
||||
/// peerspeak's own playback stream. [`Graph::app_node`] models the
|
||||
/// Pulse-emulated shape, where the pid is on the node and the Client's
|
||||
/// `sec_pid` is the *daemon's*; both shapes are live on this host, and
|
||||
/// only this one exercises key 4's Client fallback (round 10, R10-3).
|
||||
pub fn native_client_node(&mut self, name: &str, role: MediaRole, pid: u32) -> NodeRef {
|
||||
let client = match self.native_client_by_app.get(&pid) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
let id = self.client(Some(pid));
|
||||
self.native_client_by_app.insert(pid, id);
|
||||
id
|
||||
}
|
||||
};
|
||||
self.node(
|
||||
name,
|
||||
role,
|
||||
NodeProps {
|
||||
client_id: Some(client),
|
||||
..NodeProps::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// An ordinary application stream: its own client, its own PID.
|
||||
pub fn app_node(&mut self, name: &str, role: MediaRole, pid: u32) -> NodeRef {
|
||||
let client = self.client_of_app(pid);
|
||||
|
||||
Reference in New Issue
Block a user