High watch CVSS 4.3 chrome Uninitialized Memory 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in WebGPU
DescriptionUninitialized resource in WebGPU
ComponentWebGPU
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker560062638
Fix commit8f0b26829842 (chromium/src) +51/-40
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedArni Hardarson (Neonix Security)
Reported2026-09-11 (per vendor advisory)
Fix landed2026-09-16 (public git)
Disclosed2026-09-29
Patch gap13 days — fix public in source before the advisory

Background

`copyExternalImageToTexture`
A WebGPU GPUQueue API that copies pixels from an external source (e.g. an HTMLVideoElement) into a destination GPU texture.
`ExternalTexture`
A Blink helper struct bundling a wgpu_external_texture handle, a mailbox_texture, and an is_zero_copy flag that represents the imported source frame for the copy.
`CreateExternalTexture`
The helper that imports a media::VideoFrame into an ExternalTexture, with several failure points (context lost, LeaseSharedImage failure, mailbox-texture creation failure).
`CopyFromVideoElement`
The GPUQueue method that drives the video-element copy path by calling CreateExternalTexture and then issuing CopyExternalTextureForBrowser.

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.

Key insight
The core mistake is that failure from 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

  1. Invoke the video copy path From a page with WebGPU, call queue.copyExternalImageToTexture with an HTMLVideoElement source, routing into GPUQueue::CopyFromVideoElement.
  2. Force import failure Arrange a condition that makes CreateExternalTexture fail — e.g. a lost GPU context, a failed LeaseSharedImage, or failed mailbox-texture creation — so pre-patch it returns a zero/invalid ExternalTexture. (inferred as to the exact triggering condition)
  3. Silent copy with invalid resource Pre-patch, CopyFromVideoElement ignores the failure and issues CopyExternalTextureForBrowser with the invalid external texture, leaving the destination texture holding uninitialized resource contents while the API reports success.
  4. 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

A page running WebGPU in the renderer can obtain an uninitialized WebGPU texture resource whose contents were never validly written, potentially disclosing stale GPU memory, given the precondition that the external-image import can be made to fail. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3. Whether the disclosed bytes cross a security boundary (e.g. another origin’s or process’s prior GPU memory) is not established by the diff (inferred).

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/webgpu/external_texture_helper.cc
modified
if
third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
modified
if
third_party/blink/renderer/modules/webgpu/gpu_queue.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webgpu/gpu_external_texture.cc
  • third_party/blink/renderer/modules/webgpu/gpu_queue.h
  • third_party/blink/renderer/modules/webgpu/external_texture_helper.h
  • third_party/blink/renderer/modules/webgpu/gpu_queue.cc
  • third_party/blink/renderer/modules/webgpu/external_texture_helper.cc

Audit Directions

  • Infallible-looking helpers with by-value error returns
    Flag 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 paths
    Audit 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]]` coverage
    Look 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.
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, &copy_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,
Loading diff…