Medium watch CVSS 4.3 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in WebXR
DescriptionUninitialized resource in WebXR
ComponentWebXR
Bug ClassUninitialized Memory
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker524822825
Fix commita6a516a00c2f (chromium/src) +15/-5
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
CreditedGoogle
Reported2026-06-17 (per vendor advisory)
Fix landed2026-07-17 (public git)
Disclosed2026-08-25
Patch gap39 days — fix public in source before the advisory

Background

`XRWebGLTextureArraySwapChain`
a Blink WebXR helper (xr_webgl_texture_array_swap_chain.cc) that backs a texture-array XR layer and copies its layers into a side-by-side wrapped texture using the page’s WebGL context.
Vertex Array Object (VAO)
a GL object that captures vertex-attribute state (enabled arrays, attribute pointers, element buffer binding); VAO name 0 is the context’s default VAO.
Default VAO
the shared vertex state that the web page’s own WebGL code also uses whenever it has not bound a VAO of its own, so it is under page control.
`GenVertexArraysOES` / `DeleteVertexArraysOES`
GLES2Interface calls that allocate and free a named VAO.

Root Cause Analysis

XRWebGLTextureArraySwapChain declared a vao_ member for the copy from the layers texture into the side-by-side texture, but no code ever called GenVertexArraysOES to fill it in, so vao_ stayed 0 and any use of it selected the default VAO (the commit message says it “was functionally the default VAO”). The copy draw therefore ran on vertex state the page shares and can change, which breaks the state isolation that an internal compositor-style copy needs. The diff shows only that the internal resource was never initialised. Exactly what goes wrong when page-controlled attribute state reaches the copy draw, or when the copy leaves changed state in the page’s default VAO, is not shown (inferred: the copy could read stale or attacker-set attribute bindings, or the page could see changed default-VAO state afterwards).

The fix creates a private VAO lazily (if (!vao_) gl->GenVertexArraysOES(1, &vao_);) before the copy runs. It also changes Dispose() to fetch ContextGL() unconditionally and to free vao_ with DeleteVertexArraysOES before it deletes owned_texture_.

Key insight
An “owned” GL resource handle was declared but never allocated, so the zero value silently aliased the shared default VAO that the page controls. The fix allocates a real private VAO on first use and frees it on dispose.

Attack Path

  1. Enter a WebXR session with a texture-array layer a page starts an immersive WebXR session that uses a WebGL layer backed by XRWebGLTextureArraySwapChain. This needs WebXR hardware or a runtime, the usual XR session consent or user activation, and the layer configuration that selects the texture-array path (inferred from the class name).
  2. Prepare default-VAO state with no VAO bound, the page’s WebGL code sets vertex-attribute state on the default VAO (enabled arrays, attribute pointers, buffer bindings).
  3. Trigger the layer copy when the frame is submitted, the swap chain runs its copy from the layers texture to the side-by-side texture. Before the fix, that copy used vao_ == 0, which is the page-controlled default VAO.
  4. State confusion the copy draw runs with vertex state the page chose, or overwrites the page’s default-VAO state. The resulting memory-safety or information effect is not shown in the patch (inferred).

Impact Assessment

The advisory classifies this as an uninitialized resource in WebXR; it gives no in-the-wild exploitation status, and none is reported here. The code runs in the renderer’s WebGL client and issues GL commands through the GPU command buffer. Any memory-safety or cross-origin information impact beyond the state confusion is (inferred), and reaching it needs an active immersive WebXR session on a device or runtime that supports it.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc
modified

Files Changed

  • third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc

Audit Directions

  • Zero-valued GL handles used as "private" objects
    search Blink WebGL, WebXR and WebGPU interop helpers for GLuint members (VAOs, framebuffers, programs, buffers) that are bound or used but never assigned by a Gen* call; 0 silently means “default object”.
  • Internal draws on a shared user context
    look at helpers that issue draws or copies on the page’s GLES2Interface (for example XR layers, canvas or video upload paths) and check that they save and restore, or isolate, all state they touch, including VAO, program, framebuffer and pixel-store state.
  • Resource lifetime symmetry
    when a lazily created GL resource is added, confirm that Dispose/destructor paths free it and handle context loss (a null ContextGL()) correctly.
From a6a516a00c2f15b3ced4ed38d216a2233fc84505 Mon Sep 17 00:00:00 2001
From: Brandon Jones <bajones@chromium.org>
Date: Fri, 17 Jul 2026 15:16:20 -0700
Subject: [PATCH] Use internal VAO for XR texture-array copy

Previously the XRWebGLTextureArraySwapChain class had a vao_
member, but it was never initialized with an actual VAO object,
and so was functionally the default VAO. This change lazily creates
a VAO when first performing a copy and uses it to ensure state
isolation.

Fixed: 524822825
Change-Id: I01573f182f4a4ee14f08a232d49a470f8862a2cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8110498
Commit-Queue: Alexander Cooper <alcooper@chromium.org>
Auto-Submit: Brandon Jones <bajones@chromium.org>
Commit-Queue: Brandon Jones <bajones@chromium.org>
Reviewed-by: Alexander Cooper <alcooper@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1664200}
---
diff --git a/third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc b/third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc
index 1c7733e..d097ea87 100644
--- a/third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc
+++ b/third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc
@@ -44,12 +44,16 @@
 }
 
 void XRWebGLTextureArraySwapChain::Dispose() {
-  if (owned_texture_) {
-    gpu::gles2::GLES2Interface* gl = context()->ContextGL();
-    if (!gl) {
-      return;
-    }
+  gpu::gles2::GLES2Interface* gl = context()->ContextGL();
+  if (!gl) {
+    return;
+  }
 
+  if (vao_) {
+    gl->DeleteVertexArraysOES(1, &vao_);
+  }
+
+  if (owned_texture_) {
     gl->DeleteTextures(1, &owned_texture_);
   }
 }
@@ -98,6 +102,12 @@
     return;
   }
 
+  // Generate an internal VAO if one hasn't been already to more easily handle
+  // the default vertex state needed for this copy.
+  if (!vao_) {
+    gl->GenVertexArraysOES(1, &vao_);
+  }
+
   // Copy from the layers texture to the side-by-side wrapped texture.
   // Note: This could be done with less state-shifting but a bug on Qualcomm
   // devices prevents copying from non-zero layers of an array textures.
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Visual GPU Memory Disclosure via Uninitialized VAO in XRWebGLTextureArraySwapChain

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: The XRWebGLTextureArraySwapChain class declares a vao_ member intended to isolate internal copy operations but never initializes it, defaulting to 0 (the page’s default VAO). A page can poison the default VAO state, causing the internal copy draw call to fail with GL_INVALID_OPERATION under WebGL. Since the swap chain’s clearing is bypassed, this potentially results in uninitialized GPU buffers being displayed to the VR headset.

Affected files:

  • third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc
  • third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.h
  • third_party/blink/renderer/modules/xr/xr_webgl_binding.cc

Estimated timestamp from git blame: 2025-02-01

Description

In third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.h, the class XRWebGLTextureArraySwapChain declares a member variable intended to hold a private Vertex Array Object (VAO) to isolate its internal copy-draw operation from the page’s vertex state:

// third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.h:51
GLuint vao_ = 0;

However, vao_ is never initialized using GenVertexArraysOES or assigned any valid private VAO handle. As a result, the bind call in OnFrameEnd binds VAO 0 (the page’s default VAO):

// third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc:152
gl->BindVertexArrayOES(vao_); // vao_ == 0 -> default VAO

Because VAO 0 is shared with the rendering context, an attacker-controlled web page can poison the state of the default VAO (for example, by enabling a vertex attribute while VAO 0 is bound without binding a buffer to it).

When OnFrameEnd() executes the internal instanced draw call to perform the copy:

// third_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.cc:162
gl->DrawArraysInstancedANGLE(GL_TRIANGLES, 0, 6, descriptor().layers);

ANGLE’s draw validation intercepts this request. Under WebGL semantics, any enabled vertex attribute that uses client-side memory (i.e., has no bound buffer) triggers a strict validation failure inside ValidateDrawStates in validationES.cpp, rejecting the draw with a GL_INVALID_OPERATION error. Thus, zero fragments are written to the destination framebuffer.

The Clear Bypass and Potential Impact

When using a projection layer of type 'texture-array', the wrapped swap chain’s clear_on_access is explicitly set to false in xr_webgl_binding.cc:140-142 because the array wrapper is expected to completely overwrite the target texture. Since the draw call fails, the target texture is left untouched.

On Android/OpenXR, the underlying SharedImage is backed by an AHardwareBuffer that is allocated without initialization and immediately marked as cleared inside Chromium’s backing factory to bypass robust resource initialization. The OpenXR compositor subsequently samples this uninitialized buffer and presents it directly to the headset.

This could potentially allow a malicious page to visually disclose stale framebuffer or texture memory from other applications or other browser origins to the user’s VR display. This is a visual-only leak, as the attacking page does not have a mechanism to programmatically read back the pixel content from the swap chain.

Suggested/Potential Trigger Steps

Our tooling currently has no execution capabilities, but a potential sequence of events to trigger this issue is as follows:

  1. Create an immersive WebXR session with WebGL2 and the layers feature.
  2. Request a projection layer with { textureType: 'texture-array', layout: 'stereo' }.
  3. Before rendering the frame, bind the default VAO in JavaScript (gl.bindVertexArray(null)) and call gl.enableVertexAttribArray(5), leaving attribute 5 enabled without an associated buffer.
  4. Within the frame’s requestAnimationFrame callback, call getViewSubImage on the projection layer but issue no draw calls.
  5. Upon frame completion, the internal copy draw call will be rejected with GL_INVALID_OPERATION, and the uninitialized backing AHardwareBuffer will be submitted and displayed on the headset.

Suggested Fix

To resolve this issue, properly generate, bind, and destroy a private VAO within the lifetime of XRWebGLTextureArraySwapChain:

  1. In ProduceTexture() or the constructor, call gl->GenVertexArraysOES(1, &vao_) to allocate a private, isolated VAO.
  2. In Dispose(), ensure that gl->DeleteVertexArraysOES(1, &vao_) is called if vao_ is non-zero.
  3. This isolates the internal blit operation from any potential vertex state contamination introduced on the default VAO by the page.

Evaluated with Chrome root at commit: 75203b87cbf6681eb7c7dda8e1d0bf781538c76a


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