host/taint: correct the buffered-echo scoping (in-threat-model); pin ambiguous-client (Codex round 6)
Codex refuted my round-5 disposition and was right: the buffered-echo gap is NOT limited to keyless/unbounded readers. A normal PID-bearing app — recorder, DAW, GStreamer — can read the call, buffer it in application memory, fully tear down its PipeWire Node *and* Client, then (still the same live process) open a fresh Client + output and replay. `seed_sticky` drops the PID fingerprint once every old serial is gone, so the replayed leg is Eligible. That is in-threat-model, so my "outside the threat model" claim was false. - Rewrote the module-doc gap note honestly: in-threat-model, reachable by non-adversarial software, sitting on the design's §6.1.3 "full teardown ⇒ starts clean" boundary. Framed the two options — (A) accept as a documented v1 limitation, (B) process-generation lifetime (PID + /proc start-time, phase 3 supplies liveness, §6.1.3 revised). This is a designer's decision (it revises the security surface); NOT resolved in code. `a_fingerprint_does_not_outlive_its_owner` currently encodes Option A and flips under B. - P2 (fixed): pinned the ambiguous-client-id branch. A mutation remembering only the first of two clients claiming one global id survived the suite; added a test scoped to the ambiguous owner (the global count was masked by the peerspeak owner's client). Verified the `.next()` mutation now fails it. 57 tests. Phase 2 is NOT converged — the buffered-echo design decision is owed to the user before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1745,3 +1745,64 @@ fn a_local_root_receiver_bridges_without_an_inbound_link() {
|
||||
);
|
||||
assert_tainted(&decisions, sink, "pixelpass-owned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ambiguous_client_id_remembers_every_claimant_for_stickiness() {
|
||||
// Round 6 finding 2: nothing pinned the ambiguous-Client branch, so a
|
||||
// mutation remembering only the first claimant survived. Two live
|
||||
// clients claim one global id; the tainted owner's node references it.
|
||||
// If we remember only one and it is the one that later disappears, the
|
||||
// still-live claimant that reopens an output escapes.
|
||||
use super::snapshot::{ClientSnapshot, GlobalId};
|
||||
|
||||
let mut graph = Graph::new();
|
||||
let hw = graph.device_node("hw-sink", MediaRole::Sink);
|
||||
let call = graph.peerspeak_node("peerspeak", 7);
|
||||
graph.link(call, hw);
|
||||
|
||||
// Two clients share one global id (the observer saw an id collision).
|
||||
let shared_id = graph.dangling_id();
|
||||
let client_a = graph.client_with_id(shared_id, Some(PULSE_PID));
|
||||
let _client_b = graph.client_with_id(shared_id, Some(PULSE_PID));
|
||||
assert_eq!(client_a, shared_id);
|
||||
|
||||
// The tainted reader references that (ambiguous) client id, no PID/keys.
|
||||
let reader = graph.node(
|
||||
"reader",
|
||||
MediaRole::StreamInput,
|
||||
NodeProps {
|
||||
client_id: Some(shared_id),
|
||||
..NodeProps::default()
|
||||
},
|
||||
);
|
||||
let out = graph.node(
|
||||
"out",
|
||||
MediaRole::StreamOutput,
|
||||
NodeProps {
|
||||
client_id: Some(shared_id),
|
||||
..NodeProps::default()
|
||||
},
|
||||
);
|
||||
graph.link(hw, reader);
|
||||
let firefox = graph.app_node("firefox", MediaRole::StreamOutput, 11114);
|
||||
|
||||
let c = ctx();
|
||||
let (first, sticky) = evaluate(&graph.build(), &c, &StickyState::default());
|
||||
assert_eq!(
|
||||
first.candidates[&out.serial].reason().map(Reason::code),
|
||||
Some("tainted-owner-bridge")
|
||||
);
|
||||
// Both claimants must be remembered, or a mutation keeping only one
|
||||
// could drop the surviving owner. At least both client serials appear.
|
||||
let client_members: usize = sticky
|
||||
.owners
|
||||
.iter()
|
||||
.flat_map(|o| o.members.iter())
|
||||
.filter(|m| matches!(m, super::ObjectRef::Client(_)))
|
||||
.count();
|
||||
assert!(
|
||||
client_members >= 2,
|
||||
"both ambiguous-id clients should be remembered: {sticky:#?}"
|
||||
);
|
||||
let _ = (ClientSnapshot { serial: Serial(0), id: GlobalId(0), sec_pid: None }, firefox);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user