host/taint: pin what "resolved" must mean before F11-1 is implemented

Round 12 re-examined the deferral and agreed it holds while evaluate()
is audit-only, and that the recorded rule closes the path without
unbounding Pulse-emulated apps — but only under one reading of
"a node whose Client cannot be resolved at all".

The trap is worth writing down before anyone implements it: reading
"resolved" as "a unique Client object exists" passes for a unique Client
with sec_pid = None, which supplies no protected identity and leaves
exactly the self-claimed-PID hole the rule exists to close. It has to
mean an unambiguous Client yielding Some(pipewire.sec.pid), taken before
pipewire-pulse suppression.

That also means the §5.1 matrix needs five Client cases rather than two:
absent, ambiguous, unique-but-pid-less, resolved-native, and
resolved-to-pipewire-pulse. The pid-less row is the one that
distinguishes the two readings and the one a two-case matrix skips
without saying so.

Docs only. Still deferred, still to be decided with matrix data in hand.
220 tests green, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 02:01:07 -04:00
co-authored by Claude Opus 5
parent 289016d071
commit b8b8b78b09
+22
View File
@@ -300,6 +300,28 @@ fn keys_of(node: &NodeSnapshot, ctx: &OwnerCtx) -> Vec<(OwnerKey, KeyValue)> {
/// ⚠️ Do this **with the §5.1 matrix data in hand**, not before: the whole /// ⚠️ 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 /// question is how much over-exclusion the rule actually causes on a real
/// graph, and that is measurable rather than arguable. /// graph, and that is measurable rather than arguable.
///
/// ## Round 12 — the deferral holds, and "resolved" has a trap in it
///
/// Codex re-examined this and agreed the deferral is defensible while
/// `evaluate()` is audit-only, and that the rule above closes the recorded path
/// without unbounding normal Pulse-emulated apps — **but only under one
/// reading of "resolves"**, and the wrong reading reintroduces the hole:
///
/// - ✅ "Resolved" must mean **an unambiguous Client that yields
/// `Some(pipewire.sec.pid)`**, taken *before* the pipewire-pulse suppression
/// step. A Pulse app then still has the daemon's protected PID as
/// provenance, even though that value is omitted from the bridge keys, so it
/// stays bounded and the eligible half survives.
/// - ❌ **Do not** implement it as "a unique Client object exists". A unique
/// Client with `sec_pid = None` would satisfy that test while providing no
/// protected identity at all, leaving exactly the self-claimed-PID hole this
/// rule is meant to close.
///
/// So the matrix needs five Client cases, not two: **absent**, **ambiguous**,
/// **unique but pid-less**, **resolved-native**, and
/// **resolved-to-pipewire-pulse**. The third is the one that distinguishes the
/// two readings, and it is the row a two-case matrix would silently skip.
pub fn owner_is_bounded(node: &NodeSnapshot, ctx: &OwnerCtx) -> bool { pub fn owner_is_bounded(node: &NodeSnapshot, ctx: &OwnerCtx) -> bool {
keys_of(node, ctx) keys_of(node, ctx)
.iter() .iter()