Fix use-after-free freeze in Linux TaskRunnerLinux::EnqueueTask - #1177
Open
p-divita-salto wants to merge 2 commits into
Open
Fix use-after-free freeze in Linux TaskRunnerLinux::EnqueueTask#1177p-divita-salto wants to merge 2 commits into
p-divita-salto wants to merge 2 commits into
Conversation
On Linux, enabling captions during a call keeps an AudioRendererSink attached to the remote audio track for the call's duration. Every audio frame posts a task via TaskRunnerLinux::EnqueueTask, which scheduled a GLib idle callback through g_main_context_invoke capturing a raw `this` pointer. stopAudioRenderer destroys the owning sink (and its TaskRunnerLinux) synchronously via RemoveSink() + renderers_.erase(it). If an idle callback was already scheduled but hadn't run yet at that moment, the main loop later dispatched it against freed memory, locking a std::mutex inside a deallocated TaskRunnerLinux. That reproduced as the whole app freezing on hangup after a captioned call: a gdb thread dump showed the main/GTK thread permanently blocked in std::mutex::lock inside EnqueueTask's callback, with no other thread holding the mutex — the signature of a stale pointer rather than live contention. TaskRunnerLinux now inherits enable_shared_from_this and is held via shared_ptr (AudioRendererSink and VisualizerSink both share this lifetime pattern). EnqueueTask passes a heap-allocated weak_ptr through g_main_context_invoke_full instead of a raw pointer through g_main_context_invoke, freeing it via the accompanying GDestroyNotify. The dispatched callback locks the weak_ptr and no-ops if the runner was already destroyed, instead of touching freed memory. Verified on Linux: call, answer, enable captions, talk, hang up no longer freezes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
p-divita-salto
requested review from
cloudwebrtc,
hiroshihorie and
xianshijing-lk
as code owners
August 20, 2026 12:24
|
|
cloudwebrtc
approved these changes
Aug 20, 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.
On Linux, the app can freeze completely if an AudioRendererSink (or VisualizerSink) is torn down via stopAudioRenderer/stopVisualizer while an audio frame's task is still queued for dispatch on the GLib main loop.
The freeze is permanent and the whole UI thread hangs.
A gdb thread dump at the moment of freeze showed the main/GTK thread permanently blocked:
No other thread was holding the contended mutex — the signature of a stale pointer rather than live contention.
AudioRendererSink::PostEvent() calls TaskRunnerLinux::EnqueueTask(), which schedules a GLib idle callback via g_main_context_invoke(context, callback, this), capturing a raw TaskRunnerLinux*.
stopAudioRenderer destroys the owning sink (and its TaskRunnerLinux) synchronously:
it->second->RemoveSink();
renderers_.erase(it);
If an idle callback was already scheduled but had not yet run at that moment, the main loop later dispatches it against freed memory, locking a std::mutex inside deallocated memory is undefined behavior, which manifests here as a permanent futex wait.
My fix works as follow:
I reproduced and verified fixed on Linux: start a call, enable an audio renderer (I have a captions/transcription use case), talk for a while so several frames are queued, then tear down the renderer and end the call: no longer freezes.
I ran repeatedly without reproduction after the fix; reproduced reliably before it