9145b2a726fb8bcce910b0110b32a8fb6274fbf5
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
91c4dedcb0 |
host/observer: derive pipewire-pulse's PID from comm, not from repetition
MEASURED DEFECT, found by the §5.1 row-1 matrix run: the daemon PID was unresolvable on this host, permanently, which switched key 4's suppression off and fused every Pulse-emulated node into a single owner. Row 1's clean control forwarder was excluded, Firefox was excluded across an application.process.id bridge, and the taint reached the sunshine sinks and both sound cards -- the same machine-wide over-exclusion cascade as phase 5's F2, from a new cause. Stage 1 returned the one sec_pid shared by 2+ Clients, reasoning that only the Pulse shim repeats a value. WirePlumber repeats one too: it holds Clients 'WirePlumber' and 'WirePlumber [export]', both sec_pid 1747. Two values repeated, the rule called that ambiguous and returned None, and owner::keys_of's documented fail-closed asymmetry did the rest. The rule was wrong in both directions, so the prefilter is gone rather than patched: a second process holding two Clients defeats it (permanent, not a corner case), and a session where pipewire-pulse holds exactly one Client never repeats anything so the candidate is missed. comm was always the authoritative check; repetition stood in front of it and was a guess about other processes' Client counts. Now: candidates() lists every distinct sec_pid, resolve() picks the unique one whose /proc comm is exactly pipewire-pulse, and several matches still fail closed (a single Option<u32> cannot suppress two daemons -- recorded, not approximated). The adapter probes only PIDs entering the set, and retain_probed_comms bounds the map to live PIDs so a PID that leaves and returns is re-probed instead of answered from a stale comm. Row 1 now passes its exact partition, key named: tainted forwarder leg excluded on node.link-group, clean forwarder leg and Firefox eligible, taint confined to the tainted half. 225 tests green (WirePlumber-pair and single-Client regressions covered), clippy clean. |
||
|
|
471b8221ff |
host/observer: address the Codex phase-3r review (2 fixes, both verified)
Codex's adversarial review of the pure core found no *certain* P1. Two findings taken, both mutation-verified (the fix reverted, the intended test dies, nothing else moves): **F2, certain, P2 — `device_props` tested the wrong kind of ambiguity.** It required exactly one live *Device* on the claimed id rather than exactly one live *global*. With `[Device, Port]` on one id — a missed removal, the same precondition as every other recycled-id hazard — it kept answering from the older Device, so a node claiming that id held a stale `session_device = true`. That flag strips the node's owner keys and its fail-closed backstop, so a forwarder wearing it can put its output leg back on the eligible side. Now: one slot total, and it must be the Device. **F3, worth checking, P3 — `device.api` was corroborating by presence.** `device.api=v4l2` under `factory.name=api.alsa.pcm.sink` satisfied the positive classifier. No truthful configuration produces that pair, which is the argument for reading it as an observation gone wrong rather than as corroboration. The API must now equal the one the factory allowlist is written for, an empty value is not a value, and the two sides disagreeing fails closed. Tied to the allowlist being ALSA-only via a named constant. Two findings NOT fixed here, both pre-existing and neither introduced by round 8 — raised to the design doc instead: - **P1, worth checking: hardware playback-to-capture paths** (Stereo Mix, Digital Loopback) on a card whose driver is an ordinary `snd_hda_intel`. Both its sink and source classify `session_device`, taint cannot cross the hardware hop, and a capture app reading that source can re-emit the call. This is `snd_aloop` again in a form the driver name cannot detect; distinguishing it needs ALSA control inspection, which is a design change and a new I/O surface, not a local fix. - **P3: the 2 s readiness budget** can in principle never see an obligation-free instant under sustained startup churn, and `TimedOut` is sticky by design, so the process would be silent for its lifetime. Measured here: readiness at ~3 ms with 19 binds, so the margin is three orders of magnitude — but it wants a calibration argument, not a guess. 197 unit + 3 live green, clippy -D warnings and fmt clean. |
||
|
|
b3d71724ae |
host/observer: phase 3r pure core — node/device props come from a bind
v3.5 §6.7. The registry `global` event announces only a fixed 13-key subset
of a Node's properties, and eight the engine depends on are never among them
(phase-5 gate failure F1/F2). The core now treats the global as an index and
takes every property from the object's bound `info`.
- `RegEvent::NodeAdded { serial, id }` is identity only; `RegEvent::NodeInfo`
carries the properties and is both the first resolution and every later
PROPS change for the node's lifetime (decision 2). Same split for Device
(`DeviceAdded` / `DeviceInfo`).
- A node with no `info` is withheld from the snapshot and is a readiness
obligation; an unresolvable bind ends in sticky `TimedOut`, fail closed
(decision 3). Devices are keyed by serial too, so a recycled device id with
two live claimants is ambiguous ⇒ withheld rather than guessed.
- One live-node map replaces the admitted/withheld pair; classification is
recomputed at projection time from current inputs, since both sides of it
now change over an object's lifetime.
- `classify` takes the bound Device's props: presence is a union with the
Device winning (this recovers a real card whose node was never given
`alsa.driver_name` — the phase-3 review's owed fix), while the
non-terminal-driver denylist is a union in the safe direction.
- `apply` returns `Outcome`, the only sound place to enforce the suppression
rule: a property update is dropped only when model state provably did not
change, i.e. the resulting projection is identical.
Adapter: stops reading properties off Node/Device globals and emits the new
index events. Binding every Node and Device — the I/O half — is the next
commit (Codex's), so until then every node is withheld and readiness times
out by design.
Tests: 55 observer (was 38) — the prop-update matrix, readiness with node
binds, and recycled-Node-id churn (phase 3r gate rows 2–4). 195 green,
clippy -D warnings and fmt clean.
|
||
|
|
f90bee63f7 |
host/observer: close the snd_aloop absent-driver leak (Codex re-review)
Codex's re-review of the phase-3 fixes confirmed finding 1/5/6 closed but found the finding-2 fix incomplete: the denylist only rejected a *present* snd_aloop driver, so an snd_aloop node whose alsa.driver_name was not copied onto the node still classified session_device=true — the original leak. The absence is reachable: PipeWire >=1.2.6 stopped overwriting node props with card props, and WirePlumber only began copying alsa.* onto nodes in 0.5.13. Fix: session_device now requires a PRESENT, non-denied ALSA driver; a missing alsa.driver_name fails closed to NotSessionDevice (a real card without the prop is over-excluded — safe; recovering it needs reading the driver from the backing Device global, owed to a later round). Mutation-verified: reverting to fail-open on absence is killed by classify_alsa_without_driver_name_fails_closed. Also: corrected the finding-3 limitation doc to cite PipeWire's object.serial identity contract rather than overclaiming the live gate proves it (Codex P3, non-blocking). 121 unit + live gate row 6 green, clippy clean, observer files fmt-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
557c1030a7 |
host/observer: address Codex phase-3 review (2 P1 + P3s)
Cross-review round: Codex adversarially reviewed my pure core, found two merge-blocking P1s and several P3s. Triaged each for reachability; fixes below, each mutation-verified (revert killed by its intended test). P1 finding 1 — graph_ready was sticky-once-Complete, so a Link added post-enumeration whose endpoints are still binding (an INVISIBLE edge, absent from the snapshot) left graph_ready=true and a candidate could be reported eligible over unseen tainted ancestry. graph_ready is now dynamic: Complete AND no outstanding obligations. Readiness::Complete stays sticky as the epoch marker. New regression test + flipped the old sticky-churn test. P1 finding 2 — snd_aloop presents with an allowlisted ALSA factory and device.api=alsa exactly like a real card but forwards audio through a kernel hop the Link graph cannot see; it was classified session_device=true, dropping its owner keys + backstop (leak). Added alsa.driver_name to DeviceClaim and a NON_TERMINAL_ALSA_DRIVERS denylist under the factory allowlist; adapter now populates it. Negative fixture added. P3 finding 5 — the BlueZ allowlist entries (api.bluez5.pcm.*) were invented; removed them (real names are api.bluez5.media.*). A BT sink now over-excludes (safe) pending a measured fixture. P3 finding 6 — strengthened the timeout test to assert TimedOut stays sticky through later DeviceAdded/sync/tick. Findings 3 (dropped-link unrepresented) and 4 (missed-removal generation ambiguity) documented as accepted low-reachability limitations (links carry object.serial — confirmed by the live gate; registry does not drop removals). Codex confirmed the pulse-PID matrix fails safe and the adapter add() FIFO is lockstep. 120 unit + live gate row 6 green, clippy + fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8206864a43 |
host/observer: phase 3 pure core — RegistryModel reducer + classifiers
My half of the phase-3 split (impl plan §4). Pure, no PipeWire: the adapter (Codex's half) translates live registry callbacks / binds / /proc reads / core.sync into RegEvents and feeds this reducer. - RegistryModel::apply folds RegEvents into serial-keyed maps with an insertion-ordered id index so global_remove accounts for the oldest generation first; recycled ids stay Ambiguous until accounted (v3.4 §6.1.3). - Readiness epoch: graph_ready false until ServerSynced + no outstanding obligations (withheld nodes, pending link binds); bounded timeout fails closed. Gates sticky retirement only; sticky once terminal. - session_device classifier: hardware-PCM factory allowlist, exact match, fail closed to false; a node on an unresolved Device is withheld, never admitted provisional. - pulse-PID derivation split into pure candidate (repeated sec_pid) + validate (/proc comm), so the 6-case failure matrix is unit-testable; any failure => None (key 4 unusable). 34 tests cover 5 of 6 exit-gate rows (the live topology-diff row is the adapter's). cargo test --bins 117 green, fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |