CVE-2026-8549
Overview
Background
- `MediaFoundationStreamWrapper`
- Windows media-renderer object that adapts a Chromium
DemuxerStreamto a Media Foundation (MF) stream, accessed from both the Chromium media thread and MF threadpool threads. - `demuxer_stream_`
- A pointer to the underlying
DemuxerStreamthat the wrapper dereferences to read decoder config/encryption state. - `DetachDemuxerStream`
- Teardown method on the Chromium media thread that nulls out
demuxer_stream_. - BackupRefPtr (BRP)
- A Chromium pointer-protection mechanism that can be bypassed by a genuine cross-thread data race, where one thread frees/nulls while another reads unsynchronized.
Root Cause Analysis
The virtual IsEncrypted() overrides in MediaFoundationAudioStream and MediaFoundationVideoStream dereferenced demuxer_stream_ (via audio_decoder_config()/video_decoder_config()) with no lock, and these were invoked from MF threadpool threads. Concurrently, DetachDemuxerStream on the Chromium media thread set demuxer_stream_ = nullptr during teardown, also without holding lock_. This unsynchronized read/write of demuxer_stream_ is a cross-thread data race that, per the commit, could bypass BackupRefPtr protection and become a use-after-free when the MF thread dereferenced a pointer being torn down.
The fix removes the per-subclass IsEncrypted() dereferences entirely and instead computes the encryption state once, under lock_, during initialization into a new is_encrypted_ member; MediaFoundationStreamWrapper::IsEncrypted() now returns that cached bool, DetachDemuxerStream nulls demuxer_stream_ under lock_, and lock_ is made mutable so the const getter can participate. Reading the cached is_encrypted_ on internal paths avoids re-locking and the self-deadlock that a naive lock in IsEncrypted would have caused, since the encrypted state does not change.
demuxer_stream_ was read without synchronization on MF threads while the media thread nulled it during teardown, a data race that could defeat BackupRefPtr and cause a UAF; the fix snapshots the encryption flag into is_encrypted_ under lock_ and serves IsEncrypted() from that cached value instead of re-dereferencing the pointer.Attack Path
- Encrypted/MF playback
Media playback routed through the Media Foundation renderer creates a
MediaFoundationStreamWrapperwhoseIsEncrypted()is queried from MF threadpool threads. (web-reachable trigger via crafted media is inferred) - Concurrent teardown
The Chromium media thread begins teardown and calls
DetachDemuxerStream, settingdemuxer_stream_ = nullptrwithout holdinglock_(pre-patch). - Racing dereference
An MF threadpool thread runs the old
IsEncrypted()and dereferencesdemuxer_stream_concurrently, racing the null-out/free. - UAF under race The unsynchronized access bypasses BackupRefPtr and dereferences a pointer whose target is being destroyed, producing a use-after-free (as stated in the commit message).
Impact Assessment
demuxer_stream_ between MF threadpool threads and the Chromium media thread can cause a UAF in the renderer/media stack during encrypted or MF-routed playback on Windows. Exploitation into a controlled primitive or code execution is not demonstrated by the diff and is inferred; the advisory does not report in-the-wild exploitation.Files Changed
media/renderers/win/media_foundation_audio_stream.ccmedia/renderers/win/media_foundation_audio_stream.hmedia/renderers/win/media_foundation_stream_wrapper.ccmedia/renderers/win/media_foundation_stream_wrapper.hmedia/renderers/win/media_foundation_video_stream.ccmedia/renderers/win/media_foundation_video_stream.h
Audit Directions
- Members read on one thread, mutated on anotherAudit wrapper/adapter classes accessed by both MF/OS threadpool threads and the Chromium media thread for pointer members touched without
lock_/GUARDED_BY. - Const getters that dereference mutable stateLook for
constaccessors that dereference a raw pointer which teardown can null, and prefer caching an immutable snapshot under lock. - Lock-vs-deadlock trade-offsReview paths that call a getter while already holding
lock_(e.g.OnDemuxerStreamRead→ReportEncryptionType); ensure cached state is used instead of recursive locking, and thatGUARDED_BYannotations cover every access.
Patch
From a833fdedacd97fdb48c7c849e43c2d487cdf7ed7 Mon Sep 17 00:00:00 2001 From: Sangbaek Park <[email protected]> Date: Wed, 01 Apr 2026 17:36:52 -0700 Subject: [PATCH] media: Fix cross-thread data race and deadlock in stream wrapper This CL fixes a potential Use-After-Free (UAF) vulnerability caused by a cross-thread data race in MediaFoundationStreamWrapper, as well as a subsequent self-deadlock issue introduced by the initial synchronization attempt. Previously, `demuxer_stream_` was accessed by Media Foundation threadpool threads via `IsEncrypted()` without synchronization. Concurrently, the Chromium media thread could null it out during teardown in `DetachDemuxerStream()`. This race condition could bypass BackupRefPtr protections and lead to a UAF when dereferencing the pointer to check the decoder config. To fix this, `demuxer_stream_` accesses are now safely protected by `lock_`. To avoid non-recursive mutex self-deadlocks when internal paths (like `OnDemuxerStreamRead` -> `ReportEncryptionType`) check the stored encryption state instead of lock since the encrypted state won't change. Bug: 497985088 Change-Id: Ibf5168a8f65a9eb7d80da55165866d0c8721953c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7719604 Reviewed-by: Dale Curtis <[email protected]> Commit-Queue: Sangbaek Park <[email protected]> Cr-Commit-Position: refs/heads/main@{#1608895} --- diff --git a/media/renderers/win/media_foundation_audio_stream.cc b/media/renderers/win/media_foundation_audio_stream.cc index 2e8f1b3..59ff57e07 100644 --- a/media/renderers/win/media_foundation_audio_stream.cc +++ b/media/renderers/win/media_foundation_audio_stream.cc @@ -57,11 +57,6 @@ return S_OK; } -bool MediaFoundationAudioStream::IsEncrypted() const { - AudioDecoderConfig audio_config = demuxer_stream_->audio_decoder_config(); - return audio_config.is_encrypted(); -} - HRESULT MediaFoundationAudioStream::GetMediaType( IMFMediaType** media_type_out) { AudioDecoderConfig decoder_config = demuxer_stream_->audio_decoder_config(); diff --git a/media/renderers/win/media_foundation_audio_stream.h b/media/renderers/win/media_foundation_audio_stream.h index 2608074..25affcb2 100644 --- a/media/renderers/win/media_foundation_audio_stream.h +++ b/media/renderers/win/media_foundation_audio_stream.h @@ -25,7 +25,6 @@ DemuxerStream* demuxer_stream, std::unique_ptr<MediaLog> media_log, MediaFoundationStreamWrapper** stream_out); - bool IsEncrypted() const override; HRESULT GetMediaType(IMFMediaType** media_type_out) override; }; diff --git a/media/renderers/win/media_foundation_stream_wrapper.cc b/media/renderers/win/media_foundation_stream_wrapper.cc index fd7189e6..18bb9c19 100644 --- a/media/renderers/win/media_foundation_stream_wrapper.cc +++ b/media/renderers/win/media_foundation_stream_wrapper.cc @@ -79,13 +79,17 @@ { base::AutoLock auto_lock(lock_); parent_source_ = parent_source; + demuxer_stream_ = demuxer_stream; } - demuxer_stream_ = demuxer_stream; stream_id_ = stream_id; - stream_type_ = demuxer_stream_->type(); + stream_type_ = demuxer_stream->type(); + is_encrypted_ = (stream_type_ == DemuxerStream::Type::VIDEO) + ? demuxer_stream->video_decoder_config().is_encrypted() + : demuxer_stream->audio_decoder_config().is_encrypted(); DVLOG_FUNC(1) << "stream_id=" << stream_id - << ", stream_type=" << DemuxerStream::GetTypeName(stream_type_); + << ", stream_type=" << DemuxerStream::GetTypeName(stream_type_) + << ", is_encrypted=" << is_encrypted_; media_log_ = std::move(media_log); if (base::FeatureList::IsEnabled(kMediaFoundationBatchRead)) { @@ -127,6 +131,7 @@ DVLOG_FUNC(1); DCHECK(task_runner_->RunsTasksInCurrentSequence()); + base::AutoLock auto_lock(lock_); demuxer_stream_ = nullptr; } @@ -643,6 +648,10 @@ return true; } +bool MediaFoundationStreamWrapper::IsEncrypted() const { + return is_encrypted_; +} + GUID MediaFoundationStreamWrapper::GetLastKeyId() const { return last_key_id_; } diff --git a/media/renderers/win/media_foundation_stream_wrapper.h b/media/renderers/win/media_foundation_stream_wrapper.h index 39c4956..a60e60ff 100644 --- a/media/renderers/win/media_foundation_stream_wrapper.h +++ b/media/renderers/win/media_foundation_stream_wrapper.h @@ -79,7 +79,7 @@ // TODO: revisting inheritance and potentially replacing it with composition. // The stream is encrypted or not. - virtual bool IsEncrypted() const = 0; + bool IsEncrypted() const; // Let derived class to adjust the IMFSample if necessary. virtual HRESULT TransformSample(Microsoft::WRL::ComPtr<IMFSample>& sample); // Allow derived class to tell us if we can send MEStreamFormatChanged to MF. @@ -145,7 +145,7 @@ // Need exclusive access to some members between calls from MF threadpool // thread and calling thread from Chromium media stack. - base::Lock lock_; + mutable base::Lock lock_; // Indicates whether the stream is selected in the MF pipeline. bool selected_ GUARDED_BY(lock_) = false; @@ -209,6 +209,8 @@ bool encryption_type_reported_ = false; + bool is_encrypted_ = false; + // NOTE: Weak pointers must be invalidated before all other member variables. base::WeakPtrFactory<MediaFoundationStreamWrapper> weak_factory_{this}; }; diff --git a/media/renderers/win/media_foundation_video_stream.cc b/media/renderers/win/media_foundation_video_stream.cc index 4f48ea66..1bb7d27 100644 --- a/media/renderers/win/media_foundation_video_stream.cc +++ b/media/renderers/win/media_foundation_video_stream.cc @@ -352,11 +352,6 @@ return S_OK; } -bool MediaFoundationVideoStream::IsEncrypted() const { - VideoDecoderConfig decoder_config = demuxer_stream_->video_decoder_config(); - return decoder_config.is_encrypted(); -} - HRESULT MediaFoundationVideoStream::GetMediaType( IMFMediaType** media_type_out) { VideoDecoderConfig decoder_config = demuxer_stream_->video_decoder_config(); diff --git a/media/renderers/win/media_foundation_video_stream.h b/media/renderers/win/media_foundation_video_stream.h index a356c03..dceb1ca 100644 --- a/media/renderers/win/media_foundation_video_stream.h +++ b/media/renderers/win/media_foundation_video_stream.h @@ -23,8 +23,6 @@ std::unique_ptr<MediaLog> media_log, MediaFoundationStreamWrapper** stream_out); - bool IsEncrypted() const override; - protected: HRESULT GetMediaType(IMFMediaType** media_type_out) override; };
Original Bug Report
Potential UAF via cross-thread data race on demuxer_stream_ in MediaFoundationStreamWrapper
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A cross-thread data race exists in MediaFoundationStreamWrapper where a Media Foundation threadpool thread can access the demuxer_stream_ raw_ptr while the media thread concurrently nulls it during teardown. This can bypass BackupRefPtr protections and lead to a Use-After-Free and potential Remote Code Execution in the MediaFoundationCdm utility process.
Affected files:
media/renderers/win/media_foundation_stream_wrapper.hmedia/renderers/win/media_foundation_stream_wrapper.ccmedia/renderers/win/media_foundation_audio_stream.ccmedia/renderers/win/media_foundation_video_stream.ccmedia/renderers/win/media_foundation_source_wrapper.ccmedia/renderers/win/media_foundation_renderer.ccmedia/mojo/services/mojo_renderer_service.h
Estimated timestamp from git blame: 2021-11-27
Description
A potential cross-thread data race and Use-After-Free (UAF) vulnerability exists in the Media Foundation rendering pipeline in Chrome for Windows. The vulnerability is caused by a lack of synchronization when accessing demuxer_stream_ (a raw_ptr<DemuxerStream>) in MediaFoundationStreamWrapper. It is accessed by both the Chromium media thread during teardown and a Media Foundation threadpool thread during media playback validation.
Technical Details
-
Data Race on
demuxer_stream_: InMediaFoundationStreamWrapper, the memberdemuxer_stream_is not protected by a lock when accessed or modified in certain paths.- MF Threadpool: The Media Foundation framework invokes
IMFTrustedInput::GetInputTrustAuthority()on an internal threadpool thread. This callsIsEncrypted(), which dereferencesdemuxer_stream_to call virtual methods likeaudio_decoder_config()(media_foundation_audio_stream.cc:64). - Media Thread: During renderer shutdown,
MediaFoundationStreamWrapper::DetachDemuxerStream()nulls thedemuxer_stream_pointer (media_foundation_stream_wrapper.cc:130). This happens without acquiringlock_.
- MF Threadpool: The Media Foundation framework invokes
-
BackupRefPtr (BRP) Bypass & UAF: In
MojoRendererService(mojo_renderer_service.h),renderer_is declared aftermedia_resource_. Due to C++ destruction rules,renderer_is destroyed first.- As
renderer_shuts down, it callsDetachDemuxerStream(), which nulls theraw_ptr, dropping its BRP refcount to zero. - If the MF thread extracts the underlying raw pointer via
operator->just before the media thread nulls it, the MF thread holds a dangling raw pointer. - After
renderer_is destroyed,MojoRendererServicedestroysmedia_resource_, which frees the underlyingMojoDemuxerStreamAdapter. Because theraw_ptrwas already nulled, PartitionAlloc does not quarantine the memory. - The MF thread then uses its extracted raw pointer to make a virtual call (
audio_decoder_config()), hitting the freed memory.
- As
Potential Attack Steps
(Note: These are suggested steps based on code analysis; a working exploit has not been fully verified.)
- An attacker serves a page with a
<video>element using Encrypted Media Extensions (EME) to select the hardware-secure Media Foundation renderer. - As the protected media pipeline sets up, the Media Foundation threadpool repeatedly invokes
GetInputTrustAuthority(). - The attacker rapidly triggers a renderer teardown (e.g., by navigating away or removing the video element) to initiate a race between the media thread’s shutdown sequence and the concurrent MF threadpool activity.
- The MF thread reads the raw pointer from
demuxer_stream_but is preempted before making the virtual call. - The media thread nulls
demuxer_stream_and frees the underlyingMojoDemuxerStreamAdapter. - The attacker shapes the heap in the utility process via concurrent Mojo IPCs to replace the freed memory with a fake object and vtable.
- The MF thread resumes, dereferences the dangling pointer, and calls the forged vtable, leading to RCE in the sandboxed
MediaFoundationCdmutility process.
Suggested Fix
- Protect
demuxer_stream_access withlock_insideMediaFoundationStreamWrapper(e.g., inDetachDemuxerStream()andIsEncrypted()). - Ensure that any accesses to the demuxer stream on the MF thread either hold the lock for the duration of the call or that the MF threadpool is guaranteed to be fully drained/joined before the stream is detached and the underlying resource is destroyed.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.