diff --git a/src/host/taint/owner.rs b/src/host/taint/owner.rs index 7e71fd4..ac7d0eb 100644 --- a/src/host/taint/owner.rs +++ b/src/host/taint/owner.rs @@ -256,6 +256,50 @@ fn keys_of(node: &NodeSnapshot, ctx: &OwnerCtx) -> Vec<(OwnerKey, KeyValue)> { /// nothing else relates them. Its sibling output leg cannot be found, so /// the engine must fail closed rather than declare it clean /// (v3.4 §6.1.1, final paragraph). +/// +/// # 🔴 OPEN, phase-6 blocking — the key union can *reduce* taint here +/// +/// **Round 11 review, finding 1. Verified correct; deliberately not fixed in +/// that round.** Round 10 made key 4 a union of the node's +/// `application.process.id` and its Client's `pipewire.sec.pid`, and the claim +/// that this was "strictly additive" was too strong: the same key list also +/// feeds *this* predicate, so adding a value can move a node from unbounded to +/// bounded, and `propagate_unresolved_owner`'s global sweep is triggered by an +/// **un**bounded tainted reader. Concretely: +/// +/// 1. A tainted reader's node claims the pipewire-pulse PID while its Client +/// holds a real protected PID `A`. Under `or_else` the node's value won and +/// exception 1 suppressed it, leaving the reader unbounded; under the union +/// it is bounded by `A`. +/// 2. Its process's output leg uses a second Client whose id is **ambiguous** +/// (the observer missed a removal), so no protected PID is available — but +/// the leg claims a bogus `application.process.id` `B`, which bounds it. +/// 3. Neither the bridge nor the sweep fires, and the output stays eligible +/// while re-emitting the call. +/// +/// It cannot leak today: `evaluate()` is reached only by the dry-run audit, +/// which creates no links. It becomes live when phase 6 consumes eligibility. +/// +/// **Why it is not fixed yet.** The principled repair is provenance: a +/// self-claimed `application.process.id` is not a *sound* bound, only the +/// protected keys are. But applying that bluntly makes every Pulse-emulated +/// app unbounded — their Client's `sec_pid` is the daemon's and suppressed, so +/// the node's own claim is their only per-app identity — which re-triggers the +/// §6.1.1 mass over-exclusion the whole design is built to avoid, and would +/// make the eligible half of the §5.1 matrix empty. +/// +/// The targeted rule that closes the path above without that cost: **a node +/// whose Client cannot be resolved at all must not be bounded by its own +/// self-claimed PID.** An ambiguous Client already means "we do not know who +/// owns this", and a self-claim must not paper over it; a Pulse app's Client +/// *is* resolved (to the daemon's PID, then suppressed), so it keeps its +/// bound. Implementing it needs `OwnerCtx` to distinguish "resolved" from +/// "absent", and `OwnerKeyIndex` to carry boundedness separately from the key +/// set, since bridging must keep using the full union. +/// +/// ⚠️ Do this **with the §5.1 matrix data in hand**, not before: the whole +/// question is how much over-exclusion the rule actually causes on a real +/// graph, and that is measurable rather than arguable. pub fn owner_is_bounded(node: &NodeSnapshot, ctx: &OwnerCtx) -> bool { keys_of(node, ctx) .iter()