{"repro_id":"REPRO-2026-00309","version":6,"title":"PipeWire sandbox escape via malicious library loading in PulseAudio compatibility layer","repro_type":"security","status":"published","severity":"high","description":"CVE-2026-5674 is a Linux sandbox escape in PipeWire. Public advisories describe an attacker with minimal permissions inside a sandboxed environment, such as Flatpak, loading a malicious library through PipeWire's PulseAudio compatibility layer to escape the sandbox and execute code outside it.","root_cause":"# Root Cause Analysis — CVE-2026-5674 (PipeWire PulseAudio-compatibility sandbox escape)\n\n## Summary\nPipeWire's PulseAudio compatibility daemon (`pipewire-pulse`) implements the PulseAudio\nnative protocol `LOAD_MODULE` command. The request handler only checks the\n`pulse.allow-module-loading` server property (default: `true`) and then loads any of the\nbuilt-in PulseAudio compatibility modules with fully attacker-controlled arguments. One of\nthose modules, `module-ladspa-sink`, forwards the attacker-supplied `plugin=` argument to\nPipeWire's filter-chain LADSPA loader, which calls `dlopen()` on the value verbatim when it\nis an absolute path. Because Flatpak-style sandboxes deliberately expose the PulseAudio\nunix socket to sandboxed applications, an attacker confined in a bubblewrap/Flatpak sandbox\ncan make the out-of-sandbox `pipewire-pulse` daemon `dlopen()` an attacker-controlled shared\nlibrary, executing its ELF constructors in the daemon process and thereby escaping the\nsandbox.\n\n## Impact\n- Package/component affected: `pipewire` / `pipewire-pulse`\n  (`libpipewire-module-protocol-pulse`, pulse `module-ladspa-sink`/`module-ladspa-source`,\n  SPA filter-graph LADSPA plugin).\n- Affected versions: verified on PipeWire 1.6.2 (Ubuntu 26.04, `1.6.2-1ubuntu1.1`). The\n  Debian security tracker lists every release as vulnerable (bullseye 0.3.19 through\n  sid 1.6.8); no upstream fix exists at the time of this run.\n- Risk level and consequences: Important (CVSS 8.8 per Amazon ALAS). Any sandboxed\n  application with access to the PulseAudio socket (default for audio-playing Flatpaks)\n  can execute arbitrary code in the user's unsandboxed `pipewire-pulse` session daemon,\n  i.e. a full sandbox escape with the daemon's privileges.\n\n## Impact Parity\n- Disclosed/claimed maximum impact: sandbox escape — arbitrary code execution outside the\n  sandbox (bubblewrap/Flatpak-style namespace sandbox) via the PulseAudio compatibility\n  layer.\n- Reproduced impact from this run: attacker-controlled code executed in the host-side\n  `pipewire-pulse` daemon from a client confined in a real bubblewrap sandbox with separate\n  mount and user namespaces. Proof: the loaded library's constructor wrote fresh markers\n  (`CVE-2026-5674-PWNED pid=<daemon pid> uid=1000 comm=pipewire-pulse`) into a host-only\n  oracle directory that the sandboxed client could neither see nor write.\n- Parity: `full`.\n- Not demonstrated: nothing material — a constructor can contain arbitrary code; the marker\n  write is representative attacker-controlled code execution in the daemon.\n\n## Root Cause\n1. `src/modules/module-protocol-pulse/pulse-server.c` — `do_load_module()` (line 5051)\n   handles `COMMAND_LOAD_MODULE` from the PulseAudio native protocol. Its only guard is\n   `if (!impl->defs.allow_module_loading) return -EACCES;`. The property defaults to true\n   (`/usr/share/pipewire/pipewire-pulse.conf`: `#pulse.allow-module-loading = true`).\n2. `src/modules/module-protocol-pulse/modules/module-ladspa-sink.c` — the registered pulse\n   module `module-ladspa-sink` accepts `plugin=` and `label=` arguments and builds a\n   filter.graph node `{ type = ladspa plugin = \"<plugin>\" label = \"<label>\" }`, loaded via\n   `pw_context_load_module(..., \"libpipewire-module-filter-chain\", ...)` inside the\n   `pipewire-pulse` process.\n3. `spa/plugins/filter-graph/plugin_ladspa.c` — `load_ladspa_plugin()`: when the plugin\n   string starts with `/`, it is passed directly to `ladspa_handle_load_by_path()`, which\n   calls `dlopen(path, RTLD_NOW)` (line 196) with no allowlist, no confinement check, and\n   no consideration of whether the requesting client is sandboxed. `dlopen()` executes the\n   library's constructors, yielding arbitrary code execution in the daemon. (The subsequent\n   `dlsym(\"ladspa_descriptor\")` failure only rejects the module *after* attacker code has\n   already run.)\n- Fix commit: none upstream at the time of this run (Debian tracker: \"unfixed\"). The\n  disclosed mitigation is `pulse.allow-module-loading = false`, which makes\n  `do_load_module()` return `-EACCES` (\"Failure: Access denied\") — used as this run's\n  negative control.\n\n## Reproduction Steps\n1. `bundle/repro/reproduction_steps.sh` (self-contained; exit 0 = confirmed).\n2. What it does:\n   - Builds the attacker library `shared/evil.so` (constructor writes\n     `CVE-2026-5674-PWNED pid=... uid=... comm=...` into the host-only oracle dir; the\n     oracle path is delivered via the daemon's `ORACLE_PATH` environment variable).\n   - If user namespaces are blocked by the outer container seccomp (they are on this\n     worker: `unshare(CLONE_NEWUSER)` → EPERM even for root), it re-executes itself inside\n     a Docker container (`--security-opt seccomp=unconfined`, ubuntu:26.04 with\n     pipewire/pipewire-pulse/pulseaudio-utils/bubblewrap) as unprivileged uid 1000.\n   - Starts the real `pipewire` + `pipewire-pulse` daemons on the \"host\" side with a fresh\n     `XDG_RUNTIME_DIR`; health-checks via `pactl info`.\n   - Runs the attacker client inside `bwrap --unshare-user --unshare-ipc --unshare-uts\n     --unshare-cgroup` with only: the PulseAudio runtime dir, the writable shared dir\n     (containing `evil.so`), and read-only system binds. The host-only oracle directory is\n     not bound.\n   - The client proves it cannot see the oracle path and that a probe write never reaches\n     the host, then issues the real protocol request\n     `pactl load-module module-ladspa-sink sink_name=pwnsink plugin=<abs path> label=pwn`.\n   - Host verifies a marker file `marker_<daemon pid>.txt` whose pid equals the live\n     `pipewire-pulse` pid and whose comm is `pipewire-pulse`; namespace identity of client\n     vs daemon is recorded both from inside the sandbox and from the host.\n   - Two vulnerable attempts against fresh daemon processes, then two negative controls\n     with `pulse.allow-module-loading=false` (request denied, no marker).\n3. Expected evidence: markers `logs/marker_vuln_attempt{1,2}.txt`, client logs showing\n   `ORACLE_PATH_NOT_VISIBLE` + LOAD_MODULE, fixed attempts showing `Failure: Access\n   denied`, `RESULT: vuln_ok=2/2 fixed_ok=2/2`, exit 0.\n\n## Evidence\n- `bundle/logs/reproduction_steps.log` — full orchestration log; key lines:\n  - `marker present: CVE-2026-5674-PWNED pid=37 uid=1000 comm=pipewire-pulse`\n  - `marker pid/comm match live daemon (pid=37 comm=pipewire-pulse)`\n  - `client runs in a different user namespace (user=user:[4026533274] vs daemon user:[4026533197])`\n  - `RESULT: vuln_ok=2/2 fixed_ok=2/2` / `CVE-2026-5674 CONFIRMED` (two consecutive runs)\n- `bundle/logs/client_vuln_attempt{1,2}.log` — sandboxed client: oracle not visible, probe\n  write does not reach host, `pactl load-module module-ladspa-sink plugin=...evil.so`.\n- `bundle/logs/marker_vuln_attempt{1,2}.txt` — host-only markers with live daemon pid/comm.\n- `bundle/logs/client_fixed_attempt{1,2}.log` — `Failure: Access denied`;\n  `bundle/logs/negative_control_obs_fixed_attempt{1,2}.json` — structured observation.\n- `bundle/logs/ns_evidence_*.txt` / `ns_client_host_*.txt` — namespace/process identity:\n  daemon `user:[4026533197]`, sandboxed clients `user:[4026533274/...275]`, distinct mount\n  namespaces, observed both from inside the sandbox and from the host.\n- `bundle/repro/runtime_manifest.json` — runtime evidence manifest (all flags true).\n- Environment: Ubuntu 26.04 (worker) / ubuntu:26.04 (experiment container), pipewire and\n  pipewire-pulse 1.6.2-1ubuntu1.1, pactl 17.0, bubblewrap, kernel 7.0.14-arch1-1.\n\n## Recommendations / Next Steps\n- Mitigation (per disclosure): set `pulse.allow-module-loading = false` in\n  `pipewire-pulse.conf` (validated as this run's negative control).\n- Suggested upstream fix: refuse absolute-path LADSPA plugins for pulse-protocol clients\n  (or only allow plugins from trusted system directories), and/or default\n  `pulse.allow-module-loading` to false; ideally the pulse server should apply\n  Flatpak-style client confinement awareness (as PulseAudio's `client.access` model\n  starts to do) before honoring `LOAD_MODULE`.\n- Testing recommendations: regression test that `pactl load-module module-ladspa-sink\n  plugin=/tmp/x.so` fails on hardened configs; CI sandbox test mirroring this script.\n\n## Additional Notes\n- Idempotency: the script is fully idempotent — it rebuilds the payload, regenerates\n  configs, uses fresh runtime dirs per attempt, cleans oracle markers per attempt, and\n  kills its daemons on exit. It passed three consecutive end-to-end runs on this worker.\n- Outer-container limitation: the worker's seccomp blocks `CLONE_NEWUSER`/`CLONE_NEWNS`\n  even for root, so the experiment runs inside a Docker sibling container with an\n  unconfined seccomp profile. Inside it, the bubblewrap sandbox uses genuine, separate\n  mount and user namespaces (verified via namespace inode evidence from both sides); the\n  pipewire-pulse daemon runs outside that sandbox. This matches the ticket's required\n  bubblewrap/Flatpak-style confinement (chroot was *not* used).\n- `--unshare-pid` was omitted because a fresh `/proc` cannot be mounted inside rootless\n  Docker (locked procfs mounts); the ticket requires mount+user namespace separation,\n  which is provided and evidenced. `/proc/self/ns` is bind-mounted read-only into the\n  sandbox solely for identity evidence.\n- Edge case: the daemon answers `Failure: Missing implementation` for the malicious module\n  because `evil.so` exports no `ladspa_descriptor`; code execution already happened during\n  `dlopen()` (constructor), which is exactly why the marker exists despite the error.\n","cve_id":"CVE-2026-5674","source_url":"https://gitlab.freedesktop.org/pipewire/pipewire","reproduced_at":"2026-07-28T13:14:34.815377+00:00","duration_secs":5288.0,"tool_calls":286,"handoffs":2,"total_cost_usd":9.123991,"agent_costs":{"claim_matcher":0.022612,"judge":0.468765,"learning_policy":0.020685,"repro":3.309684,"support":0.089671,"vuln_variant":5.212574},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.022612},"judge":{"gpt-5.5":0.468765},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.020685},"repro":{"accounts/fireworks/models/kimi-k3":3.309684},"support":{"accounts/fireworks/models/kimi-k3":0.089671},"vuln_variant":{"accounts/fireworks/models/kimi-k3":5.212574}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-28T13:14:35.620061+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":16599,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9601,"category":"analysis"},{"path":"bundle/logs/client_fixed_attempt2.log","filename":"client_fixed_attempt2.log","size":451,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1948,"category":"other"},{"path":"bundle/logs/client_fixed_attempt1.log","filename":"client_fixed_attempt1.log","size":451,"category":"log"},{"path":"bundle/logs/client_vuln_attempt2.log","filename":"client_vuln_attempt2.log","size":460,"category":"log"},{"path":"bundle/logs/daemon_fixed_attempt1_pipewire.log","filename":"daemon_fixed_attempt1_pipewire.log","size":1235,"category":"log"},{"path":"bundle/logs/daemon_fixed_attempt1_pulse.log","filename":"daemon_fixed_attempt1_pulse.log","size":950,"category":"log"},{"path":"bundle/logs/daemon_fixed_attempt2_pipewire.log","filename":"daemon_fixed_attempt2_pipewire.log","size":1235,"category":"log"},{"path":"bundle/logs/daemon_fixed_attempt2_pulse.log","filename":"daemon_fixed_attempt2_pulse.log","size":950,"category":"log"},{"path":"bundle/logs/daemon_vuln_attempt1_pipewire.log","filename":"daemon_vuln_attempt1_pipewire.log","size":1235,"category":"log"},{"path":"bundle/logs/daemon_vuln_attempt1_pulse.log","filename":"daemon_vuln_attempt1_pulse.log","size":1832,"category":"log"},{"path":"bundle/logs/daemon_vuln_attempt2_pipewire.log","filename":"daemon_vuln_attempt2_pipewire.log","size":1235,"category":"log"},{"path":"bundle/logs/daemon_vuln_attempt2_pulse.log","filename":"daemon_vuln_attempt2_pulse.log","size":1832,"category":"log"},{"path":"bundle/logs/negative_control_obs_fixed_attempt2.json","filename":"negative_control_obs_fixed_attempt2.json","size":332,"category":"other"},{"path":"bundle/logs/ns_client_host_fixed_attempt1.txt","filename":"ns_client_host_fixed_attempt1.txt","size":126,"category":"other"},{"path":"bundle/logs/ns_client_host_fixed_attempt2.txt","filename":"ns_client_host_fixed_attempt2.txt","size":126,"category":"other"},{"path":"bundle/logs/ns_client_host_vuln_attempt1.txt","filename":"ns_client_host_vuln_attempt1.txt","size":125,"category":"other"},{"path":"bundle/logs/ns_client_host_vuln_attempt2.txt","filename":"ns_client_host_vuln_attempt2.txt","size":126,"category":"other"},{"path":"bundle/logs/ns_evidence_fixed_attempt1.txt","filename":"ns_evidence_fixed_attempt1.txt","size":354,"category":"other"},{"path":"bundle/logs/ns_evidence_fixed_attempt2.txt","filename":"ns_evidence_fixed_attempt2.txt","size":354,"category":"other"},{"path":"bundle/logs/ns_evidence_vuln_attempt2.txt","filename":"ns_evidence_vuln_attempt2.txt","size":354,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1063,"category":"other"}]}