Skip to content

SIGABRT in libpulse: twelve tick threads share one pa_context #5

Description

@ParadokS81

StreamController 1.5.0-beta.15, pulsectl 24.12.0, libpulse 17.0, pipewire-pulse 1.6.8, Python 3.14 (GIL build), Arch.

With the stock VolumeMixer page (4x VolumeUp, 4x VolumeDown, 4x VolumeMute, MoveLeft, MoveRight, Exit), StreamController aborts inside libpulse (pstream queue assertion) minutes after the page loads. Three cores in one evening; timing-dependent, so it recurs irregularly.

Mechanism (from the source, not inferred from the stack):

  1. DeckController.py spawns a new thread per tick, per input (threading.Thread(target=self.own_actions_tick, ...)).
  2. Twelve of the fifteen actions call self.plugin_base.pulse.sink_input_list() from on_tick().
  3. All twelve share the single pulsectl.Pulse("stream-controller", threading_lock=True) created in main.py:174.
  4. In pulsectl 24.12.0 that lock is only held inside _pulse_loop(), i.e. around mainloop iteration. The command submission in _pulse_get_list._wrapper_method calls pa_context_get_sink_input_info_list() outside any lock.
  5. ctypes releases the GIL for foreign calls, so a dozen OS threads push onto the same pa_context pstream queue concurrently; the queue corrupts and libpulse's own assertion aborts the process.

threading_lock=True looks like it makes this safe; it does not.

When it fires: ticking is suspended while the screensaver shows, so unlocking restarts all twelve tick threads at once — both observed aborts came 30 s to 2 min after an unlock.

Fix options: a real lock around sink_input_list / mute / volume_set_all_chans, or query the sink-input list once per tick and share the result across the actions instead of twelve times.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions