host/taint: close the partial fixes found in Codex round 2
The verification round earned its place: five of the six round-1 fixes were partial, and two of the gaps were worse than the bugs they replaced. 1. ⚠️ The round-1 sticky fix smuggled the suppressed key back in. `client_serials_of` recorded the shared `WirePlumber [export]` client as a member of a tainted hardware sink's owner, so the *second* recompute expanded that client to every sound card on the box, tainted the microphone, and excluded every app holding one — the §6.1.1 catastrophe arriving one epoch late instead of never. `client.id` may now only be recorded, or expanded, for nodes where it is a usable owner key. The regression test evaluates an unchanged snapshot three times: a correct engine's answer must not drift when nothing has. 2. Sticky followed a surviving *connection*, not a surviving *owner*. A process can leave one client idle and open a second — GStreamer opens one per stream as a matter of course — and the new leg escaped. `StickyOwner` now carries owner **fingerprints** (strong keys and a usable PID, never `client.id`), applied only while some serial member is still live, so a recyclable key cannot resurrect a dead owner. 3. An **ambiguous** link input endpoint tainted every claimant but made none of them a receiver, so their sibling output legs stayed eligible. Taint without receiver status cannot start an owner bridge. 4. `device.id` is a raw observation, not the classification the coarse-key exception needs — PipeWire defines it only as "the Device this node belongs to", so a forwarding node carrying one would have lost both its owner keys and its ability to trip the backstop. Replaced by `session_device`, a phase-3 obligation (`device.id` AND `device.api`) documented to fail closed when it cannot classify. 5. Readiness now gates sticky **retirement only**. Round 1 stopped a not-ready epoch erasing history; it also stopped it recording any, so a reader could consume and buffer the call during that epoch, vanish before readiness, and leave its output eligible. 6. Added the unresolved-output-plus-unknown-role fixture: deleting one `receivers.insert` survived all 42 previous tests. Mutation-verified: 7/7 reverts killed by their intended test. Two attempts did not land first time and both were my error, not the engine's — the client-key guard is applied at two sites so removing one is not a revert (removing the pair is, and that is killed), and the fingerprint-lifetime test put the recycled node in a snapshot *after* the entry had already been retired, so the guard was never consulted. Rewritten to place it in the same snapshot that first sees the owner gone. Cost comment corrected again, to O(D·(V+E+Σ|sources|·|targets|)). 49 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -131,8 +131,16 @@ impl Graph {
|
||||
id
|
||||
}
|
||||
};
|
||||
let device_id = self.id();
|
||||
self.node(name, role, device(session, SESSION_PID, device_id))
|
||||
self.node(name, role, device(session, SESSION_PID))
|
||||
}
|
||||
|
||||
/// A node that *belongs to* a Device but is not a passive device node —
|
||||
/// a filter associated with a card. Phase 3 must not classify this as a
|
||||
/// session device, or it loses both its coarse owner keys and its
|
||||
/// ability to trip the fail-closed backstop.
|
||||
pub fn device_associated_filter(&mut self, name: &str, role: MediaRole, pid: u32) -> NodeRef {
|
||||
let client = self.client_of_app(pid);
|
||||
self.node(name, role, app(client, pid))
|
||||
}
|
||||
|
||||
/// A **virtual** sink an application created natively: an `Audio/Sink`
|
||||
@@ -313,11 +321,11 @@ pub fn link_group(group: &str, client: GlobalId, pid: u32) -> NodeProps {
|
||||
/// here is deliberately *more* pessimistic than reality — it hands the
|
||||
/// engine a second coarse key it could fuse devices on, so a test that
|
||||
/// passes here also passes against the real props.
|
||||
pub fn device(session_client: GlobalId, session_pid: u32, device_id: GlobalId) -> NodeProps {
|
||||
pub fn device(session_client: GlobalId, session_pid: u32) -> NodeProps {
|
||||
NodeProps {
|
||||
client_id: Some(session_client),
|
||||
process_id: Some(session_pid),
|
||||
device_id: Some(device_id),
|
||||
session_device: true,
|
||||
..NodeProps::default()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user