From 882cc3ef7023d2245343f97f2ab2f830faa292ac Mon Sep 17 00:00:00 2001 From: Rex Date: Sat, 11 Apr 2026 10:41:33 -0400 Subject: [PATCH 1/3] Fix hang on OBS exit caused by use-after-free in destroy path source_record_filter_destroy queued force_stop_output_task via run_queued and then called source_record_delayed_destroy directly, which could free the context before the queued tasks ran. During OBS exit the task queue often stops processing before the queued stops fire, leaving outputs in a stopping state that OBS waits on, causing the entire application to hang on close. Introduce stop_output_sync() which force-stops and releases an output inline (after disconnecting the remove_filter signal), and use it in all closing paths: filter_remove, destroy, and the closing branches of filter_update. The non-closing stop paths (user-initiated toggle off, tick restart/disable) still use the async force_stop_output_task path. delayed_destroy is removed and its cleanup logic inlined into destroy. --- source-record.c | 169 ++++++++++++++++++++++-------------------------- 1 file changed, 76 insertions(+), 93 deletions(-) diff --git a/source-record.c b/source-record.c index 35eac9f..5f9ee07 100644 --- a/source-record.c +++ b/source-record.c @@ -375,6 +375,21 @@ static void remove_filter(void *data, calldata_t *calldata) obs_source_filter_remove(source, filter->source); } +// Used in destroy/closing paths instead of run_queued(force_stop_output_task) +// to prevent a use-after-free where queued tasks reference the context after +// it has been freed (which also causes OBS to hang on exit). +static void stop_output_sync(struct source_record_filter_context *context, obs_output_t *output) +{ + if (!output) + return; + signal_handler_t *sh = obs_output_get_signal_handler(output); + if (sh) + signal_handler_disconnect(sh, "stop", remove_filter, context); + if (obs_output_active(output)) + obs_output_force_stop(output); + obs_output_release(output); +} + static const char *get_encoder_id(obs_data_t *settings) { const char *enc_id = obs_data_get_string(settings, "encoder"); @@ -870,8 +885,11 @@ static void source_record_filter_update(void *data, obs_data_t *settings) if (record) { if (obs_source_enabled(filter->source) && filter->video_output) start_file_output(filter, settings); - } else { - if (filter->fileOutput) { + } else if (filter->fileOutput) { + if (filter->closing) { + stop_output_sync(filter, filter->fileOutput); + filter->fileOutput = NULL; + } else { struct stop_output *so = bmalloc(sizeof(struct stop_output)); so->output = filter->fileOutput; so->context = filter; @@ -900,11 +918,16 @@ static void source_record_filter_update(void *data, obs_data_t *settings) obs_data_t *hotkeys = obs_hotkeys_save_output(filter->replayOutput); obs_data_set_obj(settings, "replay_hotkeys", hotkeys); obs_data_release(hotkeys); - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = filter->replayOutput; - so->context = filter; - run_queued(force_stop_output_task, so); - filter->replayOutput = NULL; + if (filter->closing) { + stop_output_sync(filter, filter->replayOutput); + filter->replayOutput = NULL; + } else { + struct stop_output *so = bmalloc(sizeof(struct stop_output)); + so->output = filter->replayOutput; + so->context = filter; + run_queued(force_stop_output_task, so); + filter->replayOutput = NULL; + } } filter->replayBuffer = replay_buffer; @@ -955,8 +978,11 @@ static void source_record_filter_update(void *data, obs_data_t *settings) if (stream) { if (obs_source_enabled(filter->source) && filter->video_output) start_stream_output(filter, settings); - } else { - if (filter->streamOutput) { + } else if (filter->streamOutput) { + if (filter->closing) { + stop_output_sync(filter, filter->streamOutput); + filter->streamOutput = NULL; + } else { struct stop_output *so = bmalloc(sizeof(struct stop_output)); so->output = filter->streamOutput; so->context = filter; @@ -1117,47 +1143,6 @@ static void *source_record_filter_create(obs_data_t *settings, obs_source_t *sou return context; } -static void source_record_delayed_destroy(void *data) -{ - struct source_record_filter_context *context = data; - if (context->encoder && obs_encoder_active(context->encoder)) { - run_queued(source_record_delayed_destroy, context); - return; - } - - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - if (!context->audioEncoder[i] || !obs_encoder_active(context->audioEncoder[i])) - continue; - run_queued(source_record_delayed_destroy, context); - return; - } - for (int i = 0; i < MAX_AUDIO_MIXES; i++) { - obs_encoder_release(context->audioEncoder[i]); - context->audioEncoder[i] = NULL; - } - obs_encoder_release(context->encoder); - context->encoder = NULL; - - obs_weak_source_release(context->audio_source); - context->audio_source = NULL; - - if (context->audio_track == 0) - audio_output_close(context->audio_output); - - obs_service_release(context->service); - - if (context->video_output && context->view) { - obs_view_set_source(context->view, BACKGROUND_CHANNEL, NULL); - obs_view_set_source(context->view, SOURCE_CHANNEL, NULL); - obs_view_remove(context->view); - context->video_output = NULL; - } - - obs_view_destroy(context->view); - - bfree(context); -} - static void source_record_filter_destroy(void *data) { struct source_record_filter_context *context = data; @@ -1171,27 +1156,12 @@ static void source_record_filter_destroy(void *data) } obs_frontend_remove_event_callback(frontend_event, context); - if (context->fileOutput) { - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = context->fileOutput; - so->context = context; - run_queued(force_stop_output_task, so); - context->fileOutput = NULL; - } - if (context->streamOutput) { - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = context->streamOutput; - so->context = context; - run_queued(force_stop_output_task, so); - context->streamOutput = NULL; - } - if (context->replayOutput) { - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = context->replayOutput; - so->context = context; - run_queued(force_stop_output_task, so); - context->replayOutput = NULL; - } + stop_output_sync(context, context->fileOutput); + context->fileOutput = NULL; + stop_output_sync(context, context->streamOutput); + context->streamOutput = NULL; + stop_output_sync(context, context->replayOutput); + context->replayOutput = NULL; if (context->enableHotkey != OBS_INVALID_HOTKEY_PAIR_ID) obs_hotkey_pair_unregister(context->enableHotkey); @@ -1205,8 +1175,36 @@ static void source_record_filter_destroy(void *data) if (context->chapterHotkey != OBS_INVALID_HOTKEY_ID) obs_hotkey_unregister(context->chapterHotkey); + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { + obs_encoder_release(context->audioEncoder[i]); + context->audioEncoder[i] = NULL; + } + obs_encoder_release(context->encoder); + context->encoder = NULL; + + obs_weak_source_release(context->audio_source); + context->audio_source = NULL; + + if (context->audio_track == 0 && context->audio_output) + audio_output_close(context->audio_output); + context->audio_output = NULL; + + obs_service_release(context->service); + context->service = NULL; + + if (context->view) { + obs_view_set_source(context->view, BACKGROUND_CHANNEL, NULL); + obs_view_set_source(context->view, SOURCE_CHANNEL, NULL); + if (context->video_output) { + obs_view_remove(context->view); + context->video_output = NULL; + } + obs_view_destroy(context->view); + context->view = NULL; + } + context->source = NULL; - source_record_delayed_destroy(context); + bfree(context); } static bool source_record_enable_hotkey(void *data, obs_hotkey_pair_id id, obs_hotkey_t *hotkey, bool pressed) @@ -1806,27 +1804,12 @@ static void source_record_filter_filter_remove(void *data, obs_source_t *parent) UNUSED_PARAMETER(parent); struct source_record_filter_context *context = data; context->closing = true; - if (context->fileOutput) { - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = context->fileOutput; - so->context = context; - run_queued(force_stop_output_task, so); - context->fileOutput = NULL; - } - if (context->streamOutput) { - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = context->streamOutput; - so->context = context; - run_queued(force_stop_output_task, so); - context->streamOutput = NULL; - } - if (context->replayOutput) { - struct stop_output *so = bmalloc(sizeof(struct stop_output)); - so->output = context->replayOutput; - so->context = context; - run_queued(force_stop_output_task, so); - context->replayOutput = NULL; - } + stop_output_sync(context, context->fileOutput); + context->fileOutput = NULL; + stop_output_sync(context, context->streamOutput); + context->streamOutput = NULL; + stop_output_sync(context, context->replayOutput); + context->replayOutput = NULL; obs_frontend_remove_event_callback(frontend_event, context); } From 1326983bda4fe40f82f1e8dc21145fe8d5c192d0 Mon Sep 17 00:00:00 2001 From: Rex Date: Sun, 12 Apr 2026 15:19:54 -0400 Subject: [PATCH 2/3] Respect exiting flag in stop_output_sync; wait for encoder inactivity Co-Authored-By: Claude Opus 4.6 (1M context) --- source-record.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/source-record.c b/source-record.c index 5f9ee07..d15e596 100644 --- a/source-record.c +++ b/source-record.c @@ -375,9 +375,6 @@ static void remove_filter(void *data, calldata_t *calldata) obs_source_filter_remove(source, filter->source); } -// Used in destroy/closing paths instead of run_queued(force_stop_output_task) -// to prevent a use-after-free where queued tasks reference the context after -// it has been freed (which also causes OBS to hang on exit). static void stop_output_sync(struct source_record_filter_context *context, obs_output_t *output) { if (!output) @@ -387,7 +384,8 @@ static void stop_output_sync(struct source_record_filter_context *context, obs_o signal_handler_disconnect(sh, "stop", remove_filter, context); if (obs_output_active(output)) obs_output_force_stop(output); - obs_output_release(output); + if (!context->exiting) + obs_output_release(output); } static const char *get_encoder_id(obs_data_t *settings) @@ -1175,6 +1173,20 @@ static void source_record_filter_destroy(void *data) if (context->chapterHotkey != OBS_INVALID_HOTKEY_ID) obs_hotkey_unregister(context->chapterHotkey); + for (int retries = 0; retries < 300; retries++) { + bool any_active = false; + if (context->encoder && obs_encoder_active(context->encoder)) + any_active = true; + for (int i = 0; i < MAX_AUDIO_MIXES && !any_active; i++) { + if (context->audioEncoder[i] && + obs_encoder_active(context->audioEncoder[i])) + any_active = true; + } + if (!any_active) + break; + os_sleep_ms(10); + } + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { obs_encoder_release(context->audioEncoder[i]); context->audioEncoder[i] = NULL; From f689e677f954d6ef1e97dc141cf83e0290122766 Mon Sep 17 00:00:00 2001 From: Rex Date: Sun, 12 Apr 2026 16:12:18 -0400 Subject: [PATCH 3/3] Don't release output in stop_output_sync, defer to destroy obs_output_force_stop is async for encoded outputs; releasing immediately races with end_data_capture_thread still using encoders. Let destroy handle release after encoder inactivity poll. Fixes crash on filter remove and scene collection switch with replay buffer active. Fixes output reference leak on exit. Co-Authored-By: Claude Opus 4.6 (1M context) --- source-record.c | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/source-record.c b/source-record.c index d15e596..948026c 100644 --- a/source-record.c +++ b/source-record.c @@ -384,8 +384,6 @@ static void stop_output_sync(struct source_record_filter_context *context, obs_o signal_handler_disconnect(sh, "stop", remove_filter, context); if (obs_output_active(output)) obs_output_force_stop(output); - if (!context->exiting) - obs_output_release(output); } static const char *get_encoder_id(obs_data_t *settings) @@ -886,7 +884,6 @@ static void source_record_filter_update(void *data, obs_data_t *settings) } else if (filter->fileOutput) { if (filter->closing) { stop_output_sync(filter, filter->fileOutput); - filter->fileOutput = NULL; } else { struct stop_output *so = bmalloc(sizeof(struct stop_output)); so->output = filter->fileOutput; @@ -918,7 +915,6 @@ static void source_record_filter_update(void *data, obs_data_t *settings) obs_data_release(hotkeys); if (filter->closing) { stop_output_sync(filter, filter->replayOutput); - filter->replayOutput = NULL; } else { struct stop_output *so = bmalloc(sizeof(struct stop_output)); so->output = filter->replayOutput; @@ -979,7 +975,6 @@ static void source_record_filter_update(void *data, obs_data_t *settings) } else if (filter->streamOutput) { if (filter->closing) { stop_output_sync(filter, filter->streamOutput); - filter->streamOutput = NULL; } else { struct stop_output *so = bmalloc(sizeof(struct stop_output)); so->output = filter->streamOutput; @@ -1155,11 +1150,8 @@ static void source_record_filter_destroy(void *data) obs_frontend_remove_event_callback(frontend_event, context); stop_output_sync(context, context->fileOutput); - context->fileOutput = NULL; stop_output_sync(context, context->streamOutput); - context->streamOutput = NULL; stop_output_sync(context, context->replayOutput); - context->replayOutput = NULL; if (context->enableHotkey != OBS_INVALID_HOTKEY_PAIR_ID) obs_hotkey_pair_unregister(context->enableHotkey); @@ -1187,6 +1179,13 @@ static void source_record_filter_destroy(void *data) os_sleep_ms(10); } + obs_output_release(context->fileOutput); + obs_output_release(context->streamOutput); + obs_output_release(context->replayOutput); + context->fileOutput = NULL; + context->streamOutput = NULL; + context->replayOutput = NULL; + for (int i = 0; i < MAX_AUDIO_MIXES; i++) { obs_encoder_release(context->audioEncoder[i]); context->audioEncoder[i] = NULL; @@ -1817,11 +1816,8 @@ static void source_record_filter_filter_remove(void *data, obs_source_t *parent) struct source_record_filter_context *context = data; context->closing = true; stop_output_sync(context, context->fileOutput); - context->fileOutput = NULL; stop_output_sync(context, context->streamOutput); - context->streamOutput = NULL; stop_output_sync(context, context->replayOutput); - context->replayOutput = NULL; obs_frontend_remove_event_callback(frontend_event, context); }