Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Media
DescriptionInappropriate implementation in Media
ComponentMedia
Bug ClassLogic Error
Tracker518121320
Fix commitcd10f7bb2401 (chromium/src) +54/-23
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
media/base/win/mf_helpers.cc
modified
IMFMediaType
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/base/win/mf_helpers.cc
  • media/base/win/mf_helpers.h
  • media/gpu/windows/d3d12_video_encode_accelerator.cc
From cd10f7bb2401dce469417205344cfb535d89dfb5 Mon Sep 17 00:00:00 2001
From: Dale Curtis <[email protected]>
Date: Wed, 03 Jun 2026 14:53:20 -0700
Subject: [PATCH] Ensure D3D11 resources are destroyed on the right context

R=hitawala

Fixed: 518121320
Change-Id: I03d04dea5b40279434664e67515871b42b7952e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7891350
Auto-Submit: Dale Curtis <[email protected]>
Commit-Queue: Vasiliy Telezhnikov <[email protected]>
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641240}
---

diff --git a/media/base/win/mf_helpers.cc b/media/base/win/mf_helpers.cc
index 2911c218..a5d937dc 100644
--- a/media/base/win/mf_helpers.cc
+++ b/media/base/win/mf_helpers.cc
@@ -45,20 +45,46 @@
 
 namespace media {
 
+namespace {
+
+void DestroySharedImageResourcesOnGpuThread(
+    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();
+  }
+}
+
+}  // namespace
+
 SharedImageReadLock::SharedImageReadLock(
     std::unique_ptr<gpu::VideoImageRepresentation> representation,
     std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
         scoped_read_access,
-    scoped_refptr<VideoFrame> frame)
-    : representation_(representation.release(),
-                      base::OnTaskRunnerDeleter(
-                          base::SequencedTaskRunner::GetCurrentDefault())),
-      scoped_read_access_(scoped_read_access.release(),
-                          base::OnTaskRunnerDeleter(
-                              base::SequencedTaskRunner::GetCurrentDefault())),
-      frame_(std::move(frame)) {}
+    scoped_refptr<VideoFrame> frame,
+    scoped_refptr<gpu::SharedContextState> context_state)
+    : representation_(std::move(representation)),
+      scoped_read_access_(std::move(scoped_read_access)),
+      frame_(std::move(frame)),
+      context_state_(std::move(context_state)),
+      task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
 
-SharedImageReadLock::~SharedImageReadLock() = default;
+SharedImageReadLock::~SharedImageReadLock() {
+  task_runner_->PostTask(
+      FROM_HERE,
+      base::BindOnce(&DestroySharedImageResourcesOnGpuThread,
+                     std::move(representation_), std::move(scoped_read_access_),
+                     std::move(context_state_)));
+}
 
 using Microsoft::WRL::ComPtr;
 using Microsoft::WRL::MakeAndInitialize;
@@ -1078,11 +1104,11 @@
     RETURN_ON_FAILURE_WITH_CALLBACK(E_FAIL, "Invalid shared image stub");
   }
 
-  if (!shared_image_stub->shared_context_state()) {
+  auto shared_context_state = shared_image_stub->shared_context_state();
+  if (!shared_context_state) {
     RETURN_ON_FAILURE_WITH_CALLBACK(E_FAIL, "Invalid shared context state");
   }
 
-  auto shared_context_state = shared_image_stub->shared_context_state();
   if (!shared_context_state->MakeCurrent(nullptr, /*needs_gl=*/true)) {
     RETURN_ON_FAILURE_WITH_CALLBACK(E_FAIL, "Failed to make context current");
   }
@@ -1110,9 +1136,9 @@
     RETURN_ON_FAILURE_WITH_CALLBACK(E_FAIL, "Failed to begin read access");
   }
   ComPtr<SharedImageReadLock> si_lock =
-      Microsoft::WRL::Make<SharedImageReadLock>(std::move(image_representation),
-                                                std::move(scoped_read_access),
-                                                frame);
+      Microsoft::WRL::Make<SharedImageReadLock>(
+          std::move(image_representation), std::move(scoped_read_access), frame,
+          std::move(shared_context_state));
   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 1a46dc0..a5a2a42 100644
--- a/media/base/win/mf_helpers.h
+++ b/media/base/win/mf_helpers.h
@@ -32,6 +32,10 @@
 struct ID3D11Device;
 class IMFMediaType;
 
+namespace gpu {
+class SharedContextState;
+}  // namespace gpu
+
 namespace media {
 
 // Helper function to print HRESULT to std::string.
@@ -256,7 +260,8 @@
       std::unique_ptr<gpu::VideoImageRepresentation> representation,
       std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
           scoped_read_access,
-      scoped_refptr<VideoFrame> frame);
+      scoped_refptr<VideoFrame> frame,
+      scoped_refptr<gpu::SharedContextState> context_state);
 
   gpu::VideoImageRepresentation::ScopedReadAccess* access() const {
     return scoped_read_access_.get();
@@ -265,12 +270,12 @@
  private:
   ~SharedImageReadLock() override;
 
-  std::unique_ptr<gpu::VideoImageRepresentation, base::OnTaskRunnerDeleter>
-      representation_;
-  std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess,
-                  base::OnTaskRunnerDeleter>
+  std::unique_ptr<gpu::VideoImageRepresentation> representation_;
+  std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
       scoped_read_access_;
   scoped_refptr<VideoFrame> frame_;
+  scoped_refptr<gpu::SharedContextState> context_state_;
+  scoped_refptr<base::SequencedTaskRunner> task_runner_;
 };
 
 // Parameters:
diff --git a/media/gpu/windows/d3d12_video_encode_accelerator.cc b/media/gpu/windows/d3d12_video_encode_accelerator.cc
index ef4e5e067..e7b6bae 100644
--- a/media/gpu/windows/d3d12_video_encode_accelerator.cc
+++ b/media/gpu/windows/d3d12_video_encode_accelerator.cc
@@ -142,9 +142,8 @@
     RETURN_ON_FAILURE_WITH_CALLBACK(E_FAIL,
                                     "Failed to get shared context state");
   }
-  if (!shared_image_stub->shared_context_state()->MakeCurrent(
-          nullptr,
-          /*needs_gl=*/true)) {
+  auto shared_context_state = shared_image_stub->shared_context_state();
+  if (!shared_context_state->MakeCurrent(nullptr, /*needs_gl=*/true)) {
     RETURN_ON_FAILURE_WITH_CALLBACK(E_FAIL, "Failed to make context current");
   }
 
@@ -167,7 +166,8 @@
   }
   Microsoft::WRL::ComPtr<SharedImageReadLock> si_lock =
       Microsoft::WRL::Make<SharedImageReadLock>(
-          std::move(representation), std::move(scoped_read_access), frame);
+          std::move(representation), std::move(scoped_read_access), frame,
+          std::move(shared_context_state));
   if (!si_lock) {
     RETURN_ON_FAILURE_WITH_CALLBACK(E_OUTOFMEMORY,
                                     "Failed to create SharedImageReadLock");
Loading diff…

Original Bug Report

reported by [email protected]

Potential raw deletion in SharedImageReadLock causes cross-origin GL texture deletion on Windows

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: SharedImageReadLock uses base::OnTaskRunnerDeleter to destroy VideoImageRepresentation on the GPU-main thread. Because these tasks bypass the GPU Scheduler, they execute without calling MakeCurrent on the thread-local GL context. This can allow glDeleteTextures to run on an unrelated victim context that was left active on the thread, potentially causing cross-origin texture deletion.

Affected files:

  • media/base/win/mf_helpers.cc
  • media/base/win/mf_helpers.h

Estimated timestamp from git blame: 2026-04-24

Root Cause Analysis

In Chromium’s Windows hardware-encode video paths, SharedImageReadLock (defined in media/base/win/mf_helpers.h) wraps a gpu::VideoImageRepresentation and its ScopedReadAccess to manage their lifetimes when passed across threads. To guarantee that these GPU resources are destroyed on the correct thread, SharedImageReadLock utilizes base::OnTaskRunnerDeleter bound to the default sequenced task runner at construction time:

// media/base/win/mf_helpers.cc
SharedImageReadLock::SharedImageReadLock(
    std::unique_ptr<gpu::VideoImageRepresentation> representation,
    std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess>
        scoped_read_access,
    scoped_refptr<VideoFrame> frame)
    : representation_(representation.release(),
                      base::OnTaskRunnerDeleter(
                          base::SequencedTaskRunner::GetCurrentDefault())),
      scoped_read_access_(scoped_read_access.release(),
                          base::OnTaskRunnerDeleter(
                              base::SequencedTaskRunner::GetCurrentDefault())),
      frame_(std::move(frame)) {}

These locks are constructed on the GPU-main thread (e.g., inside GenerateResourceOnSyncTokenReleased). Thus, OnTaskRunnerDeleter schedules destruction on the GPU-main thread via DeleteSoon tasks.

When OnTaskRunnerDeleter executes, it posts a raw non-nestable task (DeleteSoon) to the GPU-main thread’s message loop. This task runs directly as a standard base::OnceClosure and bypasses the GPU Scheduler completely, performing no MakeCurrent call.

The Vulnerable Destruction Path

When the associated IMFSample (for MediaFoundationVideoEncodeAccelerator) is released on a background thread, the SharedImageReadLock COM object is destroyed, posting the raw DeleteSoon tasks to the GPU-main thread.

When the deletion tasks run on the GPU-main thread:

  1. ~SharedImageRepresentation() is called on the VideoImageRepresentation.
  2. SharedImageManager::OnRepresentationDestroyed(...) is invoked and releases the backing reference. If the renderer has already destroyed its client-side reference, the backing’s reference count drops to 0.
  3. The backing (e.g., GLTextureImageBacking) is destroyed synchronously.
  4. GLTextureImageBacking::~GLTextureImageBacking() destroys its GLTextureHolder elements.
  5. GLTextureHolder::~GLTextureHolder() releases the reference to the underlying gles2::TexturePassthrough (if using the passthrough command decoder).
  6. TexturePassthrough::~TexturePassthrough() checks have_context_ (which is true under normal conditions) and issues a synchronous glDeleteTextures(1, &owned_service_id_) call.

Because this raw deletion task runs without a MakeCurrent call, glDeleteTextures resolves through the thread-local GL bindings (via g_current_gl_context). Since gpu::Scheduler interleaves task dispatching and CommandBufferStub context operations do not release/null-out the bound context on scope destruction, any GL context that was previously bound by the last executed GPU task remains active on the thread.

If a victim context (e.g., a WebGL context from another origin utilizing its own gl::GLShareGroup) was the last context active on the GPU-main thread, glDeleteTextures will be dispatched directly into that victim’s share group namespace, deleting active victim textures matching the target ID.


Potential Attack Scenario

(Note: These are potential steps; our tooling does not currently run code to verify with a functional PoC)

  1. Grooming: A compromised renderer advances the GL name allocator to align texture names by allocating multiple temporary GLES2 SharedImages.
  2. Setup: The attacker creates a target SharedImage M with service ID N and sends an Encode() request to trigger the construction of a SharedImageReadLock inside the GPU process.
  3. Release: The attacker immediately destroys their client-side reference to M (refcount remains 1, pinned only by the SharedImageReadLock).
  4. Context Swapping: The attacker forces a victim WebGL context (from another origin) to flush GL tasks, leaving the victim’s GL context current on the GPU-main thread.
  5. Trigger: The encoder completes processing and releases the IMFSample on a background thread, posting the DeleteSoon tasks to the GPU-main thread. This triggers glDeleteTextures(1, &N) inside the active victim context, deleting the victim’s texture $N$.

Suggested Remediation

Instead of using base::OnTaskRunnerDeleter which posts raw contextless deletion tasks, the destruction of VideoImageRepresentation and ScopedReadAccess should be performed via a custom wrapper task that explicitly ensures the appropriate GL context is made current prior to releasing the pointers.

For example, we can introduce a helper function on the GPU thread to destroy these objects:

void DestroySharedImageResourcesOnGpuThread(
    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)) {
    scoped_read_access.reset();
    representation.reset();
  } else {
    // If context cannot be made current, mark as context lost to prevent unsafe deletion
    if (scoped_read_access) {
      // ...
    }
  }
}

And invoke this on the GPU task runner when the COM object is released, rather than utilizing the raw OnTaskRunnerDeleter bindings.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


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