High chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in ANGLE
DescriptionMemory corruption in ANGLE
ComponentANGLE
Sub-componentD3D11
Bug ClassMemory Corruption
Tracker556505644
Fix commit48a16bd63d2d (angle/angle) +145/-3
CISA KEVNot listed
CreditedZabith Mohammed
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
HardenedTransformFeedbackTest
src/tests/gl_tests/TransformFeedbackTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp
  • src/tests/gl_tests/TransformFeedbackTest.cpp
  • src/tests/gl_tests/MultiviewDrawTest.cpp
From 48a16bd63d2d5ac42b133ff4da511928af02eb97 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <[email protected]>
Date: Thu, 17 Sep 2026 17:18:13 +0800
Subject: [PATCH] D3D11: Fix InputLayoutCache key for disabled and multiview attributes

In InputLayoutCache::getInputLayout, the divisor recorded in the
cache key did not account for whether the attribute is enabled, nor
did it use the overflow-safe multiview divisor helper:

1. When an attribute is disabled, D3D11 uses current-value data which
is always stored and fetched with divisor 0 (per-vertex). Recording the
binding's non-zero divisor in the cache key caused collisions with
enabled attributes.
2. In multiview rendering, calculating the cache key divisor via
wrapping 32-bit arithmetic could wrap to 0 (e.g. 0x80000000 * 2),
while VertexArray11 clamped to UINT_MAX via
GetMultiviewAdjustedDivisor.

This change uses GetMultiviewAdjustedDivisor and sets the divisor to 0
for disabled attributes in the cache key.

Bug: chromium:560423758
Bug: chromium:556505644
Change-Id: I0e256bb80d8e9588236be20e001271d9d0698b62
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8425179
Reviewed-by: Geoff Lang <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Quyen Le <[email protected]>
Auto-Submit: Quyen Le <[email protected]>
---
diff --git a/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp b/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp
index 3795eb4..a3607da 100644
--- a/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp
@@ -24,6 +24,7 @@
 #include "libANGLE/renderer/d3d/d3d11/ShaderExecutable11.h"
 #include "libANGLE/renderer/d3d/d3d11/VertexArray11.h"
 #include "libANGLE/renderer/d3d/d3d11/formatutils11.h"
+#include "libANGLE/renderer/renderer_utils.h"
 
 namespace rx
 {
@@ -127,7 +128,7 @@
     const auto &attribs            = state.getVertexArray()->getVertexAttributes();
     const auto &bindings           = state.getVertexArray()->getVertexBindings();
     const auto &locationToSemantic = executableD3D->getAttribLocationToD3DSemantics();
-    int divisorMultiplier          = executable->usesMultiview() ? executable->getNumViews() : 1;
+    int numViews = executable->usesMultiview() ? executable->getNumViews() : 1;
 
     for (size_t attribIndex : executable->getActiveAttribLocationsMask())
     {
@@ -143,8 +144,13 @@
             state.getVertexAttribCurrentValue(static_cast<unsigned int>(attribIndex));
         angle::FormatID vertexFormatID = gl::GetVertexFormatID(attrib, currentValue.Type);
 
-        layout.addAttributeData(glslElementType, d3dSemantic, vertexFormatID,
-                                binding.getDivisor() * divisorMultiplier);
+        // Disabled attributes use current-value data, which is stored and fetched with divisor 0
+        // (per-vertex). Using divisor 0 here prevents cache key collisions with enabled attributes
+        // whose binding has a non-zero divisor.
+        GLuint divisor =
+            attrib.enabled ? GetMultiviewAdjustedDivisor(numViews, binding.getDivisor()) : 0;
+
+        layout.addAttributeData(glslElementType, d3dSemantic, vertexFormatID, divisor);
     }
 
     if (layout.numAttributes > 0)
diff --git a/src/tests/gl_tests/MultiviewDrawTest.cpp b/src/tests/gl_tests/MultiviewDrawTest.cpp
index df34655..cafb935 100644
--- a/src/tests/gl_tests/MultiviewDrawTest.cpp
+++ b/src/tests/gl_tests/MultiviewDrawTest.cpp
@@ -1622,6 +1622,76 @@
     EXPECT_EQ(GLColor::green, GetViewColor(0, 0, 1));
 }
 
+// The test verifies that using a large attribute divisor does not cause an internal integer
+// overflow when drawing with multiview after a prior draw with divisor 0.
+TEST_P(MultiviewRenderTest, LargeAttribDivisorWithPriorDraw)
+{
+    ANGLE_SKIP_TEST_IF(!requestMultiviewExtension(isMultisampled()));
+
+    updateFBOs(1, 1, 2);
+
+    const std::string nonMultiviewVS = R"(#version 300 es
+layout(location = 0) in float val;
+out vec4 color;
+void main() {
+  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
+  gl_PointSize = 1.0;
+  color = (val == 0.0) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
+})";
+
+    const std::string multiviewVS = R"(#version 300 es
+#extension )" + extensionName() +
+                                    R"( : require
+layout(num_views = 2) in;
+layout(location = 0) in float val;
+out vec4 color;
+void main() {
+  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
+  gl_PointSize = 1.0;
+  color = (val == 0.0) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+in vec4 color;
+out vec4 outColor;
+void main() { outColor = color; })";
+
+    ANGLE_GL_PROGRAM(nonMultiviewProgram, nonMultiviewVS.c_str(), kFS);
+    ANGLE_GL_PROGRAM(multiviewProgram, multiviewVS.c_str(), kFS);
+
+    // Buffer with 2 floats: index 0 is 0.0f (green); index 1 is 1.0f (red).
+    const float kData[2] = {0.0f, 1.0f};
+    GLBuffer buffer;
+    glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kData), kData, GL_STATIC_DRAW);
+
+    glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(0);
+
+    // Initial draw with single-view program and divisor 0.
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+    glVertexAttribDivisor(0, 0);
+    glUseProgram(nonMultiviewProgram);
+    glDrawArrays(GL_POINTS, 0, 1);
+    ASSERT_GL_NO_ERROR();
+
+    // Subsequent draw with 2-view multiview program and large divisor.
+    bindMemberDrawFramebuffer();
+    glViewport(0, 0, 1, 1);
+    glClearColor(0, 0, 0, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glVertexAttribDivisor(0, 0x80000000u);
+    glUseProgram(multiviewProgram);
+    glDrawArraysInstanced(GL_POINTS, 0, 2, 1);
+    ASSERT_GL_NO_ERROR();
+
+    resolveMultisampledFBO();
+    EXPECT_EQ(GLColor::green, GetViewColor(0, 0, 0));
+    EXPECT_EQ(GLColor::green, GetViewColor(0, 0, 1));
+}
+
 // Test that different sequences of vertexAttribDivisor, useProgram and bindVertexArray in a
 // multi-view context propagate the correct divisor to the driver.
 TEST_P(MultiviewRenderTest, DivisorOrderOfOperation)
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 3a74bc3..e71f68b 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -5473,6 +5473,72 @@
     EXPECT_EQ(6u, primitivesWritten);
 }
 
+// Test that an instanced attribute with a non-zero divisor produces the expected value across
+// all vertices of an instance, even after an intervening draw where the attribute was disabled.
+TEST_P(TransformFeedbackTest, InstancedAttributeAfterDisabledAttribDraw)
+{
+    constexpr char kVS[] = R"(#version 300 es
+layout(location=0) in vec4 value;
+out vec4 captured;
+void main() {
+  captured = value;
+  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
+  gl_PointSize = 1.0;
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main() {
+  color = vec4(0.0);
+})";
+
+    std::vector<std::string> tfVaryings = {"captured"};
+    ANGLE_GL_PROGRAM_TRANSFORM_FEEDBACK(program, kVS, kFS, tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    glUseProgram(program);
+
+    constexpr std::array<float, 4> kMarker = {11.25f, 22.5f, 33.75f, 44.125f};
+    GLBuffer source;
+    glBindBuffer(GL_ARRAY_BUFFER, source);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kMarker), kMarker.data(), GL_DYNAMIC_DRAW);
+    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 16, nullptr);
+    glVertexAttribDivisor(0, 0xffffffffu);
+
+    // Initial draw with attribute 0 disabled.
+    glDisableVertexAttribArray(0);
+    glVertexAttrib4fv(0, kMarker.data());
+    glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
+    glFinish();
+
+    // Re-enable attribute 0 and capture vertices via transform feedback in an instanced draw.
+    glEnableVertexAttribArray(0);
+    constexpr GLsizei kCount = 64;
+    GLBuffer destination;
+    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, destination);
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCount * sizeof(kMarker), nullptr, GL_DYNAMIC_READ);
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, destination);
+
+    glEnable(GL_RASTERIZER_DISCARD);
+    glBeginTransformFeedback(GL_POINTS);
+    glDrawArraysInstanced(GL_POINTS, 0, kCount, 1);
+    glEndTransformFeedback();
+    glDisable(GL_RASTERIZER_DISCARD);
+    glFinish();
+    ASSERT_GL_NO_ERROR();
+
+    const float *captured = static_cast<const float *>(glMapBufferRange(
+        GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCount * sizeof(kMarker), GL_MAP_READ_BIT));
+    ASSERT_NE(nullptr, captured);
+    for (GLsizei vertex = 0; vertex < kCount; ++vertex)
+    {
+        EXPECT_EQ(kMarker[0], captured[vertex * 4 + 0]) << "vertex " << vertex;
+        EXPECT_EQ(kMarker[1], captured[vertex * 4 + 1]) << "vertex " << vertex;
+        EXPECT_EQ(kMarker[2], captured[vertex * 4 + 2]) << "vertex " << vertex;
+        EXPECT_EQ(kMarker[3], captured[vertex * 4 + 3]) << "vertex " << vertex;
+    }
+    glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
+}
+
 class HardenedTransformFeedbackTest : public TransformFeedbackTest
 {
   public:
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/MultiviewDrawTest.cpp b/src/tests/gl_tests/MultiviewDrawTest.cpp
index df34655..cafb935 100644
--- a/src/tests/gl_tests/MultiviewDrawTest.cpp
+++ b/src/tests/gl_tests/MultiviewDrawTest.cpp
@@ -1622,6 +1622,76 @@
     EXPECT_EQ(GLColor::green, GetViewColor(0, 0, 1));
 }
 
+// The test verifies that using a large attribute divisor does not cause an internal integer
+// overflow when drawing with multiview after a prior draw with divisor 0.
+TEST_P(MultiviewRenderTest, LargeAttribDivisorWithPriorDraw)
+{
+    ANGLE_SKIP_TEST_IF(!requestMultiviewExtension(isMultisampled()));
+
+    updateFBOs(1, 1, 2);
+
+    const std::string nonMultiviewVS = R"(#version 300 es
+layout(location = 0) in float val;
+out vec4 color;
+void main() {
+  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
+  gl_PointSize = 1.0;
+  color = (val == 0.0) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
+})";
+
+    const std::string multiviewVS = R"(#version 300 es
+#extension )" + extensionName() +
+                                    R"( : require
+layout(num_views = 2) in;
+layout(location = 0) in float val;
+out vec4 color;
+void main() {
+  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
+  gl_PointSize = 1.0;
+  color = (val == 0.0) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+in vec4 color;
+out vec4 outColor;
+void main() { outColor = color; })";
+
+    ANGLE_GL_PROGRAM(nonMultiviewProgram, nonMultiviewVS.c_str(), kFS);
+    ANGLE_GL_PROGRAM(multiviewProgram, multiviewVS.c_str(), kFS);
+
+    // Buffer with 2 floats: index 0 is 0.0f (green); index 1 is 1.0f (red).
+    const float kData[2] = {0.0f, 1.0f};
+    GLBuffer buffer;
+    glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kData), kData, GL_STATIC_DRAW);
+
+    glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(0);
+
+    // Initial draw with single-view program and divisor 0.
+    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
+    glVertexAttribDivisor(0, 0);
+    glUseProgram(nonMultiviewProgram);
+    glDrawArrays(GL_POINTS, 0, 1);
+    ASSERT_GL_NO_ERROR();
+
+    // Subsequent draw with 2-view multiview program and large divisor.
+    bindMemberDrawFramebuffer();
+    glViewport(0, 0, 1, 1);
+    glClearColor(0, 0, 0, 1);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glVertexAttribDivisor(0, 0x80000000u);
+    glUseProgram(multiviewProgram);
+    glDrawArraysInstanced(GL_POINTS, 0, 2, 1);
+    ASSERT_GL_NO_ERROR();
+
+    resolveMultisampledFBO();
+    EXPECT_EQ(GLColor::green, GetViewColor(0, 0, 0));
+    EXPECT_EQ(GLColor::green, GetViewColor(0, 0, 1));
+}
+
 // Test that different sequences of vertexAttribDivisor, useProgram and bindVertexArray in a
 // multi-view context propagate the correct divisor to the driver.
 TEST_P(MultiviewRenderTest, DivisorOrderOfOperation)
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 3a74bc3..e71f68b 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -5473,6 +5473,72 @@
     EXPECT_EQ(6u, primitivesWritten);
 }
 
+// Test that an instanced attribute with a non-zero divisor produces the expected value across
+// all vertices of an instance, even after an intervening draw where the attribute was disabled.
+TEST_P(TransformFeedbackTest, InstancedAttributeAfterDisabledAttribDraw)
+{
+    constexpr char kVS[] = R"(#version 300 es
+layout(location=0) in vec4 value;
+out vec4 captured;
+void main() {
+  captured = value;
+  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
+  gl_PointSize = 1.0;
+})";
+
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 color;
+void main() {
+  color = vec4(0.0);
+})";
+
+    std::vector<std::string> tfVaryings = {"captured"};
+    ANGLE_GL_PROGRAM_TRANSFORM_FEEDBACK(program, kVS, kFS, tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    glUseProgram(program);
+
+    constexpr std::array<float, 4> kMarker = {11.25f, 22.5f, 33.75f, 44.125f};
+    GLBuffer source;
+    glBindBuffer(GL_ARRAY_BUFFER, source);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(kMarker), kMarker.data(), GL_DYNAMIC_DRAW);
+    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 16, nullptr);
+    glVertexAttribDivisor(0, 0xffffffffu);
+
+    // Initial draw with attribute 0 disabled.
+    glDisableVertexAttribArray(0);
+    glVertexAttrib4fv(0, kMarker.data());
+    glDrawArraysInstanced(GL_POINTS, 0, 1, 1);
+    glFinish();
+
+    // Re-enable attribute 0 and capture vertices via transform feedback in an instanced draw.
+    glEnableVertexAttribArray(0);
+    constexpr GLsizei kCount = 64;
+    GLBuffer destination;
+    glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, destination);
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCount * sizeof(kMarker), nullptr, GL_DYNAMIC_READ);
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, destination);
+
+    glEnable(GL_RASTERIZER_DISCARD);
+    glBeginTransformFeedback(GL_POINTS);
+    glDrawArraysInstanced(GL_POINTS, 0, kCount, 1);
+    glEndTransformFeedback();
+    glDisable(GL_RASTERIZER_DISCARD);
+    glFinish();
+    ASSERT_GL_NO_ERROR();
+
+    const float *captured = static_cast<const float *>(glMapBufferRange(
+        GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCount * sizeof(kMarker), GL_MAP_READ_BIT));
+    ASSERT_NE(nullptr, captured);
+    for (GLsizei vertex = 0; vertex < kCount; ++vertex)
+    {
+        EXPECT_EQ(kMarker[0], captured[vertex * 4 + 0]) << "vertex " << vertex;
+        EXPECT_EQ(kMarker[1], captured[vertex * 4 + 1]) << "vertex " << vertex;
+        EXPECT_EQ(kMarker[2], captured[vertex * 4 + 2]) << "vertex " << vertex;
+        EXPECT_EQ(kMarker[3], captured[vertex * 4 + 3]) << "vertex " << vertex;
+    }
+    glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
+}
+
 class HardenedTransformFeedbackTest : public TransformFeedbackTest
 {
   public:
Loading diff…

Original Bug Report

reported by [email protected]

[ANGLE/D3D11][WebGL] multiview InputLayout divisor overflow enables cross-site GPU data disclosure


Report description

[ANGLE/D3D11][WebGL] multiview InputLayout divisor overflow enables cross-site GPU data disclosure


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://github.com/google/angle/tree/main/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp


The problem

Please describe the technical details of the vulnerability

Vulnerability Description

ANGLE D3D11 uses two different calculations for the same multiview-adjusted vertex divisor.

InputLayoutCache.cpp builds the cache key with unchecked 32-bit arithmetic:

int divisorMultiplier =
    executable->usesMultiview() ? executable->getNumViews() : 1;

layout.addAttributeData(
    glslElementType, d3dSemantic, vertexFormatID,
    binding.getDivisor() * divisorMultiplier);

Source:

https://github.com/google/angle/blob/99ed84a1aabf593c30e86e4f7507990582ae7f54/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp

For num_views = 2 and WebGL divisor 0x80000000:

InputLayout cache-key divisor:
0x80000000 * 2 -> 0x00000000

The actual translated attribute uses the overflow-safe helper instead:

dynamicAttrib->divisor =
    GetMultiviewAdjustedDivisor(
        mAppliedNumViewsToDivisor,
        dynamicAttrib->binding->getDivisor());

GetMultiviewAdjustedDivisor() multiplies in 64 bits and clamps to GLuint max, so the same value becomes:

translated divisor = 0xffffffff

Sources:

https://github.com/google/angle/blob/99ed84a1aabf593c30e86e4f7507990582ae7f54/src/libANGLE/renderer/d3d/d3d11/VertexArray11.cpp

https://github.com/google/angle/blob/99ed84a1aabf593c30e86e4f7507990582ae7f54/src/libANGLE/renderer/renderer_utils.h

A divisor-0 draw first caches a D3D11 PER_VERTEX InputLayout under key 0.
The overflowing divisor also produces cache key 0, so ANGLE reuses that PER_VERTEX layout even though the current translated divisor is 0xffffffff and should be PER_INSTANCE.

The D3D11 InputLayout directly selects classification and step rate from the translated divisor when a new layout is created:

D3D11_INPUT_CLASSIFICATION inputClass =
    attrib.divisor > 0 ? D3D11_INPUT_PER_INSTANCE_DATA
                       : D3D11_INPUT_PER_VERTEX_DATA;

inputElement->InputSlotClass       = inputClass;
inputElement->InstanceDataStepRate = attrib.divisor;

Source:

https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp

For a huge logical divisor, WebGL/ANGLE only needs one source element. Dynamic attributes are copied into ANGLE’s shared streaming vertex buffer using the logical GL divisor:

https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/VertexDataManager.cpp

The streaming allocator suballocates attributes at mWritePosition and wraps with discard():

https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/VertexBuffer.cpp

On D3D11, normal streaming writes use D3D11_MAP_WRITE_NO_OVERWRITE; wrap/discard uses D3D11_MAP_WRITE_DISCARD:

https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp

Because the stale cached layout interprets the attacker’s one-element instanced attribute as PER_VERTEX, the native vertex fetch walks past the attacker’s legal 16-byte suballocation.

This was first reproduced across two distinct WebGLBuffer objects in one context, then escalated to two different sites and two independent WebGL contexts.

Attack Preconditions

  • Windows Chrome using ANGLE D3D11.
  • WebGL2 and OVR_multiview2 are available.
  • Victim site has WebGL vertex data present in the shared D3D11 streaming allocation before the attacker’s collision draw.
  • No Chrome flags, extension, privileged API, or user gesture is required by the PoC.

Tested renderer:

ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0)
Direct3D11 vs_5_0 ps_5_0, D3D11)

Reproduction Steps / POC

The attached minimal PoC uses two different sites:

attacker: http://127.0.0.1:21221
victim:   http://localhost:21221

The victim generates a random 4-byte RGBA secret, fills a 1 MiB WebGL dynamic vertex stream with it, and sends the attacker only SHA-256(secret).

The raw secret is never sent through postMessage.

  1. Extract the attached files.
  2. Run:
python .\server.py
  1. Open stock Chrome normally:
http://127.0.0.1:21221/attacker.html
  1. No flags are required.
  2. A successful reproduction prints:
VERDICT=CROSS_SITE_VICTIM_STREAM_BYTES_DISCLOSED
FINAL_REPRODUCED=true

Proven output

Stock Chrome 152.0.0.0 / Intel ANGLE D3D11 produced:

attacker origin:
http://127.0.0.1:21221

victim origin:
http://localhost:21221

victimSentRawSecret=false

victim commitment:
05cc7f90d3f70870ca0bb2d42c5c01f2579d74345783f3d147c6d057d5f8fd3c

attacker recovered:
[113,88,209,255]

SHA-256(attacker recovered bytes):
05cc7f90d3f70870ca0bb2d42c5c01f2579d74345783f3d147c6d057d5f8fd3c

commitmentMatch=true
commitmentMatches=62

62 is exactly 31 out-of-range pixels in each of the two multiview layers. Pixel 0 remained the attacker’s legal element; pixels 1-31 in both layers matched the victim-only secret.

Impact analysis

Cross-site GPU data disclosure between independent WebGL contexts

A malicious site can recover GPU vertex data originating from another site’s independent WebGL context.

The maximum demonstrated boundary is:

victim site
  -> victim WebGL2 context
  -> ANGLE/D3D11 streaming allocation
  -> attacker WebGL2 context
  -> attacker framebuffer/readPixels()

The victim sends only a cryptographic commitment, while the attacker independently recovers the raw value and produces the same SHA-256 hash.

Therefore this is not only a same-buffer stale-data issue or rendering correctness issue: the PoC demonstrates cross-site / cross-origin information disclosure through the GPU backend.

Root cause:

InputLayout cache key:
    uint32(divisor * numViews)

Translated/native divisor:
    clamp_uint32(uint64(divisor) * uint64(numViews))

These must use the same canonical adjusted divisor.

A direct fix is to build the InputLayout cache key from GetMultiviewAdjustedDivisor(...) (or from the already canonical translated divisor) instead of raw multiplication.


The cause

What version of Chrome have you found the security issue in?

152.0.7977.65+stable

No, it is not related to a crash.

Choose the type of vulnerability

Site Isolation Bypass

How would you like to be publicly acknowledged for your report?

Zabith Mohammed

View on issue tracker