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>
This commit is contained in:
2026-07-24 13:40:47 -04:00
co-authored by Claude Opus 4.8
parent 557c1030a7
commit f90bee63f7
3 changed files with 49 additions and 13 deletions
+22 -9
View File
@@ -61,9 +61,10 @@ const HARDWARE_PCM_FACTORIES: &[&str] = &[
/// would let tainted audio loop back untainted (Codex phase-3 review,
/// finding 2). `factory.name` alone cannot distinguish these from a real
/// card — `snd_aloop` presents as `api.alsa.pcm.{sink,source}` exactly like
/// `snd_hda_intel` — so this is a necessary denylist layered under the
/// factory allowlist, keyed on the one property that does distinguish them
/// (`alsa.driver_name`, measured present on real ALSA nodes).
/// `snd_hda_intel` — so a real ALSA terminal must present an `alsa.driver_name`
/// that is **present and not on this denylist**; a missing driver fails closed
/// (see [`classify`]). `snd_dummy` is intentionally absent: it is virtual but
/// does not couple playback to capture, so it is not a loopback hazard.
const NON_TERMINAL_ALSA_DRIVERS: &[&str] = &["snd_aloop"];
/// The three node properties the classifier reads, exactly as the adapter
@@ -88,8 +89,11 @@ pub struct DeviceClaim {
/// `alsa.driver_name` — the kernel driver behind an ALSA node (e.g.
/// `snd_hda_intel`, `snd_usb_audio`, `snd_aloop`). Needed because the
/// factory allowlist cannot tell a real card from a loopback driver that
/// shares the same factory; a driver on [`NON_TERMINAL_ALSA_DRIVERS`]
/// forfeits `session_device`. Absent on non-ALSA backends.
/// shares the same factory. `session_device` requires this to be
/// **present and not** on [`NON_TERMINAL_ALSA_DRIVERS`]; a driver on the
/// denylist, or an absent value, both fail closed (see [`classify`]).
/// May be absent on non-ALSA backends or on version pairings that do not
/// copy `alsa.*` onto the node.
pub alsa_driver_name: Option<String>,
}
@@ -132,12 +136,21 @@ pub fn classify(claim: &DeviceClaim, device_resolved: bool) -> Classification {
.factory_name
.as_deref()
.is_some_and(|f| HARDWARE_PCM_FACTORIES.contains(&f));
let non_terminal_driver = claim
// A **present, non-denied** ALSA driver is required — absence fails closed
// (Codex phase-3 re-review). `alsa.driver_name` is not copied onto the
// node on every PipeWire/WirePlumber version pairing (PipeWire ≥1.2.6
// stopped overwriting node props with card props; WirePlumber only began
// copying `alsa.*` onto nodes in 0.5.13), so a *missing* value must not be
// read as "not a loopback" — that is exactly the hole an `snd_aloop` node
// without the property would slip through. A real card whose node lacks
// the driver is instead over-excluded (keeps its owner keys — safe);
// recovering `session_device` for it needs reading the driver from the
// backing Device global, which is owed to a later round.
let driver_ok = claim
.alsa_driver_name
.as_deref()
.is_some_and(|d| NON_TERMINAL_ALSA_DRIVERS.contains(&d));
let is_hardware_pcm =
claim.device_api.is_some() && on_factory_allowlist && !non_terminal_driver;
.is_some_and(|d| !NON_TERMINAL_ALSA_DRIVERS.contains(&d));
let is_hardware_pcm = claim.device_api.is_some() && on_factory_allowlist && driver_ok;
if is_hardware_pcm {
Classification::SessionDevice
} else {
+7 -4
View File
@@ -31,10 +31,13 @@
//! The adapter drops such a global before it reaches [`RegistryModel`], so
//! readiness can reach `Complete` while permanently omitting that Link — an
//! invisible edge that could hide tainted ancestry. **Not reachable in
//! practice:** every real Link global carries `object.serial` (confirmed by
//! the live gate, which only counts links the strict parser admits). A full
//! fix needs a pure "required-observation-failed" token that holds readiness
//! false; deferred rather than built for a case that does not occur.
//! practice:** PipeWire's native protocol defines `object.serial` as the
//! unique identity every global carries, so a Link without one requires a
//! protocol/server failure, not ordinary churn. (The live gate is
//! consistent with this but does not *prove* it — it only counts Links the
//! strict parser already admitted.) A full fix needs a pure
//! "required-observation-failed" token that holds readiness false; deferred
//! rather than built for a case that does not occur.
//! - *Removal generation ordering assumes no removal is silently lost.* On a
//! recycled id with two live claimants, [`Self::on_removed`] retires the
//! oldest generation first; if the *first* generation's removal was never
+20
View File
@@ -142,6 +142,26 @@ fn classify_invented_bluez_factories_are_not_session_devices() {
}
}
#[test]
fn classify_alsa_without_driver_name_fails_closed() {
// Codex re-review: a missing `alsa.driver_name` must NOT grant
// session_device — an snd_aloop node whose driver prop was not copied onto
// the node would otherwise slip through. Absence fails closed.
for factory in ["api.alsa.pcm.sink", "api.alsa.pcm.source"] {
let claim = DeviceClaim {
device_id: Some(gid(7)),
device_api: Some("alsa".to_string()),
factory_name: Some(factory.to_string()),
alsa_driver_name: None,
};
assert_eq!(
classify(&claim, true),
Classification::NotSessionDevice,
"absent driver on {factory} must fail closed"
);
}
}
#[test]
fn classify_snd_aloop_is_not_a_session_device() {
// Finding 2: an ALSA loopback presents with an allowlisted factory and