CVE-2026-102300
Overview
Background
- `copyExternalImageToTexture`
- A WebGPU
GPUQueueAPI that copies pixels from an external source (e.g. anHTMLVideoElement) into a destination GPU texture. - `ExternalTexture`
- A Blink helper struct bundling a
wgpu_external_texturehandle, amailbox_texture, and anis_zero_copyflag that represents the imported source frame for the copy. - `CreateExternalTexture`
- The helper that imports a
media::VideoFrameinto anExternalTexture, with several failure points (context lost,LeaseSharedImagefailure, mailbox-texture creation failure). - `CopyFromVideoElement`
- The
GPUQueuemethod that drives the video-element copy path by callingCreateExternalTextureand then issuingCopyExternalTextureForBrowser.
Root Cause Analysis
CreateExternalTexture returned an ExternalTexture by value, and on its error paths it returned a default/zero-initialized struct (the removed ExternalTexture external_texture = {}; plus the return external_texture; early-exits for context-lost and failed LeaseSharedImage), while also lacking any check that mailbox_texture creation succeeded before using it. CopyFromVideoElement was declared void and “pretended to be infallible”: it took the returned struct and unconditionally built wgpu::ImageCopyExternalTexture from external_texture.wgpu_external_texture and called GetHandle().CopyExternalTextureForBrowser(...) without validating that import had succeeded. Consequently, when import failed, the copy proceeded with an invalid/incompletely-initialized external texture and the destination texture region was left as an uninitialized WebGPU resource, with no error surfaced to the caller.
The fix changes CreateExternalTexture to return std::optional<ExternalTexture> and to return {} on every failure (including a new if (!mailbox_texture) return {}; guard), makes CopyFromVideoElement return bool and bail out with false when the optional is empty, and makes GPUQueue’s copyExternalImageToTexture path throw an OperationError DOMException when the copy reports failure. The gpu_external_texture.cc caller is likewise switched to the std::optional check (if (!external_texture)), replacing the ad-hoc null-field test.
CreateExternalTexture was communicated only via a zero/partially-initialized return value that CopyFromVideoElement never checked, so a failed import silently produced an uninitialized destination texture; the fix propagates failure explicitly through std::optional<ExternalTexture> and [[nodiscard]] bool return values that force callers to abort and throw.Attack Path
- Invoke the video copy path
From a page with WebGPU, call
queue.copyExternalImageToTexturewith anHTMLVideoElementsource, routing intoGPUQueue::CopyFromVideoElement. - Force import failure
Arrange a condition that makes
CreateExternalTexturefail — e.g. a lost GPU context, a failedLeaseSharedImage, or failed mailbox-texture creation — so pre-patch it returns a zero/invalidExternalTexture. (inferred as to the exact triggering condition) - Silent copy with invalid resource
Pre-patch,
CopyFromVideoElementignores the failure and issuesCopyExternalTextureForBrowserwith the invalid external texture, leaving the destination texture holding uninitialized resource contents while the API reports success. - Read back uninitialized memory The attacker subsequently samples or copies out the destination texture to observe whatever contents were left in that GPU resource. (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/webgpu/external_texture_helper.cc |
modified | |
ifthird_party/blink/renderer/modules/webgpu/gpu_external_texture.cc |
modified | |
ifthird_party/blink/renderer/modules/webgpu/gpu_queue.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webgpu/gpu_external_texture.ccthird_party/blink/renderer/modules/webgpu/gpu_queue.hthird_party/blink/renderer/modules/webgpu/external_texture_helper.hthird_party/blink/renderer/modules/webgpu/gpu_queue.ccthird_party/blink/renderer/modules/webgpu/external_texture_helper.cc
Audit Directions
- Infallible-looking helpers with by-value error returnsFlag functions that report failure only by returning a default/zero-initialized aggregate; convert to
std::optional/status returns so callers cannot ignore the failure. - Resource-import to GPU-copy pathsAudit other WebGPU/graphics copy paths (external images, video frames, canvas imports) to confirm a failed import aborts the copy instead of writing/reading an uninitialized destination resource.
- `[[nodiscard]]` coverageLook for copy/upload helpers whose success is meaningful but whose return value is unchecked or
void, and ensure their results are propagated to a user-visible error.
Patch
From 8f0b2682984207138b06e0a90c962e34376b89d1 Mon Sep 17 00:00:00 2001
From: Kai Ninomiya <kainino@chromium.org>
Date: Wed, 16 Sep 2026 11:27:02 -0700
Subject: [PATCH] Add missing validation to copyExternalImageToTexture
copyExternalImageToTexture would ignore certain errors in
CreateExternalTexture (via CopyFromVideoElement which was incorrectly
pretending to be infallible).
Refactor several error-prone patterns so this is easier to do correctly
in the future.
Fixed: 560062638
Change-Id: I321c3a4256f8a14d4ddd505da2afb3f25ece7fab
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8415995
Reviewed-by: Brandon Jones <bajones@chromium.org>
Commit-Queue: Kai Ninomiya <kainino@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699109}
---
diff --git a/third_party/blink/renderer/modules/webgpu/external_texture_helper.cc b/third_party/blink/renderer/modules/webgpu/external_texture_helper.cc
index c90a3c5..0fa7e6f 100644
--- a/third_party/blink/renderer/modules/webgpu/external_texture_helper.cc
+++ b/third_party/blink/renderer/modules/webgpu/external_texture_helper.cc
@@ -207,7 +207,7 @@
return source;
}
-ExternalTexture CreateExternalTexture(
+std::optional<ExternalTexture> CreateExternalTexture(
GPUDevice* device,
PredefinedColorSpace dst_predefined_color_space,
scoped_refptr<media::VideoFrame> media_video_frame) {
@@ -228,8 +228,6 @@
src_color_space = gfx::ColorSpace::CreateREC709();
}
- ExternalTexture external_texture = {};
-
// TODO(crbug.com/1306753): Use SharedImageProducer and CompositeSharedImage
// rather than check 'is_webgpu_compatible'.
// Note that the feature is checked on the adapter and not the device, because
@@ -339,18 +337,18 @@
external_texture_desc.dstTransferFunctionParameters =
color_space_conversion_constants.dst_transfer_constants.data();
- external_texture.wgpu_external_texture =
- device->GetHandle().CreateExternalTexture(&external_texture_desc);
-
- external_texture.mailbox_texture = std::move(mailbox_texture);
- external_texture.is_zero_copy = true;
- return external_texture;
+ return ExternalTexture{
+ .wgpu_external_texture =
+ device->GetHandle().CreateExternalTexture(&external_texture_desc),
+ .mailbox_texture = std::move(mailbox_texture),
+ .is_zero_copy = true,
+ };
}
// If the context is lost, the resource provider would be invalid.
auto context_provider_wrapper = SharedGpuContext::ContextProviderWrapper();
if (!context_provider_wrapper ||
context_provider_wrapper->ContextProvider().IsContextLost()) {
- return external_texture;
+ return {};
}
// In 0-copy path, uploading shares the whole frame into dawn and apply
@@ -404,7 +402,7 @@
device->GetDawnControlClient()->LeaseSharedImage(
format, natural_size, resource_color_space, kPremul_SkAlphaType);
if (!lease) {
- return external_texture;
+ return {};
}
scoped_refptr<gpu::ClientSharedImage> shared_image = lease->GetSharedImage();
@@ -461,6 +459,9 @@
device->GetDawnControlClient(), device->GetHandle(),
wgpu::TextureUsage::TextureBinding, std::move(shared_image),
sync_token, std::move(lease));
+ if (!mailbox_texture) {
+ return {};
+ }
wgpu::TextureViewDescriptor view_desc = {};
wgpu::TextureView plane0 =
@@ -484,11 +485,12 @@
external_texture_desc.dstTransferFunctionParameters =
color_space_conversion_constants.dst_transfer_constants.data();
- external_texture.wgpu_external_texture =
- device->GetHandle().CreateExternalTexture(&external_texture_desc);
- external_texture.mailbox_texture = std::move(mailbox_texture);
-
- return external_texture;
+ return ExternalTexture{
+ .wgpu_external_texture =
+ device->GetHandle().CreateExternalTexture(&external_texture_desc),
+ .mailbox_texture = std::move(mailbox_texture),
+ .is_zero_copy = false,
+ };
}
} // namespace blink
diff --git a/third_party/blink/renderer/modules/webgpu/external_texture_helper.h b/third_party/blink/renderer/modules/webgpu/external_texture_helper.h
index c000940..0f77578 100644
--- a/third_party/blink/renderer/modules/webgpu/external_texture_helper.h
+++ b/third_party/blink/renderer/modules/webgpu/external_texture_helper.h
@@ -60,7 +60,7 @@
VideoFrame* frame,
ExceptionState& exception_state);
-ExternalTexture CreateExternalTexture(
+std::optional<ExternalTexture> CreateExternalTexture(
GPUDevice* device,
PredefinedColorSpace dst_predefined_color_space,
scoped_refptr<media::VideoFrame> media_video_frame);
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
index c3aa335..aae1d0b3 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
@@ -174,11 +174,9 @@
return nullptr;
}
- ExternalTexture external_texture = CreateExternalTexture(
+ std::optional<ExternalTexture> external_texture = CreateExternalTexture(
cache->device(), dst_predefined_color_space, media_video_frame);
-
- if (external_texture.wgpu_external_texture == nullptr ||
- external_texture.mailbox_texture == nullptr) {
+ if (!external_texture) {
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError,
"Failed to import texture from video");
return nullptr;
@@ -186,8 +184,8 @@
GPUExternalTexture* gpu_external_texture =
MakeGarbageCollected<GPUExternalTexture>(
- cache, std::move(external_texture.wgpu_external_texture),
- external_texture.mailbox_texture, external_texture.is_zero_copy,
+ cache, std::move(external_texture->wgpu_external_texture),
+ external_texture->mailbox_texture, external_texture->is_zero_copy,
media_video_frame->metadata().read_lock_fences_enabled,
media_video_frame_unique_id, webgpu_desc->label());
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_queue.cc b/third_party/blink/renderer/modules/webgpu/gpu_queue.cc
index ab9b4e0..7cb21f5 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_queue.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_queue.cc
@@ -416,10 +416,15 @@
// Use display size which is based on natural size but considering
// transformation metadata.
wgpu::Extent2D video_frame_display_size = {source->width, source->height};
- CopyFromVideoElement(
- source->external_texture_source, video_frame_display_size,
- origin_in_external_image, dawn_copy_size, dawn_destination,
- destination->premultipliedAlpha(), color_space, copyImage->flipY());
+ if (!CopyFromVideoElement(
+ source->external_texture_source, video_frame_display_size,
+ origin_in_external_image, dawn_copy_size, dawn_destination,
+ destination->premultipliedAlpha(), color_space,
+ copyImage->flipY())) {
+ exception_state.ThrowDOMException(
+ DOMExceptionCode::kOperationError,
+ "Failed to copy content from video element.");
+ }
return;
}
@@ -645,7 +650,7 @@
}
}
-void GPUQueue::CopyFromVideoElement(
+bool GPUQueue::CopyFromVideoElement(
const ExternalTextureSource source,
const wgpu::Extent2D& video_frame_natural_size,
const wgpu::Origin2D& origin,
@@ -658,8 +663,11 @@
// Create External Texture with dst color space. No color space conversion
// happens during copy step.
- ExternalTexture external_texture =
+ std::optional<ExternalTexture> external_texture =
CreateExternalTexture(device_, dst_color_space, source.media_video_frame);
+ if (!external_texture) {
+ return false;
+ }
wgpu::CopyTextureForBrowserOptions options = {
// Extracting contents from HTMLVideoElement (e.g.
@@ -675,17 +683,19 @@
options.flipY = flipY;
wgpu::ImageCopyExternalTexture src = {
- .externalTexture = external_texture.wgpu_external_texture,
+ .externalTexture = external_texture->wgpu_external_texture,
.origin = {origin.x, origin.y},
.naturalSize = video_frame_natural_size,
};
GetHandle().CopyExternalTextureForBrowser(&src, &destination, ©_size,
&options);
- if (external_texture.is_zero_copy &&
+ if (external_texture->is_zero_copy &&
source.media_video_frame->metadata().read_lock_fences_enabled) {
- ReferenceUntilGPUIsFinished(std::move(external_texture.mailbox_texture));
+ ReferenceUntilGPUIsFinished(std::move(external_texture->mailbox_texture));
}
+
+ return true;
}
void GPUQueue::ReferenceUntilGPUIsFinished(
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_queue.h b/third_party/blink/renderer/modules/webgpu/gpu_queue.h
index 4f4615c..30559d8 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_queue.h
+++ b/third_party/blink/renderer/modules/webgpu/gpu_queue.h
@@ -103,14 +103,15 @@
bool IsValidDestinationTexture(GPUImageCopyTextureTagged* destination,
wgpu::TexelCopyTextureInfo& dawn_destination,
ExceptionState& exception_state);
- void CopyFromVideoElement(const ExternalTextureSource source,
- const wgpu::Extent2D& video_frame_natural_size,
- const wgpu::Origin2D& origin,
- const wgpu::Extent3D& copy_size,
- const wgpu::TexelCopyTextureInfo& destination,
- bool dst_premultiplied_alpha,
- PredefinedColorSpace dst_color_space,
- bool flipY);
+ [[nodiscard]] bool CopyFromVideoElement(
+ const ExternalTextureSource source,
+ const wgpu::Extent2D& video_frame_natural_size,
+ const wgpu::Origin2D& origin,
+ const wgpu::Extent3D& copy_size,
+ const wgpu::TexelCopyTextureInfo& destination,
+ bool dst_premultiplied_alpha,
+ PredefinedColorSpace dst_color_space,
+ bool flipY);
void DrawElementImageToTextureInternal(
const V8UnionElementOrElementImage* source,
std::optional<float> sx,