CVE-2026-78962
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_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
Patch
From a6a516a00c2f15b3ced4ed38d216a2233fc84505 Mon Sep 17 00:00:00 2001 From: Brandon Jones <[email protected]> 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 <[email protected]> Auto-Submit: Brandon Jones <[email protected]> Commit-Queue: Brandon Jones <[email protected]> Reviewed-by: Alexander Cooper <[email protected]> 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.
Original Bug Report
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.ccthird_party/blink/renderer/modules/xr/xr_webgl_texture_array_swap_chain.hthird_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:
- Create an immersive WebXR session with WebGL2 and the
layersfeature. - Request a projection layer with
{ textureType: 'texture-array', layout: 'stereo' }. - Before rendering the frame, bind the default VAO in JavaScript (
gl.bindVertexArray(null)) and callgl.enableVertexAttribArray(5), leaving attribute 5 enabled without an associated buffer. - Within the frame’s
requestAnimationFramecallback, callgetViewSubImageon the projection layer but issue no draw calls. - Upon frame completion, the internal copy draw call will be rejected with
GL_INVALID_OPERATION, and the uninitialized backingAHardwareBufferwill 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:
- In
ProduceTexture()or the constructor, callgl->GenVertexArraysOES(1, &vao_)to allocate a private, isolated VAO. - In
Dispose(), ensure thatgl->DeleteVertexArraysOES(1, &vao_)is called ifvao_is non-zero. - 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.