From 22fd206db95a91c9042fb81b35103e701b970689 Mon Sep 17 00:00:00 2001 From: Steven Noonan Date: Tue, 14 Jul 2026 14:59:12 -0700 Subject: [PATCH] resource_manager: fix data buffer teardown racing async load job tail ma_resource_manager_data_buffer_uninit takes a synchronous teardown path as soon as it observes result == MA_SUCCESS, freeing the buffer. But the load job (ma_job_process__resource_manager__load_data_buffer) publishes that result code via a CAS several instructions before it finishes referencing the buffer: it still reads has_connector() and does the executionPointer increment afterward. A concurrent uninit that observes MA_SUCCESS therefore frees the buffer out from under the job tail, which then reads and atomically increments freed (and possibly recycled) memory. The asynchronous free path is safe because it waits on the order / executionPointer ticket scheme; the synchronous shortcut bypassed it. Make the synchronous path wait for executionPointer to catch up to executionCounter before tearing down, so no job is still referencing the buffer. Caught by ThreadSanitizer. --- miniaudio.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/miniaudio.h b/miniaudio.h index cd2b0772..b76c2fad 100644 --- a/miniaudio.h +++ b/miniaudio.h @@ -71470,7 +71470,19 @@ MA_API ma_result ma_resource_manager_data_buffer_uninit(ma_resource_manager_data } if (ma_resource_manager_data_buffer_result(pDataBuffer) == MA_SUCCESS) { - /* The data buffer can be deleted synchronously. */ + /* + The data buffer can be deleted synchronously. Before doing so we must wait for any + in-flight load job to fully retire. The load job publishes its result code (which is what + makes us take this branch) a few instructions before it finishes referencing pDataBuffer + and increments the execution pointer. Tearing the buffer down the instant we observe + MA_SUCCESS therefore races the tail of the load job, which then reads and writes freed + memory. Wait for the execution pointer to catch up to the execution counter so that no job + is still referencing the buffer, exactly as the asynchronous path relies on for ordering. + */ + while (ma_atomic_load_32(&pDataBuffer->executionPointer) != ma_atomic_load_32(&pDataBuffer->executionCounter)) { + ma_yield(); + } + return ma_resource_manager_data_buffer_uninit_internal(pDataBuffer); } else { /*