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 <[email protected]> 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 <[email protected]> Commit-Queue: Kai Ninomiya <[email protected]> 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,