8000 Resource Manager: Fix a use-after-free when freeing a node while it is still decoding by Ed-Fe · Pull Request #1198 · mackron/miniaudio · GitHub
[go: up one dir, main page]

Skip to content

Resource Manager: Fix a use-after-free when freeing a node while it is still decoding - #1198

Open
Ed-Fe wants to merge 1 commit into
mackron:devfrom
Ed-Fe:fix-rm-free-node-before-page-job
Open

Ed-Fe wants to merge 1 commit into
mackron:devfrom
Ed-Fe:fix-rm-free-node-before-page-job

Conversation

@Ed-Fe
@Ed-Fe Ed-Fe commented Nov 12, 2026
Copy link
Copy Markdown

Hi,

I use miniaudio in a game engine called NonVisual Gaming Toolkit (NVGT), in a private fork with my own modifications. This PR to the resource manager was made entirely by AI. Here is the problem:

When a decoded data buffer is uninitialized while its node is still paging, the node can be freed while a page job
for it is still queued.

Sequence:

  1. A PAGE_DATA_BUFFER_NODE job (order k-1) is decoding a page.
  2. The owner calls ma_resource_manager_data_source_uninit(). ma_resource_manager_data_buffer_node_unacquire() sees
    MA_BUSY, sets the node to MA_UNAVAILABLE and posts FREE_DATA_BUFFER_NODE with order k.
  3. The page job finishes its page successfully and posts its follow-up page job with order k+1.
  4. The free job runs and frees the node. The follow-up job then runs on freed memory, and its decoder is never
    uninitialized.

In practice the next node is often allocated at the same address. The stale job then advances the new node's
executionPointer past its executionCounter, and the wait added in 22fd206d
(while (executionPointer != executionCounter) ma_yield();) spins forever. Without that wait it is a silent
use-after-free and a leaked decoder.

The fix makes ma_job_process__resource_manager__free_data_buffer_node() check whether jobs were posted after it
(executionCounter != order + 1). If so, it re-posts itself with a fresh execution order and advances the execution
pointer, so the pending page job runs first (it sees MA_UNAVAILABLE, frees its decoder and finishes) and the free
always happens last. The node has already been removed, so no other thread can post jobs for it, and none of its
jobs can run concurrently with the free job, so the counter is stable at that point.

This PR also adds tests/resource_manager, registered with CTest. It loads, waits for and unloads the same 100 ms WAV
5000 times with MA_RESOURCE_MANAGER_DATA_SOURCE_FLAG_ASYNC, for decoded and encoded buffers and streams, with the
init or done fence, and with 1 and 4 job threads. A watchdog thread fails the test if an iteration doesn't complete
within 5 seconds instead of letting it hang. It builds with -std=c89 -Wall -Wextra -Wpedantic without warnings.

Results on Windows 11 (GCC 16.2, MinGW-w64), 30 runs each, 4 processes in parallel:

Hang or crash
dev (631161e4) 30 of 30 (6 of them segfaults), all in "decoded buffer, init fence, 4 job threads"
dev + this fix 0 of 30

This was found while investigating hangs reported by players of an audio game built on NVGT. The investigation,
the fix and the test were done by an AI assistant (Claude).

I release this contribution into the public domain / under MIT No Attribution, as in miniaudio's license.

🤖 Generated with Claude Code

…s still decoding.

A page job that is decoding when the node is freed posts its follow-up page job with an execution order after the free job, so the follow-up runs on the freed node (and owns the decoder, which leaked). When the next node is allocated at the same address its execution pointer is advanced past its execution counter and the synchronous teardown path spins forever. The free job now re-posts itself so that it always runs last. Adds a resource manager test that loads and unloads the same file asynchronously in a loop.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0