Phase 3: thread-safety & resource fixes (atomic stream state, JACK/ASIO/OSS) - #2
Merged
Merged
Conversation
Backend changes are compile-verified by CI (not runtime-tested here for lack of the target hardware). The portable build stays warning-clean and its tests pass under -fsanitize=address,undefined. * core: stream_.state is now std::atomic<StreamState>. It is read by the realtime callback thread and written by the control thread (e.g. stopStream) without the stream mutex; every access is a plain load/store (no address-of, switch, or arithmetic), so the type change is transparent while removing the data race. * JACK: closeStream() now always calls jack_deactivate() before unregistering ports and freeing the handle, closing the window where the realtime xrun callback could run against freed state (it was only deactivating when state == RUNNING). The duplex INPUT error path no longer tears down the client/handle shared with the already-open OUTPUT pass — it only frees the handle when this call allocated it. * ASIO: the process-global asioCallbackInfo is cleared in closeStream() and NULL-checked in bufferSwitch(), sampleRateChanged() and asioMessages(), so a late driver callback can no longer dereference destroyed stream state. * OSS: OssHandle::id[] now uses -1 (not 0) as the "unused" sentinel, and the teardown paths close only descriptors >= 0. Previously fd 0 (a valid descriptor) was treated as "no device" and leaked. AUDIT.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WHbT36gMxx7WH2tzZfiqUt
This was referenced Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Phase 3 of the audit-and-hardening work, building on the merged #1. These are thread-safety and resource-management fixes in the realtime/teardown paths of the backends, plus the core
stream_.statechange. Findings and status are tracked inAUDIT.md.All 16 CI jobs pass on this branch (run #5). The
stream_.statechange is portable and is additionally covered by the ASan/UBSan and StrictWarnings (g++ & clang++) gates; the backend changes are compile-verified across the matrix (Linux ALSA/Pulse/JACK/OSS, macOS/CoreAudio, MinGW WASAPI/ASIO/DirectSound) but — lacking the target hardware — were not runtime-tested. Please review the backend changes with that in mind.Changes
stream_.stateis nowstd::atomic<StreamState>(H-5). It's read by the realtime callback thread and written by the control thread (e.g.stopStream) without the stream mutex. All ~180 access sites are plain load/store (no address-of, switch, or arithmetic), so the type change is transparent while removing the data race.closeStream()now always callsjack_deactivate()before unregistering ports / freeing the handle (was only whenstate == RUNNING), closing the window where the realtime xrun callback ran against freed ports (H-4). The duplex INPUT error path no longer tears down the client/handle shared with the already-open OUTPUT pass — it only frees the handle when this call allocated it (H-2).asioCallbackInfois cleared incloseStream()and NULL-checked inbufferSwitch(),sampleRateChanged()andasioMessages(), so a late driver callback can't dereference destroyed stream state (H-3).OssHandle::id[]uses-1(not0) as the "unused" sentinel and the teardown paths close only descriptors>= 0; previously fd0(a valid descriptor) was treated as "no device" and leaked (M-1).Deferred (see AUDIT.md)
Remaining low-severity items: the ASIO single-instance
streamOpencheck-then-set race (ASIO is effectively single-driver), and assorted ignored return codes (ALSAsnd_pcm_drop, OSS ioctls, WASAPI/DSCreateEvent/CoCreateInstance) and non-predicate-loop drain handshakes.🤖 Generated with Claude Code
https://claude.ai/code/session_01WHbT36gMxx7WH2tzZfiqUt
Generated by Claude Code