High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Sub-componentMedia
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497985088
Fix commita833fdedacd9 (chromium/src) +16/-18
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-12
Patch gap40 days — fix public in source before the advisory

Background

`MediaFoundationStreamWrapper`
Windows media-renderer object that adapts a Chromium DemuxerStream to a Media Foundation (MF) stream, accessed from both the Chromium media thread and MF threadpool threads.
`demuxer_stream_`
A pointer to the underlying DemuxerStream that 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.

Key insight
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

  1. Encrypted/MF playback Media playback routed through the Media Foundation renderer creates a MediaFoundationStreamWrapper whose IsEncrypted() is queried from MF threadpool threads. (web-reachable trigger via crafted media is inferred)
  2. Concurrent teardown The Chromium media thread begins teardown and calls DetachDemuxerStream, setting demuxer_stream_ = nullptr without holding lock_ (pre-patch).
  3. Racing dereference An MF threadpool thread runs the old IsEncrypted() and dereferences demuxer_stream_ concurrently, racing the null-out/free.
  4. 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

Per the vendor advisory this is a high-severity use-after-free in Media: a cross-thread data race on 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.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

Audit Directions

  • Members read on one thread, mutated on another
    Audit 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 state
    Look for const accessors that dereference a raw pointer which teardown can null, and prefer caching an immutable snapshot under lock.
  • Lock-vs-deadlock trade-offs
    Review paths that call a getter while already holding lock_ (e.g. OnDemuxerStreamRead → ReportEncryptionType); ensure cached state is used instead of recursive locking, and that GUARDED_BY annotations cover every access.
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;
 };
Loading diff…

Original Bug Report

reported by [email protected]

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