CVE-2026-10889
Overview
Files Changed
src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cppsrc/tests/gl_tests/PixelLocalStorageTest.cpp
Patch
From a82644efc2694d422265e48e02e7c6715f2451e3 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 20 May 2026 14:47:19 -0400
Subject: [PATCH] Translator: Fix crash with (sideeffect,uniform)
... during monomorphisation pass. It can be very complicated to
actually fix this, so this change just drops the left-hand side of
comma. This is handled correctly in the IR.
Bug: chromium:513003797
Change-Id: I52bc58c04839f8f483fc7eb8a4d991f1b6eed3d3
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7863048
Auto-Submit: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp b/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
index 11db4e5..a583dad 100644
--- a/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
+++ b/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
@@ -75,7 +75,7 @@
*isSamplerInStructOut = true;
}
- node = asBinary->getLeft();
+ node = op == EOpComma ? asBinary->getRight() : asBinary->getLeft();
}
// Only interested in uniform opaque types. If a function call within another function uses
@@ -99,6 +99,13 @@
TIntermTyped *node,
TIntermSequence *replacementIndices)
{
+ // If this is a comma expression, throw away the left hand side. THIS IS INCORRECT, but it's
+ // overly complicated to try and support it. The IR already handles this correctly.
+ while (node->getAsBinaryNode() != nullptr && node->getAsBinaryNode()->getOp() == EOpComma)
+ {
+ node = node->getAsBinaryNode()->getRight();
+ }
+
TIntermTyped *withoutSideEffects = node->deepCopy();
for (TIntermBinary *asBinary = withoutSideEffects->getAsBinaryNode(); asBinary;
diff --git a/src/tests/gl_tests/PixelLocalStorageTest.cpp b/src/tests/gl_tests/PixelLocalStorageTest.cpp
index 44fd114..80a3af4 100644
--- a/src/tests/gl_tests/PixelLocalStorageTest.cpp
+++ b/src/tests/gl_tests/PixelLocalStorageTest.cpp
@@ -887,6 +887,92 @@
doR32Test(PixelLocalStorageTest::CoherencyMode::AlwaysNoncoherent);
}
+// Test that comma operator and passing PLS planes to functions work.
+TEST_P(PixelLocalStorageTest, CommaOperator)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_shader_pixel_local_storage"));
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_color_buffer_float"));
+
+ constexpr char kFS[] = R"(layout(r32f, binding=0) uniform highp pixelLocalANGLE plane1;
+ layout(binding=1, r32i) uniform highp ipixelLocalANGLE plane2;
+ layout(r32ui, binding=2) uniform highp upixelLocalANGLE plane3;
+
+ highp vec4 loadPlane(highp pixelLocalANGLE p)
+ {
+ return pixelLocalLoadANGLE(p);
+ }
+
+ highp ivec4 iloadPlane(highp ipixelLocalANGLE p)
+ {
+ return pixelLocalLoadANGLE(p);
+ }
+
+ highp uvec4 uloadPlane(highp pixelLocalANGLE unused, highp upixelLocalANGLE p)
+ {
+ return pixelLocalLoadANGLE(p);
+ }
+
+ int j = 0;
+ void incJ()
+ {
+ ++j;
+ }
+
+ void main()
+ {
+ highp int i = 0;
+ pixelLocalStoreANGLE(plane1, color + loadPlane((++i, plane1)));
+ pixelLocalStoreANGLE(plane2, ivec4(aux1) + iloadPlane((plane1, incJ(), plane2)));
+ pixelLocalStoreANGLE(plane3, uvec4(aux2) + uloadPlane((++i, plane1), (loadPlane((++i, incJ(), plane1)), plane3)));
+ if (i != 3 || j != 2)
+ {
+ pixelLocalStoreANGLE(plane3, uvec4(1234));
+ }
+ })";
+ mProgram.compile(kFS);
+
+ PLSTestTexture tex1(GL_R32F);
+ PLSTestTexture tex2(GL_R32I);
+ PLSTestTexture tex3(GL_R32UI);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexturePixelLocalStorageANGLE(0, tex1, 0, 0, GL_NONE);
+ glFramebufferTexturePixelLocalStorageANGLE(1, tex2, 0, 0, GL_NONE);
+ glFramebufferTexturePixelLocalStorageANGLE(2, tex3, 0, 0, GL_NONE);
+ glViewport(0, 0, W, H);
+ glDrawBuffers(0, nullptr);
+
+ glBeginPixelLocalStorageANGLE(
+ 3, GLenumArray({GL_LOAD_OP_ZERO_ANGLE, GL_LOAD_OP_ZERO_ANGLE, GL_LOAD_OP_ZERO_ANGLE}));
+
+ // Accumulate R in 4 separate passes.
+ mProgram.drawBoxes(
+ {{FULLSCREEN, {-1.5, 0, 0, 0}, {0x000000ff, 0, 0, 0}, {0x000000ff, 0, 0, 0}},
+ {FULLSCREEN, {-10.25, 0, 0, 0}, {0x0000ff00, 0, 0, 0}, {0x0000ff00, 0, 0, 0}},
+ {FULLSCREEN, {-100, 0, 0, 0}, {0x00ff0000, 0, 0, 0}, {0x00ff0000, 0, 0, 0}},
+ {FULLSCREEN, {.25, 0, 0, 0}, {-0x1000000, 0, 0, 0}, {0xff000000, 0, 0, 22}}},
+ UseBarriers::IfNotCoherent);
+
+ glEndPixelLocalStorageANGLE(3, GLenumArray({GL_STORE_OP_STORE_ANGLE, GL_STORE_OP_STORE_ANGLE,
+ GL_STORE_OP_STORE_ANGLE}));
+
+ // Incorrectly transformed without the IR. Test above makes sure there is no compiler crash
+ // without the IR.
+ ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
+
+ attachTexture2DToScratchFBO(tex1);
+ EXPECT_PIXEL_RECT32F_EQ(0, 0, W, H, GLColor32F(-111.5, 0, 0, 1));
+
+ attachTexture2DToScratchFBO(tex2);
+ EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(-1, 0, 0, 1));
+
+ attachTexture2DToScratchFBO(tex3);
+ EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0xffffffff, 0, 0, 1));
+
+ ASSERT_GL_NO_ERROR();
+}
+
// Check proper functioning of the clear value state.
TEST_P(PixelLocalStorageTest, ClearValues_rgba8)
{
Regression Test / PoC
diff --git a/src/tests/gl_tests/PixelLocalStorageTest.cpp b/src/tests/gl_tests/PixelLocalStorageTest.cpp
index 44fd114..80a3af4 100644
--- a/src/tests/gl_tests/PixelLocalStorageTest.cpp
+++ b/src/tests/gl_tests/PixelLocalStorageTest.cpp
@@ -887,6 +887,92 @@
doR32Test(PixelLocalStorageTest::CoherencyMode::AlwaysNoncoherent);
}
+// Test that comma operator and passing PLS planes to functions work.
+TEST_P(PixelLocalStorageTest, CommaOperator)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_shader_pixel_local_storage"));
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_color_buffer_float"));
+
+ constexpr char kFS[] = R"(layout(r32f, binding=0) uniform highp pixelLocalANGLE plane1;
+ layout(binding=1, r32i) uniform highp ipixelLocalANGLE plane2;
+ layout(r32ui, binding=2) uniform highp upixelLocalANGLE plane3;
+
+ highp vec4 loadPlane(highp pixelLocalANGLE p)
+ {
+ return pixelLocalLoadANGLE(p);
+ }
+
+ highp ivec4 iloadPlane(highp ipixelLocalANGLE p)
+ {
+ return pixelLocalLoadANGLE(p);
+ }
+
+ highp uvec4 uloadPlane(highp pixelLocalANGLE unused, highp upixelLocalANGLE p)
+ {
+ return pixelLocalLoadANGLE(p);
+ }
+
+ int j = 0;
+ void incJ()
+ {
+ ++j;
+ }
+
+ void main()
+ {
+ highp int i = 0;
+ pixelLocalStoreANGLE(plane1, color + loadPlane((++i, plane1)));
+ pixelLocalStoreANGLE(plane2, ivec4(aux1) + iloadPlane((plane1, incJ(), plane2)));
+ pixelLocalStoreANGLE(plane3, uvec4(aux2) + uloadPlane((++i, plane1), (loadPlane((++i, incJ(), plane1)), plane3)));
+ if (i != 3 || j != 2)
+ {
+ pixelLocalStoreANGLE(plane3, uvec4(1234));
+ }
+ })";
+ mProgram.compile(kFS);
+
+ PLSTestTexture tex1(GL_R32F);
+ PLSTestTexture tex2(GL_R32I);
+ PLSTestTexture tex3(GL_R32UI);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexturePixelLocalStorageANGLE(0, tex1, 0, 0, GL_NONE);
+ glFramebufferTexturePixelLocalStorageANGLE(1, tex2, 0, 0, GL_NONE);
+ glFramebufferTexturePixelLocalStorageANGLE(2, tex3, 0, 0, GL_NONE);
+ glViewport(0, 0, W, H);
+ glDrawBuffers(0, nullptr);
+
+ glBeginPixelLocalStorageANGLE(
+ 3, GLenumArray({GL_LOAD_OP_ZERO_ANGLE, GL_LOAD_OP_ZERO_ANGLE, GL_LOAD_OP_ZERO_ANGLE}));
+
+ // Accumulate R in 4 separate passes.
+ mProgram.drawBoxes(
+ {{FULLSCREEN, {-1.5, 0, 0, 0}, {0x000000ff, 0, 0, 0}, {0x000000ff, 0, 0, 0}},
+ {FULLSCREEN, {-10.25, 0, 0, 0}, {0x0000ff00, 0, 0, 0}, {0x0000ff00, 0, 0, 0}},
+ {FULLSCREEN, {-100, 0, 0, 0}, {0x00ff0000, 0, 0, 0}, {0x00ff0000, 0, 0, 0}},
+ {FULLSCREEN, {.25, 0, 0, 0}, {-0x1000000, 0, 0, 0}, {0xff000000, 0, 0, 22}}},
+ UseBarriers::IfNotCoherent);
+
+ glEndPixelLocalStorageANGLE(3, GLenumArray({GL_STORE_OP_STORE_ANGLE, GL_STORE_OP_STORE_ANGLE,
+ GL_STORE_OP_STORE_ANGLE}));
+
+ // Incorrectly transformed without the IR. Test above makes sure there is no compiler crash
+ // without the IR.
+ ANGLE_SKIP_TEST_IF(!getEGLWindow()->isFeatureEnabled(Feature::UseIr));
+
+ attachTexture2DToScratchFBO(tex1);
+ EXPECT_PIXEL_RECT32F_EQ(0, 0, W, H, GLColor32F(-111.5, 0, 0, 1));
+
+ attachTexture2DToScratchFBO(tex2);
+ EXPECT_PIXEL_RECT32I_EQ(0, 0, W, H, GLColor32I(-1, 0, 0, 1));
+
+ attachTexture2DToScratchFBO(tex3);
+ EXPECT_PIXEL_RECT32UI_EQ(0, 0, W, H, GLColor32UI(0xffffffff, 0, 0, 1));
+
+ ASSERT_GL_NO_ERROR();
+}
+
// Check proper functioning of the clear value state.
TEST_P(PixelLocalStorageTest, ClearValues_rgba8)
{
Original Bug Report
Memory Corruption in ANGLE via malformed comma expressions in PLS shaders
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in ANGLE’s shader translator allows comma expressions to bypass opaque-type checks, leading to incorrect function monomorphization. This results in an invalid map iterator dereference and wild pointer usage in the GPU process during shader compilation. On platforms where the GPU process is unsandboxed, such as Android, this could potentially lead to a sandbox escape.
Affected files:
third_party/angle/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cppthird_party/angle/src/compiler/translator/ParseContext.cppthird_party/angle/src/compiler/translator/tree_ops/RewritePixelLocalStorage.cppthird_party/angle/src/compiler/translator/IntermNode.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A memory corruption vulnerability potentially exists in the ANGLE shader translator due to improper handling of comma (sequence) expressions containing opaque types, specifically those related to Pixel Local Storage (PLS). When a comma expression is used to pass a PLS variable to a function, the translator fails to correctly identify and monomorphize the function call. This leads to a std::map::end() iterator dereference in the RewritePixelLocalStorage pass, resulting in wild pointer usage.
Root Cause Analysis
The vulnerability stems from three interacting issues in the translator’s tree operations:
-
Missing Opaque Type Check in
TParseContext::addComma: Insrc/compiler/translator/ParseContext.cpp, theaddCommafunction does not invokebinaryOpCommonCheck. Unlike other binary expressions, it fails to reject operands with opaque types. This allows expressions like(i++, pls)(whereplsis apixelLocalANGLEuniform) to be parsed as a valid AST node, even though opaque types are prohibited as operands to the sequence operator. -
Incorrect Traversal in
GetBaseUniform: Insrc/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp, theGetBaseUniformhelper function incorrectly handles binary nodes by always descending into the left operand. For a comma expression, the semantic value is the right operand. By walking left,GetBaseUniformencounters the side-effect (e.g.,i++) instead of the uniform. This causes the function to returnnullptr, preventing theMonomorphizeUnsupportedFunctionspass from specializing the function call. -
Map Iterator Dereference in
RewritePixelLocalStorage: TheRewritePixelLocalStoragepass expects all functions using PLS types to have been monomorphized. Because specialization was skipped, the pass encounters a PLS symbol that is a function parameter. Function parameters lack layout bindings (defaulting to -1). WhenPLSBackingStoreMap::findattempts to look up this binding:auto iter = mMap.find(binding); // binding is -1 return iter->second; // Dereferences mMap.end() in release buildsDereferencing the
end()iterator returns a wildTVariable*pointer, which is then used to construct new AST nodes, leading to memory corruption or type confusion in the GPU process.
Potential Attack Vector
An attacker with control over a renderer process could potentially trigger this vulnerability by submitting a crafted GLSL shader. The following fragment shader illustrates the potential trigger:
#version 300 es
#extension GL_ANGLE_shader_pixel_local_storage : require
precision highp float;
layout(binding=0, rgba8) uniform highp pixelLocalANGLE pls;
vec4 load_wrapper(pixelLocalANGLE p) {
return pixelLocalLoadANGLE(p);
}
out vec4 color;
void main() {
int i = 0;
// Comma expression bypasses checks and prevents monomorphization
color = load_wrapper((i++, pls));
}
Suggested Fix
- Update
TParseContext::addCommainParseContext.cppto include opaque type checks, similar tobinaryOpCommonCheckoraddTernarySelection. - Correct
GetBaseUniforminMonomorphizeUnsupportedFunctions.cppto handleEOpCommaby descending into the right operand instead of the left. - Harden
PLSBackingStoreMap::findinRewritePixelLocalStorage.cppto explicitly check if the iterator is valid before dereferencing, even in release builds.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.