CVE-2026-10889
Overview
Background
- `MonomorphizeUnsupportedFunctions`
- An ANGLE translator pass that rewrites function calls passing opaque uniforms (samplers,
pixelLocalANGLEplanes) into specialized (monomorphized) variants, walking the argument AST to locate the base uniform. - `EOpComma`
- The AST binary-node operator for the GLSL/C comma expression
(a, b), whose value is the right operandbwhile the left operandais evaluated only for side effects. - Opaque uniform
- A sampler or pixel-local-storage plane handle that cannot be copied by value, so the pass must resolve the underlying uniform declaration and any indexing to build a valid replacement.
- `deepCopy`
- A
TIntermTypedclone used to reconstruct the argument expression (without side effects) when synthesizing the monomorphized call and its index bookkeeping.
Root Cause Analysis
When an opaque-uniform argument was wrapped in a comma expression such as (sideeffect, uniform), the extraction helper walked to asBinary->getLeft() — the side-effect operand — rather than asBinary->getRight(), which per C/GLSL comma semantics is the actual uniform value. This mis-identification meant the pass treated the wrong subtree as the base uniform, corrupting the monomorphization and replacement-index bookkeeping. Separately, the replacement-index builder called node->deepCopy() without first peeling comma nodes, so it cloned a subtree that did not resolve to the intended uniform. The result was a malformed shader whose generated indexing addressed the uniform / PLS array outside its declared bounds. Because the pass runs during translation, the bad index bookkeeping drives an out-of-bounds read of the array metadata (a crash/OOB during compilation).
The fix follows the comma to the right operand in both the walk (op == EOpComma ? getRight() : getLeft()) and via a while loop that strips leading EOpComma nodes before deepCopy, matching the IR’s already-correct handling.
Attack Path
- Craft comma-wrapped uniform argument
Author a shader that passes an opaque uniform to a function as a comma expression, e.g.
f((sideeffect, sampler)). - Trigger monomorphization
The call meets the pass’s opaque-uniform criterion, invoking
MonomorphizeUnsupportedFunctionson the argument. - Mis-resolve base uniform
The helper walks to
getLeft()(side effect) instead ofgetRight(), and the index builderdeepCopys the wrong subtree. - Emit malformed indexing Corrupted index bookkeeping produces indices outside the uniform / PLS array bounds.
- Out-of-bounds read Translation-driven access to the array metadata reads past its end, crashing the compiler.
Impact Assessment
Files Changed
src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cppsrc/tests/gl_tests/PixelLocalStorageTest.cpp
Audit Directions
- Comma-expression traversalAudit every AST walk that resolves a base value or lvalue for uses of
getLeft()/getRight()onEOpCommanodes; the value lives in the right operand, and left-operand resolution silently picks the wrong subtree. - Opaque-uniform / PLS index bookkeepingReview other translator passes that build replacement indices for samplers or
pixelLocalANGLEplanes to ensure the base-uniform node they clone actually resolves to the uniform, not a wrapping expression. - `deepCopy` on unnormalized nodesFlag
deepCopycalls that assume a normalized node shape; peel comma and other wrapper operators first so the clone matches the semantically-relevant subexpression.
Patch
From a82644efc2694d422265e48e02e7c6715f2451e3 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> 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 <[email protected]> Commit-Queue: Geoff Lang <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- 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.