CVE-2026-8549
Overview
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
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.