CVE-2026-17846
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/base/win/mf_helpers.cc |
modified | |
IMFMediaTypemedia/base/win/mf_helpers.h |
modified | |
SharedContextStatemedia/base/win/mf_helpers.h |
modified | |
ifmedia/gpu/windows/d3d12_video_encode_accelerator.cc |
modified |
Files Changed
media/base/win/mf_helpers.ccmedia/base/win/mf_helpers.hmedia/gpu/windows/d3d12_video_encode_accelerator.cc
Patch
From cd10f7bb2401dce469417205344cfb535d89dfb5 Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
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 <dalecurtis@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
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");
Original Bug Report
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.ccmedia/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:
~SharedImageRepresentation()is called on theVideoImageRepresentation.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.- The backing (e.g.,
GLTextureImageBacking) is destroyed synchronously. GLTextureImageBacking::~GLTextureImageBacking()destroys itsGLTextureHolderelements.GLTextureHolder::~GLTextureHolder()releases the reference to the underlyinggles2::TexturePassthrough(if using the passthrough command decoder).TexturePassthrough::~TexturePassthrough()checkshave_context_(which istrueunder normal conditions) and issues a synchronousglDeleteTextures(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)
- Grooming: A compromised renderer advances the GL name allocator to align texture names by allocating multiple temporary GLES2 SharedImages.
- Setup: The attacker creates a target SharedImage
Mwith service IDNand sends anEncode()request to trigger the construction of aSharedImageReadLockinside the GPU process. - Release: The attacker immediately destroys their client-side reference to
M(refcount remains 1, pinned only by theSharedImageReadLock). - 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.
- Trigger: The encoder completes processing and releases the
IMFSampleon a background thread, posting theDeleteSoontasks to the GPU-main thread. This triggersglDeleteTextures(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.