CVE-2026-113082
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
HardenedTransformFeedbackTestsrc/tests/gl_tests/TransformFeedbackTest.cpp |
modified |
Files Changed
src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cppsrc/tests/gl_tests/TransformFeedbackTest.cppsrc/tests/gl_tests/MultiviewDrawTest.cpp
Patch
From 48a16bd63d2d5ac42b133ff4da511928af02eb97 Mon Sep 17 00:00:00 2001
From: Le Hoang Quyen <lehoangquyen@chromium.org>
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 <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Quyen Le <lehoangquyen@google.com>
Auto-Submit: Quyen Le <lehoangquyen@google.com>
---
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:
Regression Test / PoC
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:
Original Bug Report
[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:
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:
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_multiview2are 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.
- Extract the attached files.
- Run:
python .\server.py
- Open stock Chrome normally:
http://127.0.0.1:21221/attacker.html
- No flags are required.
- 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
Is the security issue related to a crash?
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
- http://127.0.0.1:21221
- http://127.0.0.1:21221/attacker.html
- http://localhost:21221
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://github.com/google/angle/blob/99ed84a1aabf593c30e86e4f7507990582ae7f54/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp
- 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
- https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/VertexBuffer.cpp
- https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/VertexDataManager.cpp
- https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp
- https://github.com/google/angle/blob/main/src/libANGLE/renderer/d3d/d3d11/VertexBuffer11.cpp
- https://github.com/google/angle/tree/main/src/libANGLE/renderer/d3d/d3d11/InputLayoutCache.cpp