stride3d / stride3d/stride

Linux/OpenAL: `xnAudioSourceStop` self-deadlocks on a global spinlock, permanently freezing all audio (plus two contributing defects in the same subsystem)

Open
#3,314 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
7.8k
Forks
1.2k
Avg merge
2d 17h
Merged PRs (30d)
49

Description

Release Type: Official Release
Version: 4.3.0.2507
Platform(s): Linux (any OpenAL platform: Linux/macOS/Android; Windows/XAudio2 unaffected)

Describe the bug
libstrideaudio.so (built from sources/engine/Stride.Audio/Native/OpenAL.cpp) synchronizes
every xnAudio* entry point with one process-global 1-byte spinlock
(OpenAL::ContextState::sOpenAlLock). xnAudioSourceStop acquires that lock and then — still
holding it — calls xnAudioSourceFlushBuffers, which acquires the same non-recursive lock.
The calling thread spins forever with the lock latched at 1, so every subsequent audio call
from any thread also spins forever: the game freezes permanently (we measured ~182% CPU — the
wedged engine worker plus the wedged game thread).

Disassembly of the shipped binary:

f480 <xnAudioSourceStop>:
  f4a2:  lock cmpxchg %cl,(%rbx)          # acquire sOpenAlLock
  f4a6:  jne  f4a0
  f4d7:  call *(%rax)                     # alSourceStop
  f4dc:  call 2f9d0 <xnAudioSourceFlushBuffers@plt>   # <-- STILL HOLDING THE LOCK
  f503:  movb $0x0,(%rbx)                 # release -- NEVER REACHED

f330 <xnAudioSourceFlushBuffers>:
  f362:  lock cmpxchg %cl,(%r12)          # acquires the SAME lock -> can never succeed

xnAudioSourceStop is the only entry point with this double acquire (xnAudioSourceDestroy
calls no PLT audio helper and is safe).

Two contributing defects in the same subsystem make the freeze easy to reach:

  1. DynamicSoundSource.Worker busy-spins at 100% CPU. The loop only Thread.Sleep(10)s
    when no source reports CanFill, but CanFill is permanently true for any idle/finished
    source (it owns free device buffers). A single idle one-shot pins the worker thread, which
    hammers sOpenAlLock continuously and starves every other thread that touches audio.
  2. The spinlock itself has no pause, no yield, no backoff, no fairness
    (objdump -d libstrideaudio.so | grep -c pause → 0), and alcMakeContextCurrent is called
    inside the critical section, so hold times are real ALC calls. Under any contention the
    loser can be starved unboundedly.

To Reproduce

  1. Linux build, any scene with a DynamicSoundSource-backed one-shot sound.
  2. Play the one-shot, then call Stop() on it (engine worker path:
    StopInternal(ignoreQueuedBuffer: true)AudioLayer.SourceStopxnAudioSourceStop).
  3. The stopping thread deadlocks; the next audio call from any thread (e.g. a
    SoundInstance.Volume set) spins forever. Game frozen.

Expected behavior
Stopping a sound stops the sound.

Additional context
Suggested fixes, smallest first: inline the flush body into xnAudioSourceStop (or make the
guard recursion-aware) for the deadlock; gate the worker's sleep on sources that are actually
playing for the busy-spin; add pause/backoff (or a real mutex) for the spinlock. Our
app-side workaround (documented extension points only, no engine patch): override
StopInternal(ignoreQueuedBuffer: false) so the native SourceStop is never reached, keep
voices permanently open streaming silence, override CanFill to report false unless playing,
and route all audio calls through one dedicated thread. Happy to PR the native one-liner.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with sources/engine/Stride.Audio/Native/OpenAL.cpp and trace xnAudioSourceStop, xnAudioSourceFlushBuffers, and the sOpenAlLock guard. Then inspect DynamicSoundSource.Worker and CanFill on a Linux build, reproducing the one-shot Stop() path. Done means the reported stop freeze, idle-source busy spin, and lock contention behavior are addressed and audio calls complete normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, csharp, linux
Domain
audio-video-rtc, game-dev, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.