Skip to content

Fix use after free in producer_avformat's cache-miss path - #1330

Open
unicorntearsproject wants to merge 2 commits into
mltframework:masterfrom
unicorntearsproject:fix-avformat-cache-miss-race
Open

unicorntearsproject wants to merge 2 commits into
mltframework:masterfrom
unicorntearsproject:fix-avformat-cache-miss-race

Conversation

@unicorntearsproject

Copy link
Copy Markdown
Contributor

Fixes #1318.

In producer_get_frame(), on a cache miss the state is created, mlt_service_cache_put() into the cache, then fetched again to take a reference. The cache holds only 4 avformat states by default, so with more producers decoding on several threads another thread's put can evict the new entry in between. Nobody holds a reference yet, so its destructor runs and the state is freed, but the code carried on with the pointer it had put, and stored a NULL cache item on the frame. This now uses the state the cache returns (with our reference) and creates it again if it has already gone.

My description in the issue (that an entry in use is evicted) was imprecise: held entries are protected by their reference count. The window is only between the put and the get.

Tested on master 89fbe37: repro (16 threads, each with its own avformat producer, 150 frames of video and audio, default cache size): 30 of 30 runs crashed in producer_set_up_video() before, 0 of 30 after. clang-format clean.

🤖 Generated with Claude Code

producer_get_frame() creates the producer's state on a cache miss,
puts it in the cache, then fetches it again to take a reference. The
cache holds only a few avformat states at once (4 by default), so with
more producers decoding on several threads, another thread's put can
evict the new entry in between. Nobody holds a reference yet, so its
destructor runs and the state is freed, but the code carried on with the
pointer it had put, and also stored a NULL cache item on the frame.

Use the state the cache returns, with our reference, and create it again
if it has already gone.

Sixteen threads, each with its own avformat producer of the same clip
reading 150 frames of video and audio (cache size 4): 30 of 30 runs
crashed in producer_set_up_video() on master, 0 of 30 with this change.
The crashing state was a freshly zeroed one belonging to another
producer.

Fixes mltframework#1318.
@ddennedy

ddennedy commented Oct 1, 2026

Copy link
Copy Markdown
Member

This is considered a critical fix and low risk change suitable for the immediate release. Thanks

@ddennedy ddennedy added this to the v7.42.0 milestone Oct 1, 2026
@ddennedy
ddennedy requested a lite review from Copilot October 1, 2026 18:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address the critical mutex-initialization issue and both moderate cache/re-registration issues.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Fixes a use-after-free race in producer_avformat cache-miss handling by reacquiring cache-owned state and retrying after eviction.

Changes:

  • Recreates state when eviction occurs before reacquisition.
  • Uses the cache-returned reference during frame setup.
  • Adds comments describing the race window.
File Description and review findings
src/​modules/​avformat/​producer_avformat.c Updates cache-miss handling. Critical (4 votes): mutexes may be used before initialization when the state is published. Moderate (1 vote): retry loop can spin indefinitely with a zero-sized cache. Moderate (1 vote): recreated states are not re-registered with property_changed.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/modules/avformat/producer_avformat.c
@ddennedy

ddennedy commented Oct 3, 2026

Copy link
Copy Markdown
Member

Moving to the release after next since this requires a little more invasive change per the legitimate review comment.

@ddennedy ddennedy modified the milestones: v7.42.0, v7.44.0 Oct 3, 2026
@unicorntearsproject

unicorntearsproject commented Oct 3, 2026 via email

Copy link
Copy Markdown
Contributor Author

@ddennedy ddennedy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See review comment.

As raised in review: on a cache miss the state was put in the cache with
its mutexes still zeroed. If it was evicted before we took our reference,
or used by producer_set_up_video() before producer_open() ran, a mutex
was locked before it was initialised (and producer_open() then
initialised it while it was locked).

- Move the mutex setup into init_mutexes() (it was written out twice) and
  call it before the state is put in the cache.
- Try the cache miss path at most 10 times, so a cache that will not keep
  the state (a size of 0) cannot make it loop forever; return an empty
  frame and an error then, as the caller expects a frame.
@unicorntearsproject

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed a follow-up:

  • the mutexes are now initialised (init_mutexes(), which replaces two copies of the same code) before the state is put in the cache, so an eviction or an early producer_set_up_video() can't lock an uninitialised mutex;
  • the cache-miss path tries at most 10 times, and with a cache size of 0 returns an empty frame and an error instead of looping or crashing.

Re-checked: the 16-thread test passes 30 of 30 (it crashed 30 of 30 before the fix), a normal render works, and cache sizes 0 and 1 both finish.

Not changed: the recreated state is not re-registered with property_changed. The listener already holds the first state's pointer, so that looks like a separate, older problem with evicted states; I'm happy to look at it in its own PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A zero-sized cache still causes an out-of-bounds write before the new fallback executes.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment on lines +4477 to +4480
if (!self) {
// The cache would not keep the state (a cache size of 0, or no memory).
// The caller expects a frame even on failure.
*frame = mlt_frame_init(service);

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.

Crash: avformat decoder cache (size 4) evicts state another thread is still decoding with

3 participants