CVE-2026-78948
Overview
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) orvaryingvariables a vertex shader passes to the next stage. Their size is measured in scalar components, for examplevec4= 4 andmat4= 16, multiplied by array sizes.- Driver workaround features
- ANGLE
FeaturesGLflags such aslimitOutputVaryingsTo256AtCompileTime, turned on per vendor inrenderergl_utils.cppand passed to the translator asShCompileOptionsbits such aslimitOutputVaryingsTo256. - `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.
Attack Path
- 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.
- 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 vec4declarations, and arrays, matrices, structs or output interface blocks would also count. - 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
GLSLValidationTest_ES3_LimitOutputVaryingssrc/tests/gl_tests/GLSLValidationTest.cpp |
modified |
Files Changed
src/compiler/translator/ParseContext.cppsrc/compiler/translator/ParseContext.hutil/autogen/angle_features_autogen.hsrc/libANGLE/renderer/gl/ShaderGL.cppsrc/libANGLE/renderer/gl/renderergl_utils.cppinclude/platform/autogen/FeaturesGL_autogen.hsrc/tests/gl_tests/GLSLValidationTest.cpputil/autogen/angle_features_autogen.cppinclude/GLSLANG/ShaderLang.hinclude/platform/gl_features.json
Audit Directions
- Other unbounded declaration countsCheck 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
validateMaxPerStageUniformBlocksAtCompileTimePowerVR workaround. - Vendor-gated mitigationsThe 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 correctnessAudit
GetTypeComponentCount-style helpers for under-counting: arrays of arrays, nested structs in blocks, packed orflatqualifiers, and redeclared built-ins skipped by theSymbolType::BuiltInexemption. Any of these could let a shader exceed the intended cap.
Patch
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,
Regression Test / PoC
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));