Summary
PipeWireBackendFactory::init() can block the calling application forever when the PipeWire daemon accepts connections but never completes registry syncs. There is no timeout on the wait: a stalled or wedged PipeWire daemon turns any alcOpenDevice() call into a permanent hang of the host application's main thread.
We hit this in production with telegram-desktop on NixOS: the app froze at startup for an entire boot session because PipeWire came up in a state where client syncs never completed.
Environment
- openal-soft 1.24.3 (nixpkgs package)
- PipeWire 1.6.8 (client library and daemon)
- Host app: telegram-desktop 7.0.2 (calls
alcOpenDevice() during startup)
- Linux x86_64, NixOS 26.11 unstable
Backtrace
From a core dump taken (SIGABRT) while the app was hung, ~20 s after launch:
#3 pthread_cond_wait@@GLIBC_2.3.2 libc.so.6
#4 loop_wait() libspa-support.so (SPA data loop)
#5 PipeWireBackendFactory::init() libopenal.so.1
#6 alc_initconfig() libopenal.so.1
#7 __pthread_once_slow libc.so.6
#8 ... app startup (main thread)
Other threads: two PipeWire do_loop/loop_iterate threads alive and polling (epoll_wait), so the backend's loop threads exist — the wakeup/done event that would release loop_wait never arrives.
Code path (alc/backends/pipewire.cpp @ master, 2dc741b)
bool PipeWireBackendFactory::init()
{
...
pw_init(nullptr, nullptr);
if(!gEventHandler.init())
return false;
if(!GetConfigValueBool({}, "pipewire", "assume-audio", false)
&& !gEventHandler.waitForAudio()) // ← blocks forever
auto waitForAudio() -> bool
{
auto const plock = MainloopUniqueLock{mLoop};
auto has_audio = false;
plock.wait([this,&has_audio]
{
has_audio = mHasAudio.load(std::memory_order_acquire);
return has_audio || initIsDone(std::memory_order_acquire);
});
return has_audio;
}
initIsDone is only set when the pw_core_sync round-trip started in EventManager::init() completes. In our failing state, PipeWire clients could connect but syncs never completed — pw-dump and wpctl status hung the same way from the command line, with no error in the PipeWire/WirePlumber journals. Result: waitForAudio() waits on the condition forever, and because this runs under pthread_once from the app's main thread during startup, the whole application is dead — window never appears, event loop never starts.
Reproduction
- Get the PipeWire daemon into a state where it accepts client connections but never answers registry syncs (we observed this after a session bring-up;
pw-dump hanging is the tell).
- Run any app that calls
alcOpenDevice() with the PipeWire backend enabled.
- App hangs permanently in
PipeWireBackendFactory::init().
Restarting pipewire/wireplumber (making syncs complete again) makes the exact same binary launch instantly, which rules out the application and confirms the backend wait as the blocking point.
Observed impact
Because the first hung instance becomes the single-instance server without ever processing events, every later launch of the app connects as a second instance, sends its command, and hangs too — one wedged PipeWire state takes down the app for the whole session.
Suggestion
Bound the wait in waitForAudio() (and waitForInit()): e.g. wait on the PipeWire fd with a timeout, or use a timed condition wait, and treat a stalled sync as "no PipeWire support" (return false, kill() the handler) so the caller falls back to the next backend. Current master (2dc741b) still has the unbounded wait.
Workaround we verified: forcing another driver (drivers = pulse in alsoft.conf) skips the PipeWire backend entirely and the app starts instantly.
Summary
PipeWireBackendFactory::init()can block the calling application forever when the PipeWire daemon accepts connections but never completes registry syncs. There is no timeout on the wait: a stalled or wedged PipeWire daemon turns anyalcOpenDevice()call into a permanent hang of the host application's main thread.We hit this in production with telegram-desktop on NixOS: the app froze at startup for an entire boot session because PipeWire came up in a state where client syncs never completed.
Environment
alcOpenDevice()during startup)Backtrace
From a core dump taken (SIGABRT) while the app was hung, ~20 s after launch:
Other threads: two PipeWire
do_loop/loop_iteratethreads alive and polling (epoll_wait), so the backend's loop threads exist — the wakeup/doneevent that would releaseloop_waitnever arrives.Code path (alc/backends/pipewire.cpp @ master, 2dc741b)
initIsDoneis only set when thepw_core_syncround-trip started inEventManager::init()completes. In our failing state, PipeWire clients could connect but syncs never completed —pw-dumpandwpctl statushung the same way from the command line, with no error in the PipeWire/WirePlumber journals. Result:waitForAudio()waits on the condition forever, and because this runs underpthread_oncefrom the app's main thread during startup, the whole application is dead — window never appears, event loop never starts.Reproduction
pw-dumphanging is the tell).alcOpenDevice()with the PipeWire backend enabled.PipeWireBackendFactory::init().Restarting
pipewire/wireplumber(making syncs complete again) makes the exact same binary launch instantly, which rules out the application and confirms the backend wait as the blocking point.Observed impact
Because the first hung instance becomes the single-instance server without ever processing events, every later launch of the app connects as a second instance, sends its command, and hangs too — one wedged PipeWire state takes down the app for the whole session.
Suggestion
Bound the wait in
waitForAudio()(andwaitForInit()): e.g. wait on the PipeWire fd with a timeout, or use a timed condition wait, and treat a stalled sync as "no PipeWire support" (return false,kill()the handler) so the caller falls back to the next backend. Current master (2dc741b) still has the unbounded wait.Workaround we verified: forcing another driver (
drivers = pulsein alsoft.conf) skips the PipeWire backend entirely and the app starts instantly.