Flatpak pipewire escape through module loading

PipeWire’s PulseAudio path is the weak point

The exposed path sits in the PulseAudio compatibility layer, not in Flatpak itself. A Flatpak app with --socket=pulseaudio can reach the user session’s audio service, and that service runs as the user, not as root. If the service treats the client as authenticated after checking only the length of the cookie, the gate is already open.

The cookie handling is the awkward bit. The check looks for a 256-byte value, but it does not compare that value against the token in ~/.config/pulse/cookie. Any 256 bytes of junk satisfy the length check. Once that happens, the client is marked authenticated and can move on to module loading.

Cookie checks stop at length, not identity

That failure matters because it turns a shared secret into a size check. A sandboxed app does not need to know the real cookie if the server only checks the payload length. The authentication step becomes theatre.

At that point, the client is inside the trusted side of the PulseAudio compatibility path. PipeWire then accepts later commands that were meant for authenticated clients only. The boundary is not the Flatpak sandbox any more. It is the service’s idea of trust, and that idea is broken.

Module loading stays open after authentication

Authenticated clients can still ask for modules to be loaded, and the default setting allows it. The relevant policy was pulse.allow-module-loading=true, which kept module loading enabled unless changed. That gives the attacker a second step after auth, which is usually where a bad day starts.

The dangerous module path is module-ladspa-sink. It accepts a plugin= value and passes it to dlopen(path, RTLD_NOW) without path validation or an allowlist. If the path points to a malicious shared object, dlopen() loads it and runs its ELF constructors immediately. No ceremony, just code execution in the user session.

The sandbox breaks when audio access meets a writable host path

Flatpak’s sandbox boundary holds until the app can both talk to the host audio socket and write a file the host can read. One permission alone is not enough. A writable host path alone is not enough either. Together they are a route from sandboxed code to code running outside the sandbox.

A Flatpak app only needs --socket=pulseaudio and some host-visible writable location such as --filesystem=/tmp. The audio socket lets it reach PipeWire’s PulseAudio path. The writable path gives it somewhere to place a malicious shared library that the host session can later load.

A Flatpak app only needs pulseaudio and somewhere on the host to write

That combination is not exotic. Audio-enabled Flatpaks often already have PulseAudio access, and some also have access to host paths for downloads or temp files. The demo configuration used --filesystem=/tmp, which is enough because the host session can read from there.

The exact app does not matter much. Any sandboxed program with those two permissions can stage the payload and then ask the audio service to load it. The sandbox has no say once the service accepts the module request.

Plugin loading turns that write into code execution in the user session

The payload is just a shared library with a constructor or other load-time code. Once dlopen() touches it, the code runs in the user session under the user account. That gives the attacker access to the desktop session, files, and credentials available to that user.

This is a user-context escape, not a root escalation. That still matters. For a desktop session, user-context code execution outside the sandbox is usually enough to read secrets, tamper with files, and interfere with anything else running under the same account. Flatpak’s boundary is supposed to stop that sort of thing. Here it does not.

Close the gap with a tighter module policy and a narrower attack surface

The practical fix is to stop treating module loading as a default convenience. Set pulse.allow-module-loading=false unless there is a clear reason to keep it on. Then restrict LADSPA plugin paths to known directories such as /usr/lib/ladspa/ and /usr/lib64/ladspa/ rather than accepting arbitrary paths from the client.

That still leaves the audio socket itself. A Flatpak app that does not need host audio access should not get it. If it does need audio, keep the rest of its host access narrow. --socket=pulseaudio plus a writable host path is the dangerous pair. Remove either side and the attack chain stops being useful.

PipeWire is a user-level service, so the blast radius sits inside the user session. That is smaller than a kernel bug and still bad enough. A sandbox escape that ends at dlopen() is crude, but it works well enough to ruin an afternoon.

Related posts

Linux kernel net/sched UAF constraints on CentOS 9 desktop

Linux kernel net/sched use-after-free looks messy on paper, and on CentOS 9 Desktop it is worse, because the obvious paths are blocked and the useful ones are just awkward enough to miss. I would not...

Uptime Kuma | 2.5.9

Uptime Kuma v2 5 9: security and stability update addressing RSS cache poisoning, bug fixes for timeouts, retries and UI refreshes, recommended upgrade

paperless-ngx | v3.4.2

paperless ngx v3 4 2: mobile doc detail UI fixes, save view and form fixes, backend OCR and task stability, dependency and tooling upgrades