High watch CVSS 9.6 chrome OOB 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in WebGL
DescriptionBuffer overflow in WebGL
ComponentWebGL
Sub-componentGL
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker529991907
Fix commit318948133839 (angle/angle) +137/-3
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-01 (per vendor advisory)
Fix landed2026-07-18 (public git)
Disclosed2026-08-25
Patch gap38 days — fix public in source before the advisory

Background

ANGLE shader translator
ANGLE parses and validates WebGL/GLES shaders in TParseContext (src/compiler/translator/ParseContext.cpp). It then emits shader source for the native driver, which on the OpenGL backend compiles that source again with its own compiler.
Vertex output varyings
out (ES 3.0) or varying variables a vertex shader passes to the next stage. Their size is measured in scalar components, for example vec4 = 4 and mat4 = 16, multiplied by array sizes.
Driver workaround features
ANGLE FeaturesGL flags such as limitOutputVaryingsTo256AtCompileTime, turned on per vendor in renderergl_utils.cpp and passed to the translator as ShCompileOptions bits such as limitOutputVaryingsTo256.
`angle::CheckedNumeric`
an overflow-checked integer type. In this patch it saturates the running component count instead of letting it wrap.

Root Cause Analysis

The commit describes this as a workaround for “a bug in the driver’s shader compiler” in Imagination PowerVR OpenGL ES drivers. The buffer overflow itself is in the driver, not in ANGLE code shown in the diff. Before the patch, ANGLE passed vertex shaders with arbitrarily many declared output varying components to that driver compiler. The added comment says the new 1024-component cap is “much higher than the device limits (mResources.MaxVertexOutputVectors * 4)”. So shaders that declare far more varyings than the device supports were reaching the driver compiler, which mishandles them. Declarations are counted even if the varyings are unused, which suggests the earlier link-time limits did not prevent the driver compile step (inferred).

The fix adds TParseContext::addAndCheckOutputVaryings. For vertex shaders with the option set, it sums GetTypeComponentCount over non-built-in IsVaryingOut variables, including interface-block fields and instances, using a saturating CheckedNumeric. Past 1024 components it raises a compile error, so the shader never reaches the driver. The check is enabled only when limitOutputVaryingsTo256AtCompileTime is on, which renderergl_utils.cpp sets only for IsPowerVR(vendor) on ANGLE’s GL backend. Other GPUs and backends are unchanged.

Key insight
ANGLE relied on the native driver to reject over-sized varying declarations, and the PowerVR driver compiler overflows instead. The fix enforces a compile-time cap on declared vertex output components in ANGLE’s own parser before the source is handed to the driver.

Attack Path

  1. WebGL on an affected device A web page creates a WebGL context on a device whose GPU uses Imagination PowerVR OpenGL ES drivers through ANGLE’s GL backend. This is the only configuration where the new workaround is enabled.
  2. Shader with excessive output varyings The page compiles a vertex shader that declares more than 1024 output varying components. The regression test uses 257 out highp vec4 declarations, and arrays, matrices, structs or output interface blocks would also count.
  3. Translator forwards it unchecked Before the fix, ANGLE’s parser had no running count of declared output components, so the translated shader went to the PowerVR driver’s compiler.
  4. Driver compiler overflow The driver’s shader compiler mishandles the oversized declaration set. The advisory calls this a buffer overflow; it happens inside the vendor driver in the GPU process. Any control over the corruption is (inferred).

Impact Assessment

Web content that can create a WebGL context can trigger a buffer overflow in the PowerVR GL driver’s shader compiler, inside Chrome’s GPU process. Exploitability beyond a crash, and any sandbox-escape value, are (inferred). The bug is limited to PowerVR hardware on ANGLE’s OpenGL/GLES backend. The workaround now applies to all context types, not only WebGL. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.

Changed Functions

FunctionChangeNotes
GLSLValidationTest_ES3_LimitOutputVaryings
src/tests/gl_tests/GLSLValidationTest.cpp
modified

Files Changed

  • src/compiler/translator/ParseContext.cpp
  • src/compiler/translator/ParseContext.h
  • util/autogen/angle_features_autogen.h
  • src/libANGLE/renderer/gl/ShaderGL.cpp
  • src/libANGLE/renderer/gl/renderergl_utils.cpp
  • include/platform/autogen/FeaturesGL_autogen.h
  • src/tests/gl_tests/GLSLValidationTest.cpp
  • util/autogen/angle_features_autogen.cpp
  • include/GLSLANG/ShaderLang.h
  • include/platform/gl_features.json

Audit Directions

  • Other unbounded declaration counts
    Check whether ANGLE limits input varyings, fragment outputs, uniforms, samplers, SSBOs and geometry/tessellation I/O at parse time on GL backends. Device limits are often enforced only at link time, after the driver has already compiled oversized declarations. Compare with the existing validateMaxPerStageUniformBlocksAtCompileTime PowerVR workaround.
  • Vendor-gated mitigations
    The cap only applies on PowerVR. Review whether other mobile GL drivers (Mali, Adreno) have similar compiler-side limits, and whether a vendor-detection mismatch could leave affected devices without the workaround.
  • Component accounting correctness
    Audit GetTypeComponentCount-style helpers for under-counting: arrays of arrays, nested structs in blocks, packed or flat qualifiers, and redeclared built-ins skipped by the SymbolType::BuiltIn exemption. Any of these could let a shader exceed the intended cap.
From 318948133839dcf445f6b39854538f776e3302b3 Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Fri, 17 Jul 2026 15:24:36 -0700
Subject: [PATCH] Reland "GL: Limit declared vertex output varying components"

This reverts commit 3cf1a6c74785246ed0c29f58816632e05d7d6616.

Add a workaround which, when enabled, tracks the number of output
components from the vertex shader and fails the compile if there's an
attempt to exceed a specific limit (1024).

Relative to the original commit, apply a much higher compile time
limit which is unlikely to be reached by any test or real-world
application, and apply it to all context types, not just WebGL's.

Apply this workaround on Imagination's PowerVR OpenGL ES drivers to
work around a bug in the driver's shader compiler.

Co-authored with Gemini.

Test: angle_end2end_tests \
  --gtest_filter=GLSLValidationTest_ES3_LimitOutputVaryings.\
  TooManyDeclaredVertexOutputComponents
TAG=agy
CONV=8ed05bff-eb25-4de9-92b8-d097e4c0abae

Bug: chromium:529991907
Change-Id: I79d0bd117d6e0d6412011d361183484f088243e6
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8114814
Commit-Queue: Kenneth Russell <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
---
diff --git a/include/GLSLANG/ShaderLang.h b/include/GLSLANG/ShaderLang.h
index a725c38..dde82d2 100644
--- a/include/GLSLANG/ShaderLang.h
+++ b/include/GLSLANG/ShaderLang.h
@@ -26,7 +26,7 @@
 
 // Version number for shader translation API.
 // It is incremented every time the API changes.
-#define ANGLE_SH_VERSION 417
+#define ANGLE_SH_VERSION 418
 
 enum ShShaderSpec
 {
@@ -179,8 +179,8 @@
     // If requested, validates the AST after every transformation.  Useful for debugging.
     uint64_t validateAST : 1;
 
-    // placeholder bit for removed validateLoopIndexing option.
-    uint64_t unused3 : 1;
+    // Limit the number of output varyings allowed in vertex shaders to work around driver bugs.
+    uint64_t limitOutputVaryingsTo256 : 1;
 
     // Emits #line directives in HLSL.
     uint64_t lineDirectives : 1;
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index 127cccf..789e621 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -942,6 +942,14 @@
                 "Split full-image level 0 PBO uploads via TexSubImage2D into two calls to work around driver bugs."
             ],
             "issue": "http://crbug.com/496807874"
+        },
+        {
+            "name": "limit_output_varyings_to_256_at_compile_time",
+            "category": "Workarounds",
+            "description": [
+                "Limit the number of declared varying components at compile time to work around a PowerVR driver bug."
+            ],
+            "issue": "http://crbug.com/529991907"
         }
     ]
 }
diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index ddcf9bc..7745cbe 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -498,6 +498,31 @@
     structDeclaration->appendDeclarator(structDeclarator);
     return structDeclaration;
 }
+
+unsigned int GetTypeComponentCount(const TType &type)
+{
+    unsigned int components = 0;
+    if (type.getBasicType() == EbtInterfaceBlock)
+    {
+        for (const TField *field : type.getInterfaceBlock()->fields())
+        {
+            components += GetTypeComponentCount(*field->type());
+        }
+    }
+    else if (type.getStruct())
+    {
+        for (const TField *field : type.getStruct()->fields())
+        {
+            components += GetTypeComponentCount(*field->type());
+        }
+    }
+    else
+    {
+        components = static_cast<unsigned int>(type.getNominalSize()) * type.getSecondarySize();
+    }
+    components *= type.getArraySizeProduct();
+    return components;
+}
 }  // namespace
 
 // This tracks each binding point's current default offset for inheritance of subsequent
@@ -577,6 +602,7 @@
       mNumViews(-1),
       mMaxUniformBlocks(GetMaxUniformBlocksForShaderType(mShaderType, options, resources)),
       mNumUniformBlocks(0),
+      mNumOutputVaryingComponents(0),
       mDeclaringFunction(false),
       mDeclaringMain(false),
       mMainFunction(nullptr),
@@ -2343,6 +2369,7 @@
         error(line, "redefinition", identifier);
         return false;
     }
+    addAndCheckOutputVaryings(**variable, line);
 
     if (!checkIsNonVoid(line, identifier, type->getBasicType()))
     {
@@ -7035,6 +7062,7 @@
                 error(field->line(), "redefinition of an interface block member name",
                       field->name());
             }
+            addAndCheckOutputVaryings(*fieldVariable, field->line());
 
             // Don't declare variables for fields of nameless interface blocks in the IR, just
             // remember to implicitly index the instance variable when referenced.
@@ -7064,6 +7092,7 @@
         {
             error(instanceLine, "redefinition of an interface block instance name", instanceName);
         }
+        addAndCheckOutputVaryings(*instanceVariable, instanceLine);
     }
 
     TIntermSymbol *blockSymbol = new TIntermSymbol(instanceVariable);
@@ -10768,6 +10797,44 @@
     return numErrors() == 0;
 }
 
+void TParseContext::addAndCheckOutputVaryings(const TVariable &variable, const TSourceLoc &line)
+{
+    if (mShaderType != GL_VERTEX_SHADER)
+    {
+        return;
+    }
+
+    if (!mCompileOptions.limitOutputVaryingsTo256)
+    {
+        return;
+    }
+
+    if (variable.symbolType() == SymbolType::BuiltIn)
+    {
+        return;
+    }
+
+    if (!IsVaryingOut(variable.getType().getQualifier()))
+    {
+        return;
+    }
+
+    angle::CheckedNumeric<unsigned int> checkedNum = mNumOutputVaryingComponents;
+    checkedNum += GetTypeComponentCount(variable.getType());
+    mNumOutputVaryingComponents =
+        checkedNum.ValueOrDefault(std::numeric_limits<unsigned int>::max());
+
+    // The cap to 256 vec4s = 1024 components seems somewhat arbitrary, but this is intended as a
+    // workaround for a specific driver bug, and this limit being much
+    // higher than the device limits (mResources.MaxVertexOutputVectors *
+    // 4), it avoids regressing both tests and applications.
+    if (mNumOutputVaryingComponents > 1024)
+    {
+        error(line, "Too many declared shader output varying components for this device",
+              variable.name());
+    }
+}
+
 //
 // Parse an array of strings using yyparse.
 //
diff --git a/src/compiler/translator/ParseContext.h b/src/compiler/translator/ParseContext.h
index 3d62dfa..d086955 100644
--- a/src/compiler/translator/ParseContext.h
+++ b/src/compiler/translator/ParseContext.h
@@ -613,6 +613,7 @@
                          const TType *type,
                          GeomTessArray sized,
                          TVariable **variable);
+    void addAndCheckOutputVaryings(const TVariable &variable, const TSourceLoc &line);
 
     void checkNestingLevel(const TSourceLoc &line);
     bool checkCase(const TSourceLoc &line, int64_t caseValue, const char *caseOrDefault);
@@ -895,6 +896,9 @@
     // Current count of declared uniform blocks.
     unsigned int mNumUniformBlocks;
 
+    // Current count of declared output varying components.
+    unsigned int mNumOutputVaryingComponents;
+
     // Keeps track of whether any of the built-ins that can be redeclared (see
     // IsRedeclarableBuiltIn()) has been marked as invariant/precise before the possible
     // redeclaration.
diff --git a/src/libANGLE/renderer/gl/ShaderGL.cpp b/src/libANGLE/renderer/gl/ShaderGL.cpp
index 02adcce..c23fccd 100644
--- a/src/libANGLE/renderer/gl/ShaderGL.cpp
+++ b/src/libANGLE/renderer/gl/ShaderGL.cpp
@@ -271,6 +271,11 @@
         options->expandFragmentOutputsToVec4 = true;
     }
 
+    if (features.limitOutputVaryingsTo256AtCompileTime.enabled)
+    {
+        options->limitOutputVaryingsTo256 = true;
+    }
+
     return std::shared_ptr<ShaderTranslateTask>(
         new ShaderTranslateTaskGL(functions, mShaderID, contextGL->hasNativeParallelCompile()));
 }
diff --git a/src/libANGLE/renderer/gl/renderergl_utils.cpp b/src/libANGLE/renderer/gl/renderergl_utils.cpp
index 5cbbd9f..8c78bae 100644
--- a/src/libANGLE/renderer/gl/renderergl_utils.cpp
+++ b/src/libANGLE/renderer/gl/renderergl_utils.cpp
@@ -2834,6 +2834,10 @@
     ANGLE_FEATURE_CONDITION(features, validateMaxPerStageUniformBlocksAtCompileTime,
                             IsPowerVR(vendor));
 
+    // Some drivers have compilation issues when shaders declare too many output varyings.
+    // crbug.com/529991907
+    ANGLE_FEATURE_CONDITION(features, limitOutputVaryingsTo256AtCompileTime, IsPowerVR(vendor));
+
     // Mac Intel drivers are unable to allocate buffers larger than ~1gb
     ANGLE_FEATURE_CONDITION(features, limitMaxBufferSizeTo1gb, isApple && isIntel);
 }
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 014ab69..28a44aa 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -9469,6 +9469,39 @@
     validateError(GL_FRAGMENT_SHADER, kFS, "version");
 }
 
+class GLSLValidationTest_ES3_LimitOutputVaryings : public GLSLValidationTest_ES3
+{};
+
+// Regression test for crbug.com/529991907.
+// Verify that compiling a shader with up to 1024 output varying components
+// succeeds, and exceeding 1024 components is rejected at compile time.
+TEST_P(GLSLValidationTest_ES3_LimitOutputVaryings, TooManyDeclaredVertexOutputComponents)
+{
+    ANGLE_SKIP_TEST_IF(
+        !getEGLWindow()->isFeatureEnabled(Feature::LimitOutputVaryingsTo256AtCompileTime));
+
+    constexpr int kMaxVectors = 1024 / 4;
+
+    std::stringstream vsValid;
+    vsValid << "#version 300 es\n";
+    for (int i = 0; i < kMaxVectors; ++i)
+    {
+        vsValid << "out highp vec4 v" << i << ";\n";
+    }
+    vsValid << "void main() { gl_Position = vec4(0.0); }\n";
+    validateSuccess(GL_VERTEX_SHADER, vsValid.str().c_str());
+
+    std::stringstream vsInvalid;
+    vsInvalid << "#version 300 es\n";
+    for (int i = 0; i < kMaxVectors + 1; ++i)
+    {
+        vsInvalid << "out highp vec4 v" << i << ";\n";
+    }
+    vsInvalid << "void main() { gl_Position = vec4(0.0); }\n";
+    validateError(GL_VERTEX_SHADER, vsInvalid.str().c_str(),
+                  "Too many declared shader output varying components for this device");
+}
+
 }  // namespace
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(GLSLValidationTest);
@@ -9491,6 +9524,11 @@
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(WebGL2GLSLValidationTest);
 ANGLE_INSTANTIATE_TEST_ES3(WebGL2GLSLValidationTest);
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(GLSLValidationTest_ES3_LimitOutputVaryings);
+ANGLE_INSTANTIATE_TEST(GLSLValidationTest_ES3_LimitOutputVaryings,
+                       ES3_OPENGL().enable(Feature::LimitOutputVaryingsTo256AtCompileTime),
+                       ES3_OPENGLES().enable(Feature::LimitOutputVaryingsTo256AtCompileTime));
+
 ANGLE_INSTANTIATE_TEST_ES2_AND(WebGLGLSLValidationExtensionDisableTest,
                                ES2_OPENGL().enable(Feature::AllowExtensionDisableAfterNonPpTokens));
 
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index 9f60d6b..2b4a23e 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -722,6 +722,12 @@
         &members,
     };
 
+    FeatureInfo limitOutputVaryingsTo256AtCompileTime = {
+        "limitOutputVaryingsTo256AtCompileTime",
+        FeatureCategory::OpenGLWorkarounds,
+        &members,
+    };
+
 };
 
 inline FeaturesGL::FeaturesGL()  = default;
diff --git a/util/autogen/angle_features_autogen.cpp b/util/autogen/angle_features_autogen.cpp
index adce1f1..2b52609 100644
--- a/util/autogen/angle_features_autogen.cpp
+++ b/util/autogen/angle_features_autogen.cpp
@@ -246,6 +246,7 @@
     {Feature::LimitMaxMSAASamplesTo4, "limitMaxMSAASamplesTo4"},
     {Feature::LimitMaxStorageBufferSize, "limitMaxStorageBufferSize"},
     {Feature::LimitMaxTextureBytesTo1MB, "limitMaxTextureBytesTo1MB"},
+    {Feature::LimitOutputVaryingsTo256AtCompileTime, "limitOutputVaryingsTo256AtCompileTime"},
     {Feature::LimitSampleCountTo2, "limitSampleCountTo2"},
     {Feature::LimitWebglMaxTextureSizeTo4096, "limitWebglMaxTextureSizeTo4096"},
     {Feature::LimitWebglMaxTextureSizeTo8192, "limitWebglMaxTextureSizeTo8192"},
diff --git a/util/autogen/angle_features_autogen.h b/util/autogen/angle_features_autogen.h
index 09308ab..16733c1 100644
--- a/util/autogen/angle_features_autogen.h
+++ b/util/autogen/angle_features_autogen.h
@@ -246,6 +246,7 @@
     LimitMaxMSAASamplesTo4,
     LimitMaxStorageBufferSize,
     LimitMaxTextureBytesTo1MB,
+    LimitOutputVaryingsTo256AtCompileTime,
     LimitSampleCountTo2,
     LimitWebglMaxTextureSizeTo4096,
     LimitWebglMaxTextureSizeTo8192,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index 014ab69..28a44aa 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -9469,6 +9469,39 @@
     validateError(GL_FRAGMENT_SHADER, kFS, "version");
 }
 
+class GLSLValidationTest_ES3_LimitOutputVaryings : public GLSLValidationTest_ES3
+{};
+
+// Regression test for crbug.com/529991907.
+// Verify that compiling a shader with up to 1024 output varying components
+// succeeds, and exceeding 1024 components is rejected at compile time.
+TEST_P(GLSLValidationTest_ES3_LimitOutputVaryings, TooManyDeclaredVertexOutputComponents)
+{
+    ANGLE_SKIP_TEST_IF(
+        !getEGLWindow()->isFeatureEnabled(Feature::LimitOutputVaryingsTo256AtCompileTime));
+
+    constexpr int kMaxVectors = 1024 / 4;
+
+    std::stringstream vsValid;
+    vsValid << "#version 300 es\n";
+    for (int i = 0; i < kMaxVectors; ++i)
+    {
+        vsValid << "out highp vec4 v" << i << ";\n";
+    }
+    vsValid << "void main() { gl_Position = vec4(0.0); }\n";
+    validateSuccess(GL_VERTEX_SHADER, vsValid.str().c_str());
+
+    std::stringstream vsInvalid;
+    vsInvalid << "#version 300 es\n";
+    for (int i = 0; i < kMaxVectors + 1; ++i)
+    {
+        vsInvalid << "out highp vec4 v" << i << ";\n";
+    }
+    vsInvalid << "void main() { gl_Position = vec4(0.0); }\n";
+    validateError(GL_VERTEX_SHADER, vsInvalid.str().c_str(),
+                  "Too many declared shader output varying components for this device");
+}
+
 }  // namespace
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(GLSLValidationTest);
@@ -9491,6 +9524,11 @@
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(WebGL2GLSLValidationTest);
 ANGLE_INSTANTIATE_TEST_ES3(WebGL2GLSLValidationTest);
 
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(GLSLValidationTest_ES3_LimitOutputVaryings);
+ANGLE_INSTANTIATE_TEST(GLSLValidationTest_ES3_LimitOutputVaryings,
+                       ES3_OPENGL().enable(Feature::LimitOutputVaryingsTo256AtCompileTime),
+                       ES3_OPENGLES().enable(Feature::LimitOutputVaryingsTo256AtCompileTime));
+
 ANGLE_INSTANTIATE_TEST_ES2_AND(WebGLGLSLValidationExtensionDisableTest,
                                ES2_OPENGL().enable(Feature::AllowExtensionDisableAfterNonPpTokens));
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.