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

Overview

High
Severity
9.6
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.
Tracker537832446
Fix commit38acc8b39421 (chromium/src) +25/-19
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-22 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-06
Patch gap14 days — fix public in source before the advisory

Background

`SharedImageReadLock`
a COM object (created with Microsoft::WRL::Make) in media/base/win/mf_helpers that keeps a gpu::VideoImageRepresentation and its ScopedReadAccess alive while a GPU video frame’s D3D11 texture is being consumed. It can be attached to an IMFSample handed to Media Foundation, so its lifetime is controlled by COM reference counting.
`gpu::MemoryTypeTracker`
a per-client accounting helper that a shared-image representation is given at ProduceVideo time and uses for memory tracking during its lifetime. It wraps a ref-counted gpu::MemoryTracker, but is not itself ref-counted.
`CommandBufferHelper::GetMemoryTypeTracker`
returns a raw pointer to a MemoryTypeTracker owned by the CommandBufferHelper. Before the patch, this was the tracker passed to ProduceVideo.

Root Cause Analysis

In the Windows hardware-encode input path, GenerateResourceOnSyncTokenReleased (in mf_helpers.cc) and GenerateResourceOnSynTokenReleased (in d3d12_video_encode_accelerator.cc) produced a VideoImageRepresentation with command_buffer_helper->GetMemoryTypeTracker(). The hunks do not name these functions, but they are the enclosing functions in the source. The representation keeps that raw pointer and was then moved into a SharedImageReadLock. The lock can be passed on to an IMFSample, so it, and the representation inside it, can outlive the CommandBufferHelper that owns the tracker. When the lock is finally released, ~SharedImageReadLock posts DestroySharedImageResourcesOnGpuThread, which destroys the representation. If the helper and its tracker are already gone, that teardown touches a freed MemoryTypeTracker; the commit describes this as a UAF “based on unexpected destruction orders”.

The fix makes each read lock own its own std::unique_ptr<gpu::MemoryTypeTracker>, constructed around a base::WrapRefCounted reference to the stub’s MemoryTracker. That tracker is passed to ProduceVideo and moved into the lock, and DestroySharedImageResourcesOnGpuThread resets it only after scoped_read_access and representation have been destroyed.

Key insight
A representation whose lifetime is unbounded, because it is held through a COM IMFSample, borrowed a raw MemoryTypeTracker* from a shorter-lived owner. The fix gives the lock its own tracker, which keeps the underlying ref-counted MemoryTracker alive, and destroys it last.

Attack Path

  1. Start hardware video encoding on Windows Content drives an encode through the Media Foundation path (mf_helpers.cc) or the D3D12 encoder (d3d12_video_encode_accelerator.cc) using GPU-backed VideoFrames with shared images. That an ordinary web API such as WebCodecs, WebRTC or MediaRecorder reaches this is (inferred).
  2. Create the read lock When the frame’s sync token is released, the GPU-side helper produces a video representation tied to the CommandBufferHelper’s tracker and wraps it in a SharedImageReadLock, which may be attached to an IMFSample.
  3. Tear down out of order The encoder, command buffer or stub is destroyed (for example by closing the encoder or the page) while Media Foundation or the encoder still holds the sample or lock, which frees the CommandBufferHelper’s MemoryTypeTracker (inferred timing).
  4. Use after free on release The final release runs DestroySharedImageResourcesOnGpuThread, and destroying the representation dereferences the stale tracker pointer in the GPU process.

Impact Assessment

The use-after-free is in the GPU process and is limited to Windows builds that use the Media Foundation or D3D12 hardware video encoder with shared-image input frames. Reaching it depends on a destruction-order race between the encoder’s or command buffer’s teardown and the release of the IMFSample or lock. Whether unprivileged web content can reliably steer that ordering, and whether the freed tracker gives a corruption primitive leading to GPU-process code execution, are (inferred) and not shown by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.

Changed Functions

FunctionChangeNotes
if
media/base/win/mf_helpers.cc
modified
IMFMediaType
media/base/win/mf_helpers.h
modified
MemoryTypeTracker
media/base/win/mf_helpers.h
modified
SharedContextState
media/base/win/mf_helpers.h
modified
if
media/gpu/windows/d3d12_video_encode_accelerator.cc
modified

Files Changed

  • media/gpu/windows/d3d12_video_encode_accelerator.cc
  • media/base/win/mf_helpers.cc
  • media/base/win/mf_helpers.h

Audit Directions

  • Raw tracker pointers in representations
    Find every ProduceVideo/Produce* call that passes GetMemoryTypeTracker() from a helper or stub, especially those whose representation is handed to COM, platform APIs, or posted tasks (media/gpu/windows, media/gpu/mac, media/mojo/services). Confirm that the tracker owner strictly outlives the representation.
  • COM/WRL objects wrapping Chromium-owned state
    Look at Microsoft::WRL::Make objects and IMFSample/IMFMediaBuffer attachments that hold unique_ptrs or raw pointers to GPU-side objects. Their release is driven by platform reference counts and can occur after the Chromium owner has been torn down.
  • Deferred destruction tasks
    Review PostTask destructors such as DestroySharedImageResourcesOnGpuThread for objects whose dependencies (trackers, context state, helpers) are not moved into the task. Check the reset order inside it.
From 38acc8b39421655426c218609986023ad483c241 Mon Sep 17 00:00:00 2001
From: Saifuddin Hitawala <hitawala@chromium.org>
Date: Thu, 23 Jul 2026 12:29:16 -0700
Subject: [PATCH] [media] Keep ref on MemoryTypeTracker in SharedImageReadLock

Add MemoryTypeTracker ref on SharedImageReadLock to avoid UAF based
on unexpected destruction orders, as the lock can be passed to
IMFSample. We will eventually get rid of MemoryTypeTracker in
SharedImage to instead use MemoryTracker directly which is
RefCountedThreadSafe and should avoid future issues.

Note that this does not pass CommandBufferHelper as we want to get
rid of it as well eventually.

Bug: 537832446
Change-Id: I048a2e20527748c0e169d6b285d0cab5d6c22fa9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8139742
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Commit-Queue: Saifuddin Hitawala <hitawala@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1667317}
---
diff --git a/media/base/win/mf_helpers.cc b/media/base/win/mf_helpers.cc
index 4e3cfa2..00e115a 100644
--- a/media/base/win/mf_helpers.cc
+++ b/media/base/win/mf_helpers.cc
@@ -51,17 +51,16 @@
     std::unique_ptr<gpu::VideoImageRepresentation> representation,
     std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
         scoped_read_access,
-    scoped_refptr<gpu::SharedContextState> context_state) {
-  if (context_state && context_state->MakeCurrent(nullptr, /*needs_gl=*/true)) {
-    scoped_read_access.reset();
-    representation.reset();
-  } else {
-    if (representation) {
-      representation->OnContextLost();
-    }
-    scoped_read_access.reset();
-    representation.reset();
+    scoped_refptr<gpu::SharedContextState> context_state,
+    std::unique_ptr<gpu::MemoryTypeTracker> tracker) {
+  const bool context_current =
+      context_state && context_state->MakeCurrent(nullptr, /*needs_gl=*/true);
+  if (representation && !context_current) {
+    representation->OnContextLost();
   }
+  scoped_read_access.reset();
+  representation.reset();
+  tracker.reset();
 }
 
 }  // namespace
@@ -71,11 +70,13 @@
     std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
         scoped_read_access,
     scoped_refptr<VideoFrame> frame,
-    scoped_refptr<gpu::SharedContextState> context_state)
+    scoped_refptr<gpu::SharedContextState> context_state,
+    std::unique_ptr<gpu::MemoryTypeTracker> tracker)
     : representation_(std::move(representation)),
       scoped_read_access_(std::move(scoped_read_access)),
       frame_(std::move(frame)),
       context_state_(std::move(context_state)),
+      tracker_(std::move(tracker)),
       task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
 
 SharedImageReadLock::~SharedImageReadLock() {
@@ -83,7 +84,7 @@
       FROM_HERE,
       base::BindOnce(&DestroySharedImageResourcesOnGpuThread,
                      std::move(representation_), std::move(scoped_read_access_),
-                     std::move(context_state_)));
+                     std::move(context_state_), std::move(tracker_)));
 }
 
 using Microsoft::WRL::ComPtr;
@@ -1134,10 +1135,11 @@
   bool use_same_device = (encoder_device.Get() == shared_d3d11_device.Get());
   gpu::SharedImageManager* shared_image_manager =
       command_buffer_helper->GetSharedImageManager();
+  auto tracker = std::make_unique<gpu::MemoryTypeTracker>(
+      base::WrapRefCounted(shared_image_stub->memory_tracker()));
   std::unique_ptr<gpu::VideoImageRepresentation> image_representation =
       shared_image_manager->ProduceVideo(
-          shared_d3d11_device, frame->shared_image()->mailbox(),
-          command_buffer_helper->GetMemoryTypeTracker());
+          shared_d3d11_device, frame->shared_image()->mailbox(), tracker.get());
   RETURN_ON_FAILURE_WITH_CALLBACK(image_representation ? S_OK : E_FAIL,
                                   "Failed to produce video");
 
@@ -1152,7 +1154,7 @@
   ComPtr<SharedImageReadLock> si_lock =
       Microsoft::WRL::Make<SharedImageReadLock>(
           std::move(image_representation), std::move(scoped_read_access), frame,
-          std::move(shared_context_state));
+          std::move(shared_context_state), std::move(tracker));
   if (!si_lock) {
     RETURN_ON_FAILURE_WITH_CALLBACK(E_OUTOFMEMORY,
                                     "Failed to create SharedImageReadLock");
diff --git a/media/base/win/mf_helpers.h b/media/base/win/mf_helpers.h
index a5a2a42..73db391f 100644
--- a/media/base/win/mf_helpers.h
+++ b/media/base/win/mf_helpers.h
@@ -33,6 +33,7 @@
 class IMFMediaType;
 
 namespace gpu {
+class MemoryTypeTracker;
 class SharedContextState;
 }  // namespace gpu
 
@@ -261,7 +262,8 @@
       std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
           scoped_read_access,
       scoped_refptr<VideoFrame> frame,
-      scoped_refptr<gpu::SharedContextState> context_state);
+      scoped_refptr<gpu::SharedContextState> context_state,
+      std::unique_ptr<gpu::MemoryTypeTracker> tracker);
 
   gpu::VideoImageRepresentation::ScopedReadAccess* access() const {
     return scoped_read_access_.get();
@@ -275,6 +277,7 @@
       scoped_read_access_;
   scoped_refptr<VideoFrame> frame_;
   scoped_refptr<gpu::SharedContextState> context_state_;
+  std::unique_ptr<gpu::MemoryTypeTracker> tracker_;
   scoped_refptr<base::SequencedTaskRunner> task_runner_;
 };
 
diff --git a/media/gpu/windows/d3d12_video_encode_accelerator.cc b/media/gpu/windows/d3d12_video_encode_accelerator.cc
index e7b6bae..1762d35 100644
--- a/media/gpu/windows/d3d12_video_encode_accelerator.cc
+++ b/media/gpu/windows/d3d12_video_encode_accelerator.cc
@@ -149,10 +149,11 @@
 
   gpu::SharedImageManager* shared_image_manager =
       command_buffer_helper->GetSharedImageManager();
+  auto tracker = std::make_unique<gpu::MemoryTypeTracker>(
+      base::WrapRefCounted(shared_image_stub->memory_tracker()));
   std::unique_ptr<gpu::VideoImageRepresentation> representation =
       shared_image_manager->ProduceVideo(
-          d3d11_device, frame->shared_image()->mailbox(),
-          command_buffer_helper->GetMemoryTypeTracker());
+          d3d11_device, frame->shared_image()->mailbox(), tracker.get());
   RETURN_ON_FAILURE_WITH_CALLBACK(representation ? S_OK : E_FAIL,
                                   "Failed to produce video");
 
@@ -167,7 +168,7 @@
   Microsoft::WRL::ComPtr<SharedImageReadLock> si_lock =
       Microsoft::WRL::Make<SharedImageReadLock>(
           std::move(representation), std::move(scoped_read_access), frame,
-          std::move(shared_context_state));
+          std::move(shared_context_state), std::move(tracker));
   if (!si_lock) {
     RETURN_ON_FAILURE_WITH_CALLBACK(E_OUTOFMEMORY,
                                     "Failed to create SharedImageReadLock");
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in GPU process via asynchronous release of SharedImageReadLock by Media Foundation

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the Windows GPU process when using MediaFoundationVideoEncodeAccelerator (MFVEA). SharedImageReadLock wraps a VideoImageRepresentation holding a raw pointer to a CommandBufferHelperImpl member but does not maintain a reference to keep the helper alive. If an asynchronous hardware encoder releases an IMFSample holding the lock after the encoder and its helper are destroyed, the lock’s destruction accesses freed memory on the GPU main thread.

Affected files:

  • media/base/win/mf_helpers.h
  • media/base/win/mf_helpers.cc
  • media/gpu/windows/media_foundation_video_encode_accelerator_win.cc

Estimated timestamp from git blame: 2026-04-24

Root Cause Analysis

In media/base/win/mf_helpers.cc, a SharedImageReadLock is constructed to manage the lifetime of a VideoImageRepresentation and its ScopedReadAccess. The VideoImageRepresentation holds a raw pointer (tracker_) to an inline MemoryTypeTracker member of a CommandBufferHelperImpl instance:

// media/base/win/mf_helpers.cc
std::unique_ptr<gpu::VideoImageRepresentation> image_representation =
    shared_image_manager->ProduceVideo(
        shared_d3d11_device, frame->shared_image()->mailbox(),
        command_buffer_helper->GetMemoryTypeTracker());

However, SharedImageReadLock does not hold a scoped_refptr<CommandBufferHelper> to guarantee the lifetime of the tracker.

When MFVEA populates input samples for the asynchronous Media Foundation Transform (HMFT), it attaches the SharedImageReadLock directly to the IMFSample COM attributes using SetUnknown with SharedImageReadLock::kSampleExtensionGUID and drops its local queue references. This hands ownership of the lock’s lifetime over to the Media Foundation driver and the OS-managed COM runtime graph.

Upon encoder destruction (MediaFoundationVideoEncodeAccelerator::Destroy()), MFVEA is deleted. No flush command is processed beforehand, which leaves active sample references inside the driver’s queued pipeline. The destruction of MFVEA’s command_buffer_helper_ member drops the last Chrome-side reference to CommandBufferHelperImpl, posting its deletion (~CommandBufferHelperImpl) to the GPU main thread.

Subsequently, when the HMFT completes and releases the IMFSample on an OS background thread, the COM reference count of the SharedImageReadLock drops to 0. Its destructor (~SharedImageReadLock()) runs on the background thread and posts DestroySharedImageResourcesOnGpuThread to the GPU main thread.

Because both deletion tasks are executed sequentially on the GPU main thread, the CommandBufferHelperImpl is deallocated first. The subsequent cleanup task then executes TrackMemFree on the dangling MemoryTypeTracker pointer, causing a Use-After-Free with a virtual call to TrackMemoryAllocatedChange on the freed memory space.

Potential Trigger Path

Note: These are potential steps that an attacker might follow; our analysis is based on static code tracing as we do not have a running PoC.

  1. From a sandboxed renderer on Windows, initiate hardware encoding via the mojom::VideoEncodeAcceleratorProvider interface.
  2. Submit a GPU-backed VideoFrame whose SharedImage is non-mappable, triggering the creation of a SharedImageReadLock wrapping the VideoImageRepresentation and its read-access token.
  3. The lock is attached to the input IMFSample via SetUnknown and handed off to the async HMFT via ProcessInput().
  4. Drop the local queue references in MFVEA via pending_input_queue_.pop_front() so that the lock is kept alive only by the OS-owned driver sample.
  5. Release the source VideoFrame mailbox reference in the renderer so that the representation wrapped by the lock becomes the owning reference (refs_[0]) in the SharedImageBacking.
  6. Close/destroy the encoder from the renderer. MFVEA’s Destroy() runs, deleting the object and posting ~CommandBufferHelperImpl to the GPU main thread.
  7. The async HMFT releases the IMFSample on a background thread after the helper deletion task has been queued, posting DestroySharedImageResourcesOnGpuThread to the GPU main thread.
  8. The GPU main thread executes the tasks: it deallocates CommandBufferHelperImpl (freeing its inline MemoryTypeTracker member) and then executes TrackMemFree on the dangling tracker pointer during representation cleanup, resulting in a virtual call on freed memory.

Suggested Fix

Modify SharedImageReadLock (media/base/win/mf_helpers.h and media/base/win/mf_helpers.cc) to capture and hold a scoped_refptr<CommandBufferHelper> member. Passing the helper into the lock’s constructor guarantees that the CommandBufferHelperImpl (and its inline MemoryTypeTracker) remains alive until all outstanding SharedImageReadLock instances are fully destroyed, preventing the raw tracker pointer from dangling.

Evaluated with Chrome root at commit: 5a99d0c5d2ec6c066f5131e7868ff637440cc3dc


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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