libobs: Fix use-after-free of canvas view in audio thread#736
Open
summeroff wants to merge 1 commit into
Open
Conversation
audio_callback() walks obs->video.mixes under mixes_mutex and locks each mix->view->channels_mutex. mixes_mutex guards the mixes array, not the lifetime of the obs_view objects the mixes point to, so the audio thread can dereference a view that was already freed and crash in pthread_mutex_lock (obs-audio.c:519). Standalone views are protected by obs_view_remove(), which nulls every matching mix->view under mixes_mutex before the view is freed; the audio thread then skips them via its "if (!view) continue" guard. Canvas-owned views had no equivalent: obs_canvas_destroy() freed canvas->view while a mix could still reference it, relying only on obs_canvas_clear_mix() having erased canvas->mix. Call obs_view_remove() in obs_canvas_destroy() before obs_view_free() so the canvas path honors the same contract. Triggered in the wild by a GPU device-removed storm driving repeated partial video resets while the audio thread kept running. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes a use-after-free risk where the audio thread can lock view->channels_mutex through an obs_core_video_mix that still references a canvas-embedded obs_view after the canvas is destroyed.
Changes:
- Ensure canvas destruction nulls all
mix->viewpointers referencing the canvas view by callingobs_view_remove(&canvas->view)beforeobs_view_free(&canvas->view).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pthread_mutex_destroy(&canvas->sources_mutex); | ||
| obs_context_data_free(&canvas->context); | ||
|
|
||
| /* Null mixes referencing this view before freeing it (obs-audio.c:519). */ |
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.
Problem
Sentry crash in the audio thread:
EXCEPTION_ACCESS_VIOLATION_READ / 0x2c200000007atobs-audio.c:519,pthread_mutex_lock(&view->channels_mutex). The faulting value is a freed-and-reused heap pointer — an in-array mix had a non-NULLviewpointing at a freedobs_view.Root cause
audio_callback()walksobs->video.mixesundermixes_mutexand locks eachmix->view->channels_mutex(obs-audio.c:513-538).mixes_mutexguards the mixes array, not the lifetime of theobs_viewobjects the mixes point at. Freeing a view takes no lock, so the design is safe only if every freeing path nulls/erases the referencing mix pointer (undermixes_mutex) before the view is freed.obs_view_remove()(obs-view.c:222), which nulls every matchingmix->viewundermixes_mutex; the audio thread then skips them viaif (!view) continue;(obs-audio.c:516).obs_viewlives insideobs_canvas) had no equivalent.obs_canvas_destroy()freedcanvas->viewrelying only onobs_canvas_clear_mix()having erasedcanvas->mix— which removes only that one mix, not any other in-array mix referencing the view.Triggered in the wild by a GPU device-removed storm (DXGI
887A0006) driving repeated partial video resets (SetVideoContext→obs_deactivate_video_info→stop_video()+obs_free_video(false)).stop_video()joins only the graphics thread; the audio thread keeps running across the reset.Fix
Call
obs_view_remove(&canvas->view)inobs_canvas_destroy()immediately beforeobs_view_free(), so the canvas path honors the same contract as the standalone path. Placed in destroy only — not inobs_canvas_clear_mix(), which is also used by the reset path where the canvas survives and a new mix is created pointing at the same live view.Lock ordering:
obs_view_remove()takes onlymixes_mutex, respecting the existingencoders_mutex -> mixes_mutexorder. Shutdown ordering is safe:obs_free_data()(canvas destroy,obs.c:1612) runs beforeobs_free_video(true)destroysmixes_mutex(obs.c:1654).Verification
cmake --build build_x64 --target libobs --config RelWithDebInfo→ exit 0,obs.dlllinks clean.Follow-ups (out of scope, not fixed here)
obs_free_video(false)assumes index 0 is always the main mix (obs.c:920-921), butobs_canvas_clear_mix'sda_erasecan shift a secondary mix into index 0.🤖 Generated with Claude Code