diff --git a/docs/screenshare-audio-exclusion-impl-plan.md b/docs/screenshare-audio-exclusion-impl-plan.md index 6da13ae..5d0bc3f 100644 --- a/docs/screenshare-audio-exclusion-impl-plan.md +++ b/docs/screenshare-audio-exclusion-impl-plan.md @@ -472,9 +472,10 @@ observable in Phase 5 before they gate anything real. > **The eligible half of row 1 is the only thing that exposed it** — the verdict was > fail-closed and silent. > -> ⚠️ **Phase 6 is NOT unblocked by this file alone.** F11-1 is a separate open gate (see §4's -> owner-union note and `owner_is_bounded`'s doc), as are phases 0b/0c/0d and the "Stereo Mix" -> design call. +> ⚠️ **Phase 6 is NOT unblocked by this file alone.** F11-1 was the other gate and is now +> **closed** (2026-07-26, pixelpass `c78eb2d`: key 4 bounds an owner only when the node's +> Client resolves; measured cost on the live graph, zero — see the results file). Phases +> 0b/0c/0d and the "Stereo Mix" design call still precede phase 6. > > Two things to keep when re-running: **every partition row must run with `AEC=off`** (a > configured-but-unvalidated AEC shuts the fan-out gate and empties the eligible half of every diff --git a/docs/screenshare-audio-exclusion-phase5-results.md b/docs/screenshare-audio-exclusion-phase5-results.md index 03894c8..149913d 100644 --- a/docs/screenshare-audio-exclusion-phase5-results.md +++ b/docs/screenshare-audio-exclusion-phase5-results.md @@ -4,8 +4,8 @@ eligible half of every row is non-empty. O5 re-measured on the fixed graph and stays closed.** One new defect was found and fixed during the run (F13-1); three findings are recorded as non-blocking, and three rows carry recorded -substitutions. Phase 6 is unblocked **by this file**, but see "What still blocks -phase 6" — F11-1 is a separate gate and is still open. +substitutions. Phase 6 is unblocked **by this file**, and F11-1 — the other gate — +was closed with this data on 2026-07-26 (see "What still blocks phase 6"). - **Run date:** 2026-07-26 (run 1: 2026-07-25, gate FAILED — see history below) - **Host:** `cazen` — PipeWire 1.6.8, WirePlumber 0.5.15, CachyOS @@ -332,24 +332,41 @@ lesson: the row could not have been asserted from any previous build's output. ## What still blocks phase 6 -This file passing removes **one** of the two gates. Still outstanding: +This file passing removes **one** of the two gates. F11-1, the other, is now +closed. Still outstanding: -1. **🔴 F11-1 — the owner-key union can *reduce* taint.** Unchanged and still - open; it was to be decided *with* this matrix data. The data now exists: the - suppression path is live and correct (F13-1 fixed), and rows 2/9 show key 4 - doing real work, which means the interaction F11-1 describes is reachable in - principle. The rule to implement is written in `owner_is_bounded`'s doc in - pixelpass `src/host/taint/owner.rs`, sharpened by Codex round 12: "resolved" - must mean **an unambiguous Client yielding `Some(pipewire.sec.pid)` before - pipewire-pulse suppression**. A five-case Client matrix (absent · ambiguous · - unique-but-pid-less · resolved-native · resolved-to-pipewire-pulse) is owed. -2. **Hardware playback-to-capture paths ("Stereo Mix")** defeat `session_device` +1. **Hardware playback-to-capture paths ("Stereo Mix")** defeat `session_device` and are a real echo path — needs ALSA control inspection; user design call owed. -3. **Phases 0b / 0c / 0d** are untouched and all precede phase 6. -4. **The readiness budget calibration argument** (above). -5. **Owed samples:** a real third-party forwarder (row 1b), EasyEffects (row 8), +2. **Phases 0b / 0c / 0d** are untouched and all precede phase 6. +3. **The readiness budget calibration argument** (above). +4. **Owed samples:** a real third-party forwarder (row 1b), EasyEffects (row 8), a real `Audio/Duplex` device (row 13). +### ✅ F11-1 — closed 2026-07-26, with this matrix's data + +The rule now implemented (pixelpass `c78eb2d`, §6.1.2's round-13 box): **key 4 bounds an +owner only when the node's Client resolves** — an unambiguous Client yielding +`Some(pipewire.sec.pid)`, read *before* pipewire-pulse suppression — so a node can no +longer bound itself, and escape `propagate_unresolved_owner`'s sweep, with an +`application.process.id` it invented. Bridging still uses the full union. + +Codex's round-12 sharpening was the decisive part: "resolved" must mean a `sec_pid`, not +"a unique Client object exists", and the **unique-but-pid-less** row is the only one that +tells the two apart. All five Client cases are unit tests (absent · ambiguous · +unique-but-pid-less · resolved-native · resolved-to-pipewire-pulse), plus the recorded +three-step leak path end to end. Mutation-verified: dropping the provenance test fails +four of the six rows and leaves the two no-over-exclusion rows green. + +**The cost question the deferral was waiting on, measured on this host:** the before- and +after-binaries audited the *same* live graph simultaneously (both are read-only observers) +— tagged producer into the default sink, `parec` on its monitor as a live tainted reader +so the sweep was genuinely armed, Firefox + `aplay` + `pacat` as bystanders. **181 records +each, the same 14 distinct decision states, none exclusive to either side, no +`unresolved-owner` on either, eligible half non-empty throughout.** O5 unmoved (identical +p50 15 µs and busy fraction 0.0012). Every real app here is native or Pulse-emulated and +**both resolve**; sweeping all 18 live nodes, the only unresolved-Client ones were +`Dummy-Driver` and `Freewheel-Driver`, which carry no pid key to lose. + --- ## Reproducing this run diff --git a/docs/screenshare-audio-exclusion-plan.md b/docs/screenshare-audio-exclusion-plan.md index 87918fb..7d863ba 100644 --- a/docs/screenshare-audio-exclusion-plan.md +++ b/docs/screenshare-audio-exclusion-plan.md @@ -581,6 +581,30 @@ Where an owner has a tainted input leg and an output leg with **no** resolvable (§6.1.2), fail closed and exclude the output leg. This only ever engages for owners actually reading a tainted monitor, so the blast radius is small. +> **⚠️ Round 13 (F11-1) — "bounded" is not "has a key". A self-claimed pid is not +> provenance.** Which legs this backstop sweeps depends on whether the tainted reader and +> the candidate outputs are *bounded* — i.e. whether we could enumerate their sibling legs +> and be right. Key 4 is a union of the node's `application.process.id` (client-controlled, +> optional) and its Client's `pipewire.sec.pid` (protected), so a node could bound itself +> with a value it invented and escape the sweep while its real sibling was unfindable. +> +> **Rule:** a strong key (`node.link-group`, `pulse.module.id`) bounds an owner on its own; +> key 4 bounds an owner **only when the node's Client resolves** — an unambiguous Client +> yielding `Some(pipewire.sec.pid)`, read *before* pipewire-pulse suppression. The +> ordering is load-bearing in both directions: read after suppression and every +> Pulse-emulated app on the box goes unbounded (§6.1.1 catastrophe, new door); accept "a +> unique Client object exists" instead of a `sec_pid` and a pid-less Client leaves the hole +> open. +> +> **Bridging is unchanged** — it still uses the full union, because a self-claimed pid is +> perfectly good *evidence that two legs are related*, which is the taint-increasing +> direction. Only the permission to declare a differently-keyed output "provably someone +> else" now demands a `pipewire.*` answer to "who is this". +> +> Measured cost on this host: **zero** — before/after binaries audited the same live graph +> simultaneously, same 14 decision states, no `unresolved-owner` on either side, eligible +> half non-empty. Implemented in pixelpass `c78eb2d`; five-case Client matrix in the tests. + ### 6.1.3 ⚠️ Taint must be STICKY — current topology is not enough **Conceded to Codex in round 6; my "conditional bridge" was correct about topology and @@ -1198,7 +1222,7 @@ requirement to assert what must remain **eligible**: §6.1.2's pulse-PID derivat | §5.1 matrix | **PASSED** — 13/13, incl. the full sticky lifecycle (row 10) and provable identifier recycling (row 11) | | O5 | **closed on the real graph** — worst recompute 67 µs, churn mean 10 µs, busy fraction 0.0006 | | Phase 5 | **machinery unchanged and correct** — three real defects caught on first contact with the live graph, none of them in the engine | -| Phase 6 | **still blocked** — by F11-1, phases 0b/0c/0d, and the "Stereo Mix" design call, *not* by this matrix | +| Phase 6 | **still blocked** — by F11-1, phases 0b/0c/0d, and the "Stereo Mix" design call, *not* by this matrix. (F11-1 was **closed later the same day** with this matrix's data — §6.1.2's round-13 box; the rest stand.) | Three rows passed with recorded substitutions (8 EasyEffects, 9 Firefox's own mic/monitor paths, 13 a real `Audio/Duplex` device) and the third-party samples stay owed.