Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebXR
DescriptionInappropriate implementation in WebXR
ComponentWebXR
Bug ClassLogic Error
Tracker524822998
Fix commita4b2dfb51b0f (chromium/src) +170/-167
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc
modified
if
third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc
modified
source_texture_target_
third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
modified
if
third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
modified

Files Changed

  • third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc
  • third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
From a4b2dfb51b0f7cbfce2e8f93ff92c8d7035eafb9 Mon Sep 17 00:00:00 2001
From: Alexander Cooper <alcooper@chromium.org>
Date: Mon, 22 Jun 2026 12:26:14 -0700
Subject: [PATCH] Reset polygon mode when blitting to XR cubemap texture

Ensures that if the WEBGL_polygon_mode extension is used by the
page it doesn't interfere with copies done to the XR cubemap
swap chains.

To avoid code duplication, this CL also introduces a helper class
`ScopedXRWebGLStateRestorer` that handles saving, setting up, and
restoring the WebGL state required for WebXR copy-draw operations.
This helper is used by both XRWebGLTextureArraySwapChain and
XRWebGLCubemapSwapChain.

Fixed: 524822998
Change-Id: I85f78dc68234f67426ee8e9b3f5ca07056a72aff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7958958
Commit-Queue: Brian Sheedy <bsheedy@chromium.org>
Reviewed-by: Brian Sheedy <bsheedy@chromium.org>
Auto-Submit: Alexander Cooper <alcooper@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1650490}
---

diff --git a/third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc b/third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc
index 29e1f52..cb57f7b0 100644
--- a/third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc
+++ b/third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc
@@ -237,97 +237,44 @@
   CHECK_EQ(wrapped_swapchain_->descriptor().width, descriptor().width);
   CHECK_EQ(wrapped_swapchain_->descriptor().height, descriptor().height);
 
-  // Read the old state.
-  std::array<GLint, 4> curr_viewport = {0, 0, 0, 0};
-  gl->GetIntegerv(GL_VIEWPORT, curr_viewport.data());
+  {
+    ScopedXRWebGLStateRestorer restorer(context(), GL_TEXTURE_CUBE_MAP);
 
-  const bool depth_test_enabled = gl->IsEnabled(GL_DEPTH_TEST);
-  const bool stencil_test_enabled = gl->IsEnabled(GL_STENCIL_TEST);
-  const bool culling_enabled = gl->IsEnabled(GL_CULL_FACE);
-  const bool blend_enabled = gl->IsEnabled(GL_BLEND);
-  const bool dither_enabled = gl->IsEnabled(GL_DITHER);
+    gl->ActiveTexture(GL_TEXTURE0);
+    gl->BindTexture(GL_TEXTURE_CUBE_MAP,
+                    source_texture->Object());  // Source cubemap
 
-  gl->Disable(GL_DEPTH_TEST);
-  gl->Disable(GL_STENCIL_TEST);
-  gl->Disable(GL_CULL_FACE);
-  gl->Disable(GL_BLEND);
-  gl->Disable(GL_DITHER);
-  gl->Disable(GL_SCISSOR_TEST);
+    gl->UseProgram(copy_program_);
+    gl->Uniform1i(texture_uniform_, 0);
 
-  if (webgl2()) {
-    gl->Disable(GL_RASTERIZER_DISCARD);
+    gl->BindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_);
+    gl->BindBuffer(GL_ARRAY_BUFFER, vertex_buffer_);
+
+    // Set up position attribute.
+    gl->VertexAttribPointer(position_handle_, 2, GL_FLOAT, false, 0, nullptr);
+    gl->EnableVertexAttribArray(position_handle_);
+
+    gl->BindFramebuffer(GL_FRAMEBUFFER, GetFramebuffer()->Object());
+    gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+                             GL_TEXTURE_2D, target_texture->Object(), 0);
+
+    const GLenum draw_buffers[] = {GL_COLOR_ATTACHMENT0};
+    gl->DrawBuffersEXT(1, draw_buffers);
+
+    // 6 faces are placed as 3 tiles per row.
+    for (int i = 0; i < 6; ++i) {
+      gl->Viewport(descriptor().width * (i % 3), descriptor().height * (i / 3),
+                   descriptor().width, descriptor().height);
+      gl->Uniform1f(face_index_uniform_, i);
+      gl->DrawElements(GL_TRIANGLES, std::size(kQuadIndices), GL_UNSIGNED_SHORT,
+                       nullptr);
+    }
+
+    gl->DisableVertexAttribArray(position_handle_);
+    gl->BindBuffer(GL_ARRAY_BUFFER, 0);
+    gl->BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
   }
 
-  gl->ColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
-  gl->DepthMask(GL_FALSE);
-
-  gl->ActiveTexture(GL_TEXTURE0);
-  gl->BindTexture(GL_TEXTURE_CUBE_MAP,
-                  source_texture->Object());  // Source cubemap
-
-  gl->UseProgram(copy_program_);
-  gl->Uniform1i(texture_uniform_, 0);
-
-  gl->BindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_);
-  gl->BindBuffer(GL_ARRAY_BUFFER, vertex_buffer_);
-
-  // Set up position attribute.
-  gl->VertexAttribPointer(position_handle_, 2, GL_FLOAT, false, 0, nullptr);
-  gl->EnableVertexAttribArray(position_handle_);
-
-  gl->BindFramebuffer(GL_FRAMEBUFFER, GetFramebuffer()->Object());
-  gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
-                           target_texture->Object(), 0);
-
-  const GLenum draw_buffers[] = {GL_COLOR_ATTACHMENT0};
-  gl->DrawBuffersEXT(1, draw_buffers);
-
-  // 6 faces are placed as 3 tiles per row.
-  for (int i = 0; i < 6; ++i) {
-    gl->Viewport(descriptor().width * (i % 3), descriptor().height * (i / 3),
-                 descriptor().width, descriptor().height);
-    gl->Uniform1f(face_index_uniform_, i);
-    gl->DrawElements(GL_TRIANGLES, std::size(kQuadIndices), GL_UNSIGNED_SHORT,
-                     nullptr);
-  }
-
-  gl->DisableVertexAttribArray(position_handle_);
-  gl->BindBuffer(GL_ARRAY_BUFFER, 0);
-  gl->BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
-
-  // Restore the saved old state
-  gl->Viewport(curr_viewport[0], curr_viewport[1], curr_viewport[2],
-               curr_viewport[3]);
-  if (depth_test_enabled) {
-    gl->Enable(GL_DEPTH_TEST);
-  }
-  if (stencil_test_enabled) {
-    gl->Enable(GL_STENCIL_TEST);
-  }
-  if (culling_enabled) {
-    gl->Enable(GL_CULL_FACE);
-  }
-  if (blend_enabled) {
-    gl->Enable(GL_BLEND);
-  }
-  if (dither_enabled) {
-    gl->Enable(GL_DITHER);
-  }
-
-  // WebGLRenderingContextBase inherits from DrawingBuffer::Client, but makes
-  // all the methods private. Downcasting allows us to access them.
-  DrawingBuffer::Client* client =
-      static_cast<DrawingBuffer::Client*>(context());
-  client->DrawingBufferClientRestoreTextureCubeMapBinding();
-  client->DrawingBufferClientRestoreScissorTest();
-  client->DrawingBufferClientRestoreRasterizerDiscard();
-  client->DrawingBufferClientRestoreMaskAndClearValues();
-  client->DrawingBufferClientRestoreFramebufferBinding();
-
-  context()->RestoreVertexArrayObjectBinding();
-  context()->RestoreProgram();
-  context()->RestoreActiveTexture();
-
   wrapped_swapchain_->OnFrameEnd();
 
   // Intentionally not calling ResetCurrentTexture() here to keep the previously
diff --git a/third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc b/third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
index cddccfa..9cc8af2 100644
--- a/third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
+++ b/third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
@@ -4,6 +4,7 @@
 
 #include "third_party/blink/renderer/modules/xr/xr_webgl_swap_chain.h"
 
+#include "base/notreached.h"
 #include "third_party/blink/renderer/modules/webgl/webgl_framebuffer.h"
 #include "third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h"
 #include "third_party/blink/renderer/modules/webgl/webgl_texture.h"
@@ -12,6 +13,101 @@
 
 namespace blink {
 
+ScopedXRWebGLStateRestorer::ScopedXRWebGLStateRestorer(
+    WebGLRenderingContextBase* context,
+    GLenum source_texture_target)
+    : context_(context),
+      gl_(context->ContextGL()),
+      source_texture_target_(source_texture_target) {
+  if (!gl_) {
+    return;
+  }
+
+  gl_->GetIntegerv(GL_VIEWPORT, viewport_.data());
+
+  depth_test_enabled_ = gl_->IsEnabled(GL_DEPTH_TEST);
+  stencil_test_enabled_ = gl_->IsEnabled(GL_STENCIL_TEST);
+  culling_enabled_ = gl_->IsEnabled(GL_CULL_FACE);
+  blend_enabled_ = gl_->IsEnabled(GL_BLEND);
+  dither_enabled_ = gl_->IsEnabled(GL_DITHER);
+
+  polygon_mode_extension_enabled_ =
+      context_->ExtensionsUtil()->IsExtensionEnabled("WEBGL_polygon_mode");
+  if (polygon_mode_extension_enabled_) {
+    GLint value = 0;
+    gl_->GetIntegerv(GL_POLYGON_MODE_ANGLE, &value);
+    polygon_mode_ = static_cast<GLenum>(value);
+    gl_->PolygonModeANGLE(GL_FRONT_AND_BACK, GL_FILL_ANGLE);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential state leakage of WEBGL_polygon_mode into XRWebGLCubemapSwapChain::OnFrameEnd

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 state of the WEBGL_polygon_mode extension can potentially leak into the internal copy-draw operations of XRWebGLCubemapSwapChain::OnFrameEnd. If an attacking page sets the polygon mode to wireframe, the internal copy-draws will render only edges, leaving the wrapped SharedImage interior unwritten. This can potentially result in the visual disclosure of uninitialized or stale GPU memory on the Head-Mounted Display (HMD).

Affected files:

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

Estimated timestamp from git blame: 2025-11-19

Potential Vulnerability: Incomplete State Neutralization in XRWebGLCubemapSwapChain

Location

third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc:250-292

Description

An incomplete fix for a prior security issue (vulnerability 42691043, which neutralized WEBGL_polygon_mode state leak into WebXR internal copy-draws) only addressed XRWebGLTextureArraySwapChain::OnFrameEnd. Its sibling, XRWebGLCubemapSwapChain::OnFrameEnd, remains potentially vulnerable to the same class of state leakage.

The persistent GL state GL_POLYGON_MODE_ANGLE set by a page via the WEBGL_polygon_mode extension can leak into internal rendering commands executed on the page’s WebGL context. Unlike the texture-array counterpart, the cubemap copy routine disables tests like DEPTH_TEST, STENCIL_TEST, CULL_FACE, BLEND, DITHER, and SCISSOR_TEST, but does not query or neutralize GL_POLYGON_MODE_ANGLE before invoking gl->DrawElements:

// third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc:250-292
gl->Disable(GL_DEPTH_TEST);
gl->Disable(GL_STENCIL_TEST);
gl->Disable(GL_CULL_FACE);
gl->Disable(GL_BLEND);
gl->Disable(GL_DITHER);
gl->Disable(GL_SCISSOR_TEST);
if (webgl2()) {
  gl->Disable(GL_RASTERIZER_DISCARD);
}
// <-- NO PolygonModeANGLE(GL_FRONT_AND_BACK, GL_FILL_ANGLE) neutralization here
...
for (int i = 0; i < 6; ++i) {
  gl->Viewport(descriptor().width * (i % 3), descriptor().height * (i / 3),
               descriptor().width, descriptor().height);
  gl->Uniform1f(face_index_uniform_, i);
  gl->DrawElements(GL_TRIANGLES, std::size(kQuadIndices), GL_UNSIGNED_SHORT,
                   nullptr);                                       // :290
}

Why the Destination is Uninitialized

During creation of the layer, the wrapped swap chain is allocated with clear_on_access = false, as specified in xr_webgl_binding.cc:

// We don't need to clear the buffer anyway because the wrapper
// XRWebGLCubemapSwapChain will do it.
XRWebGLSwapChain* texture_2d_swapchain = CreateColorSwapchain(
    init->colorFormat(),
    gfx::Size(init->viewPixelWidth(), init->viewPixelHeight()),
    V8XRTextureType(V8XRTextureType::Enum::kTexture),
    V8XRLayerLayout::Enum::kMono, false /*clear_on_access*/);

Because clear_on_access is false, calling OnTextureQueried() on the wrapped swap chain bypasses ClearCurrentTexture(). If the page sets WEBGL_polygon_mode to wireframe mode (GL_LINE_ANGLE), the internal DrawElements call only draws the edges of the full-screen quad. The interior of the quad remains completely unwritten and contains stale or uninitialized GPU memory (such as previously allocated SharedImage bytes), which is subsequently submitted to the XR compositor and displayed on the user’s headset.

Suggested Potential Reproduction Steps

Note: These are potential, theoretical steps modeled from code inspection, as our automated tooling cannot execute code directly.

  1. In an immersive WebXR session supporting the layers feature, the application obtains the ‘WEBGL_polygon_mode’ extension via gl.getExtension('WEBGL_polygon_mode').
  2. The application creates a cube layer with binding.createCubeLayer(...) and requests a sub-image to mark the texture as queried.
  3. The application sets the polygon mode to wireframe: ext.polygonModeWEBGL(gl.FRONT_AND_BACK, ext.LINE_WEBGL).
  4. At frame submission, XRWebGLCubemapSwapChain::OnFrameEnd() executes the internal draw without neutralizing the polygon mode.
  5. The interior pixels of the wrapped SharedImage are left unwritten, potentially revealing stale VRAM contents on the HMD display.

Suggested Fix

To fix this potential issue, mirror the state neutralization logic from XRWebGLTextureArraySwapChain::OnFrameEnd to XRWebGLCubemapSwapChain::OnFrameEnd:

GLenum polygon_mode = GL_FILL_ANGLE;
if (context()->ExtensionsUtil()->IsExtensionEnabled('WEBGL_polygon_mode')) {
  GLint value = 0;
  gl->GetIntegerv(GL_POLYGON_MODE_ANGLE, &value);
  polygon_mode = static_cast<GLenum>(value);
  gl->PolygonModeANGLE(GL_FRONT_AND_BACK, GL_FILL_ANGLE);
}

// ... performs DrawElements ...

if (polygon_mode != GL_FILL_ANGLE) {
  gl->PolygonModeANGLE(GL_FRONT_AND_BACK, polygon_mode);
}

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