resource_manager: fix data buffer node teardown racing async page job tail

ma_resource_manager_data_buffer_node_unacquire frees the node synchronously
when it observes a non-MA_BUSY result (the "sound isn't loading" branch). But
a decoded-paged node publishes result == MA_SUCCESS as soon as it is playable,
while ma_job_process__resource_manager__page_data_buffer_node jobs keep running
to decode the remaining pages. Those jobs reference the node and increment its
executionPointer at their tail, several instructions after the result becomes
observable. A concurrent unacquire that sees a non-BUSY result therefore frees
the node out from under the page job tail, which then reads and atomically
increments freed (and possibly recycled) memory.

The node carries the same order / executionPointer ticket scheme as the data
buffer, and the MA_BUSY branch already relies on it (it posts an ordered
FREE_DATA_BUFFER_NODE job). Only the synchronous shortcut skipped it. Make it
wait for executionPointer to catch up to executionCounter before freeing, so no
job is still referencing the node -- the same fix as
ma_resource_manager_data_buffer_uninit for the buffer level, one level down.
Caught by ThreadSanitizer.
This commit is contained in:
Steven Noonan
2026-07-18 22:13:16 -07:00
committed by David Reid
parent e93ec1d340
commit f73efd2d08
+14 -1
View File
@@ -71164,7 +71164,20 @@ stage2:
/* Threading is enabled. The job queue will deal with the rest of the cleanup from here. */
}
} else {
/* The sound isn't loading so we can just free the node here. */
/*
The node isn't loading, so it can be freed synchronously here. But first wait for any
in-flight job to fully retire. A decoded-paged node reports MA_SUCCESS as soon as it is
playable, while page_data_buffer_node jobs keep decoding the remaining pages and only
increment the execution pointer at their tail. Freeing the node the instant we observe a
non-BUSY result therefore races the tail of such a job, which then reads/writes freed
memory. Wait for the execution pointer to catch up to the execution counter so no job is
still referencing the node, exactly as the asynchronous free path relies on for ordering.
(Mirrors the data-buffer fix in ma_resource_manager_data_buffer_uninit.)
*/
while (ma_atomic_load_32(&pDataBufferNode->executionPointer) != ma_atomic_load_32(&pDataBufferNode->executionCounter)) {
ma_yield();
}
ma_resource_manager_data_buffer_node_free(pResourceManager, pDataBufferNode);
}
}