CVE-2026-17892
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.cc |
modified | |
ifthird_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 | |
ifthird_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc |
modified |
Files Changed
third_party/blink/renderer/modules/xr/xr_webgl_cubemap_swap_chain.ccthird_party/blink/renderer/modules/xr/xr_webgl_swap_chain.cc
Patch
From a4b2dfb51b0f7cbfce2e8f93ff92c8d7035eafb9 Mon Sep 17 00:00:00 2001 From: Alexander Cooper <[email protected]> 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 <[email protected]> Reviewed-by: Brian Sheedy <[email protected]> Auto-Submit: Alexander Cooper <[email protected]> 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);
Original Bug Report
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.
- In an immersive WebXR session supporting the layers feature, the application obtains the ‘WEBGL_polygon_mode’ extension via
gl.getExtension('WEBGL_polygon_mode'). - The application creates a cube layer with
binding.createCubeLayer(...)and requests a sub-image to mark the texture as queried. - The application sets the polygon mode to wireframe:
ext.polygonModeWEBGL(gl.FRONT_AND_BACK, ext.LINE_WEBGL). - At frame submission,
XRWebGLCubemapSwapChain::OnFrameEnd()executes the internal draw without neutralizing the polygon mode. - 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.