host/taint: concede the unbounded-reader rule; pin link-group fingerprints (Codex round 4)
Round 4 adjudicated my three round-3 pushbacks. Codex ruled: F2 bool seam sufficient (YES), F3 accepted as a phase-3 contract not a phase-2 blocker (YES) — but my F1 narrowing was unsound (NO), with a clean counterexample. F1 (conceded): I had narrowed "unbounded tainted reader ⇒ exclude every output" to spare outputs carrying a real, non-daemon PID, arguing an unbounded reader must be daemon-owned. Codex refuted it: `application.process.id` is optional and client-controlled, so one real process can present NO pid on its reading leg (unbounded) and a real pid on its output leg — the narrowing spares that output and leaks the call. App properties cannot carry a soundness argument; only `pipewire.*` has protected identity. Reverted to the broad rule: an unbounded tainted reader excludes the whole candidate universe. Added the exact counterexample as a test (`a_real_app_with_no_pid_on_its_reader_leg...`) and kept a bounded-reader test to show the round-1 blast-radius guarantee still holds for the bounded tier. F2 (doc corrected): removed the "a mis-classified filter is still braced" claim — Codex showed a filter with no shared strong key, wrongly marked `session_device`, cannot trip the backstop from its reading leg and leaks through a differently-keyed output. A false positive is now documented as leak-capable; the only defence is the correct positive classifier. F3 (link-group fingerprint, pinned): a mutation dropping LinkGroup fingerprints survived all 53 tests, because the strong-key fingerprint test used pulse.module.id. Added a link-group new-connection test. Mutation-verified 2/2. 55 tests. Phase-2 open item is now only F3-as-phase-3-contract, which Codex accepted is not a phase-2 blocker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-30
@@ -361,8 +361,7 @@ pub fn evaluate(
|
||||
let mut changed = false;
|
||||
changed |= propagate_links(&edges.edges, &mut taint);
|
||||
changed |= propagate_owner_bridge(&keys, &components, &edges, &mut taint);
|
||||
changed |=
|
||||
propagate_unresolved_owner(snapshot, ctx.pipewire_pulse_pid, &keys, &edges, &mut taint);
|
||||
changed |= propagate_unresolved_owner(snapshot, &keys, &edges, &mut taint);
|
||||
if !changed {
|
||||
break;
|
||||
}
|
||||
@@ -678,25 +677,25 @@ fn propagate_owner_bridge(
|
||||
/// owner and stays eligible; only unbounded output legs are its possible
|
||||
/// siblings. → exclude unbounded outputs.
|
||||
/// - An *unbounded* tainted reader has nothing that identifies its owner, so
|
||||
/// its re-emitting leg could carry a strong key we cannot match back to
|
||||
/// it. But it cannot be *anything*: a reader with no usable owner key is
|
||||
/// necessarily **daemon-owned** — a real application has its own PID,
|
||||
/// which is a usable key, so it would be bounded. Its sibling is therefore
|
||||
/// another daemon-owned output, never an ordinary app. → also exclude the
|
||||
/// daemon-owned outputs (bounded by a strong key or not); leave outputs
|
||||
/// carrying a real, non-daemon PID eligible, because a real app is
|
||||
/// provably a different owner from a daemon module leg.
|
||||
/// its re-emitting leg could be **any** output on the box, and no property
|
||||
/// on an output leg can prove it is unrelated. → exclude every output
|
||||
/// candidate.
|
||||
///
|
||||
/// ⚠️ This is deliberately **narrower than Codex round 3's suggested
|
||||
/// "exclude every output"**, which would make an ordinary app unshareable
|
||||
/// whenever any keyless module forwarder reads the call — losing the
|
||||
/// round-1 "blast radius stays small" guarantee for no safety gain, since
|
||||
/// a real app cannot be the sibling of a daemon leg. When the daemon PID
|
||||
/// is *unknown* the distinction collapses (we cannot tell a real app from
|
||||
/// a module leg) and the rule degrades to Codex's: exclude everything.
|
||||
/// ⚠️ I tried to narrow this to "daemon-owned outputs only", on the
|
||||
/// theory that an unbounded reader must be daemon-owned (a real app has a
|
||||
/// PID, which would bound it) so a real-PID output is provably a different
|
||||
/// owner. **Codex refuted it (round 4):** `application.process.id` is
|
||||
/// optional and client-controlled, so a real process can present *no* PID
|
||||
/// on its reading leg (unbounded) and a real PID on its output leg — one
|
||||
/// owner, spared by the narrowing, leaking the call. Only `pipewire.*`
|
||||
/// properties have protected identity; app properties cannot carry a
|
||||
/// soundness argument. So: exclude everything. The trigger is genuinely
|
||||
/// anomalous — a keyless reader actively consuming the call; EasyEffects
|
||||
/// and loopbacks carry a `node.link-group` and are *bounded*, so they do
|
||||
/// not trip this tier — and phase 5's dry run surfaces it before it can
|
||||
/// gate anything real.
|
||||
fn propagate_unresolved_owner(
|
||||
snapshot: &GraphSnapshot,
|
||||
ctx_pulse_pid: Option<u32>,
|
||||
keys: &owner::OwnerKeyIndex,
|
||||
edges: &Edges,
|
||||
taint: &mut BTreeMap<Serial, Reason>,
|
||||
@@ -717,18 +716,9 @@ fn propagate_unresolved_owner(
|
||||
}
|
||||
let mut changed = false;
|
||||
for node in snapshot.nodes() {
|
||||
if node.role != MediaRole::StreamOutput {
|
||||
continue;
|
||||
}
|
||||
// A real, non-daemon PID proves the node is an ordinary app, not a
|
||||
// module leg — the one thing an unbounded daemon reader's sibling
|
||||
// cannot be. Unknown daemon PID ⇒ cannot prove it ⇒ swept in.
|
||||
let is_real_app = matches!(
|
||||
(node.props.process_id, ctx_pulse_pid),
|
||||
(Some(pid), Some(daemon)) if pid != daemon
|
||||
);
|
||||
let swept_by_unbounded = has_unbounded_tainted_reader && !is_real_app;
|
||||
if !keys.is_bounded(node.serial) || swept_by_unbounded {
|
||||
if node.role == MediaRole::StreamOutput
|
||||
&& (has_unbounded_tainted_reader || !keys.is_bounded(node.serial))
|
||||
{
|
||||
changed |= raise(taint, node.serial, Reason::UnresolvedOwner);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user