Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker497985088
Fix commita833fdedacd9 (chromium/src) +16/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Files Changed

  • media/renderers/win/media_foundation_audio_stream.cc
  • media/renderers/win/media_foundation_audio_stream.h
  • media/renderers/win/media_foundation_stream_wrapper.cc
  • media/renderers/win/media_foundation_stream_wrapper.h
  • media/renderers/win/media_foundation_video_stream.cc
  • media/renderers/win/media_foundation_video_stream.h
From a833fdedacd97fdb48c7c849e43c2d487cdf7ed7 Mon Sep 17 00:00:00 2001
From: Sangbaek Park <sangbaekpark@google.com>
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 <dalecurtis@chromium.org>
Commit-Queue: Sangbaek Park <sangbaekpark@chromium.org>
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;
 };
Loading diff…

Original Bug Report

reported by vm...@google.com

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.h
  • media/renderers/win/media_foundation_stream_wrapper.cc
  • media/renderers/win/media_foundation_audio_stream.cc
  • media/renderers/win/media_foundation_video_stream.cc
  • media/renderers/win/media_foundation_source_wrapper.cc
  • media/renderers/win/media_foundation_renderer.cc
  • media/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

  1. Data Race on demuxer_stream_: In MediaFoundationStreamWrapper, the member demuxer_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 calls IsEncrypted(), which dereferences demuxer_stream_ to call virtual methods like audio_decoder_config() (media_foundation_audio_stream.cc:64).
    • Media Thread: During renderer shutdown, MediaFoundationStreamWrapper::DetachDemuxerStream() nulls the demuxer_stream_ pointer (media_foundation_stream_wrapper.cc:130). This happens without acquiring lock_.
  2. BackupRefPtr (BRP) Bypass & UAF: In MojoRendererService (mojo_renderer_service.h), renderer_ is declared after media_resource_. Due to C++ destruction rules, renderer_ is destroyed first.

    • As renderer_ shuts down, it calls DetachDemuxerStream(), which nulls the raw_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, MojoRendererService destroys media_resource_, which frees the underlying MojoDemuxerStreamAdapter. Because the raw_ptr was 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.

Potential Attack Steps

(Note: These are suggested steps based on code analysis; a working exploit has not been fully verified.)

  1. An attacker serves a page with a <video> element using Encrypted Media Extensions (EME) to select the hardware-secure Media Foundation renderer.
  2. As the protected media pipeline sets up, the Media Foundation threadpool repeatedly invokes GetInputTrustAuthority().
  3. 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.
  4. The MF thread reads the raw pointer from demuxer_stream_ but is preempted before making the virtual call.
  5. The media thread nulls demuxer_stream_ and frees the underlying MojoDemuxerStreamAdapter.
  6. The attacker shapes the heap in the utility process via concurrent Mojo IPCs to replace the freed memory with a fake object and vtable.
  7. The MF thread resumes, dereferences the dangling pointer, and calls the forged vtable, leading to RCE in the sandboxed MediaFoundationCdm utility process.

Suggested Fix

  1. Protect demuxer_stream_ access with lock_ inside MediaFoundationStreamWrapper (e.g., in DetachDemuxerStream() and IsEncrypted()).
  2. 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.

View on issue tracker