Critical watch CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Sub-componentTranslator
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513003797
Fix commita82644efc269 (angle/angle) +94/-1
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-05-21 (public git)
Disclosed2026-06-02
Patch gap12 days — fix public in source before the advisory

Background

`MonomorphizeUnsupportedFunctions`
An ANGLE translator pass that rewrites function calls passing opaque uniforms (samplers, pixelLocalANGLE planes) 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 operand b while the left operand a is 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 TIntermTyped clone 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.

Key insight
The pass ignored comma-expression semantics and resolved the base uniform from the left (side-effect) operand instead of the right (value) operand, so the “base uniform” it indexed was the wrong node — a correctness bug in AST traversal that surfaces as an out-of-bounds array access.

Attack Path

  1. 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)).
  2. Trigger monomorphization The call meets the pass’s opaque-uniform criterion, invoking MonomorphizeUnsupportedFunctions on the argument.
  3. Mis-resolve base uniform The helper walks to getLeft() (side effect) instead of getRight(), and the index builder deepCopys the wrong subtree.
  4. Emit malformed indexing Corrupted index bookkeeping produces indices outside the uniform / PLS array bounds.
  5. Out-of-bounds read Translation-driven access to the array metadata reads past its end, crashing the compiler.

Impact Assessment

A malicious or malformed shader can drive an out-of-bounds read inside the ANGLE shader translator, yielding a crash and potential information disclosure. Because shader compilation is reachable from untrusted web content via WebGL / WebGPU, the surface is remotely triggerable in the GPU process. The impact is bounded to a read-side OOB during translation rather than direct memory corruption of runtime buffers.

Files Changed

  • src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
  • src/tests/gl_tests/PixelLocalStorageTest.cpp

Audit Directions

  • Comma-expression traversal
    Audit every AST walk that resolves a base value or lvalue for uses of getLeft()/getRight() on EOpComma nodes; the value lives in the right operand, and left-operand resolution silently picks the wrong subtree.
  • Opaque-uniform / PLS index bookkeeping
    Review other translator passes that build replacement indices for samplers or pixelLocalANGLE planes to ensure the base-uniform node they clone actually resolves to the uniform, not a wrapping expression.
  • `deepCopy` on unnormalized nodes
    Flag deepCopy calls that assume a normalized node shape; peel comma and other wrapper operators first so the clone matches the semantically-relevant subexpression.
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)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
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)
 {
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/angle/src/compiler/translator/ParseContext.cpp
  • third_party/angle/src/compiler/translator/tree_ops/RewritePixelLocalStorage.cpp
  • third_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:

  1. Missing Opaque Type Check in TParseContext::addComma: In src/compiler/translator/ParseContext.cpp, the addComma function does not invoke binaryOpCommonCheck. Unlike other binary expressions, it fails to reject operands with opaque types. This allows expressions like (i++, pls) (where pls is a pixelLocalANGLE uniform) to be parsed as a valid AST node, even though opaque types are prohibited as operands to the sequence operator.

  2. Incorrect Traversal in GetBaseUniform: In src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp, the GetBaseUniform helper 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, GetBaseUniform encounters the side-effect (e.g., i++) instead of the uniform. This causes the function to return nullptr, preventing the MonomorphizeUnsupportedFunctions pass from specializing the function call.

  3. Map Iterator Dereference in RewritePixelLocalStorage: The RewritePixelLocalStorage pass 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). When PLSBackingStoreMap::find attempts to look up this binding:

    auto iter = mMap.find(binding); // binding is -1
    return iter->second;            // Dereferences mMap.end() in release builds
    

    Dereferencing the end() iterator returns a wild TVariable* 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

  1. Update TParseContext::addComma in ParseContext.cpp to include opaque type checks, similar to binaryOpCommonCheck or addTernarySelection.
  2. Correct GetBaseUniform in MonomorphizeUnsupportedFunctions.cpp to handle EOpComma by descending into the right operand instead of the left.
  3. Harden PLSBackingStoreMap::find in RewritePixelLocalStorage.cpp to 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.

View on issue tracker