CVE-2026-79189
Overview
Files Changed
src/compiler/translator/ParseContext.cppsrc/compiler/translator/ParseContext.hsrc/tests/gl_tests/GLSLValidationTest.cpp
Patch
From 3d3c181bdcf2aa1e7b0f05637247479e56ca075e Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 21 Jul 2026 13:44:48 -0400
Subject: [PATCH] Translator: Check gl_FragData passed to function
... when gl_SecondaryFragDataEXT is used.
Bug: chromium:536505721
Change-Id: I644862db03603892b4bdb493a7c2d0a4b694e46d
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8132423
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Alexey Knyazev <lexa.knyazev@gmail.com>
---
diff --git a/src/compiler/translator/ParseContext.cpp b/src/compiler/translator/ParseContext.cpp
index 7773434..74bc544 100644
--- a/src/compiler/translator/ParseContext.cpp
+++ b/src/compiler/translator/ParseContext.cpp
@@ -3338,6 +3338,22 @@
}
}
+void TParseContext::functionCallFragDataCheck(const TFunction *fnCandidate,
+ TIntermAggregate *fnCall)
+{
+ for (size_t i = 0; i < fnCandidate->getParamCount(); ++i)
+ {
+ TIntermTyped *argument = (*fnCall->getSequence())[i]->getAsTyped();
+ if (argument->getType().getQualifier() == EvqFragData)
+ {
+ // The whole array is passed to the function. For validation purposes, assume all
+ // indices are accessed in the function.
+ ASSERT(argument->getType().isArray());
+ mMaxFragDataArrayIndexUsed = argument->getType().getOutermostArraySize() - 1;
+ }
+ }
+}
+
void TParseContext::checkInvariantVariableQualifier(bool invariant,
const TQualifier qualifier,
const TSourceLoc &invariantLocation)
@@ -10062,6 +10078,7 @@
checkImageMemoryAccessForUserDefinedFunctions(fnCandidate, callNode);
functionCallRValueLValueErrorCheck(fnCandidate, callNode);
functionCallClipCullDistanceCheck(fnCandidate, callNode);
+ functionCallFragDataCheck(fnCandidate, callNode);
mCallGraph[mCurrentFunction].insert(fnCandidate);
mIRBuilder.callFunction(mFunctionToId.at(fnCandidate));
diff --git a/src/compiler/translator/ParseContext.h b/src/compiler/translator/ParseContext.h
index 956bcf3..38d0604 100644
--- a/src/compiler/translator/ParseContext.h
+++ b/src/compiler/translator/ParseContext.h
@@ -220,6 +220,7 @@
TQualifier qualifier,
const char *message);
void functionCallClipCullDistanceCheck(const TFunction *fnCandidate, TIntermAggregate *fnCall);
+ void functionCallFragDataCheck(const TFunction *fnCandidate, TIntermAggregate *fnCall);
void checkInvariantVariableQualifier(bool invariant,
const TQualifier qualifier,
const TSourceLoc &invariantLocation);
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index c7197e5..3785d58 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -5037,6 +5037,33 @@
"GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when gl_SecondaryFragDataEXT is used");
}
+// Shader that writes to SecondaryFragData and passes FragData to a function.
+TEST_P(GLSLValidationTest, BlendFuncExtendedPassFragDataToFunction)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ GLint maxDrawBuffers = 0, maxDualSourceDrawBuffers = 0;
+ glGetIntegerv(GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT, &maxDualSourceDrawBuffers);
+ glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
+ ANGLE_SKIP_TEST_IF(maxDualSourceDrawBuffers == maxDrawBuffers);
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+void f(out vec4 fragData[gl_MaxDrawBuffers])
+{
+ fragData[0] = vec4(0.1);
+}
+void main() {
+ f(gl_FragData);
+ gl_SecondaryFragDataEXT[0] = vec4(1.0);
+})";
+ validateError(GL_FRAGMENT_SHADER, kFS,
+ "array index for gl_FragData must be less than "
+ "GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when gl_SecondaryFragDataEXT is used");
+}
+
// Shader that writes to FragData at an index >= than gl_MaxDualSourceDrawBuffersEXT is fine if
// SecondaryFragData is not used. Note that gl_MaxDualSourceDrawBuffersEXT is typically 1, while
// the size of gl_FragData (gl_MaxDrawBuffers) is larger.
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLValidationTest.cpp b/src/tests/gl_tests/GLSLValidationTest.cpp
index c7197e5..3785d58 100644
--- a/src/tests/gl_tests/GLSLValidationTest.cpp
+++ b/src/tests/gl_tests/GLSLValidationTest.cpp
@@ -5037,6 +5037,33 @@
"GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when gl_SecondaryFragDataEXT is used");
}
+// Shader that writes to SecondaryFragData and passes FragData to a function.
+TEST_P(GLSLValidationTest, BlendFuncExtendedPassFragDataToFunction)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ GLint maxDrawBuffers = 0, maxDualSourceDrawBuffers = 0;
+ glGetIntegerv(GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT, &maxDualSourceDrawBuffers);
+ glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
+ ANGLE_SKIP_TEST_IF(maxDualSourceDrawBuffers == maxDrawBuffers);
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+void f(out vec4 fragData[gl_MaxDrawBuffers])
+{
+ fragData[0] = vec4(0.1);
+}
+void main() {
+ f(gl_FragData);
+ gl_SecondaryFragDataEXT[0] = vec4(1.0);
+})";
+ validateError(GL_FRAGMENT_SHADER, kFS,
+ "array index for gl_FragData must be less than "
+ "GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT when gl_SecondaryFragDataEXT is used");
+}
+
// Shader that writes to FragData at an index >= than gl_MaxDualSourceDrawBuffersEXT is fine if
// SecondaryFragData is not used. Note that gl_MaxDualSourceDrawBuffersEXT is typically 1, while
// the size of gl_FragData (gl_MaxDrawBuffers) is larger.
Original Bug Report
OOB Write via Type-Mismatched OpStore in ANGLE SPIR-V
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: ANGLE’s EmulateFragColorData SPIR-V translator pass shrinks the gl_FragData array size but fails to update enclosing function parameter types. This causes OutputSPIRV to emit a type-mismatched OpStore that violates SPIR-V §3.42.8. In release builds, this invalid SPIR-V reaches the Vulkan driver unvalidated, potentially leading to a GPU memory out-of-bounds write.
Affected files:
third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFragColorData.cppthird_party/angle/src/compiler/translator/spirv/OutputSPIRV.cpp
Estimated timestamp from git blame: 2023-07-14
1. Summary of the Issue (Meant for Human Triage)
ANGLE contains a potential vulnerability in its SPIR-V shader translation pipeline on the Vulkan backend (the default for Android, Linux, and ChromeOS). When a WebGL1 shader uses dual-source blending (via the WEBGL_blend_func_extended extension), the EmulateFragColorData AST transformation pass correctly shrinks the built-in gl_FragData array from its maximum draw buffers size (typically 4-8) down to MaxDualSourceDrawBuffers (typically 1). However, this shrink operation is performed as a symbol-only replacement. If gl_FragData is passed as a whole array to a user-defined function, the enclosing function’s formal parameter type is not updated and remains sized at MaxDrawBuffers.
When OutputSPIRV generates SPIR-V for this function call, it allocates a temporary variable based on the (stale, larger) formal parameter type. After the function returns, it copies the temporary variable back to the (shrunk, smaller) gl_FragData output variable using an OpStore instruction. This results in a type-mismatched OpStore where a larger object is written into a smaller pointer, strictly violating SPIR-V specification §3.42.8.
Because SPIR-V validation (spirv::Validate()) is gated behind a debug-only ASSERT macro in ANGLE, release builds strip the validation entirely. The spec-invalid SPIR-V binary is subsequently fed directly into the Vulkan driver via vkCreateShaderModule. The behavior of Vulkan drivers processing a mismatched OpStore is undefined. Drivers that lower the composite OpStore element-wise based on the object type will write MaxDrawBuffers * 16 bytes into an output slot sized for 1 * 16 bytes, resulting in a potential out-of-bounds write in contiguous GPU memory. Since Output storage classes are not covered by Vulkan’s robustBufferAccess, this serves as a viable GPU-memory memory corruption primitive.
2. Proof-of-Concept & Detailed Execution Flow
Note: Our tooling does not execute code directly; the following represents a potential, theoretically verified execution trace derived from exhaustive static analysis.
Step-by-Step Execution Flow:
- Attacker Payload Injection: An attacker provides a WebGL1 fragment shader via
gl.shaderSource(). The shader enables#extension GL_EXT_draw_buffers : require(settinggl_MaxDrawBuffers> 1) and#extension GL_EXT_blend_func_extended : require(exposinggl_SecondaryFragDataEXTand restrictingMaxDualSourceDrawBuffersto 1). - Vulnerable Shader Structure: The shader declares:
void f(out vec4 d[gl_MaxDrawBuffers]) { d[0] = vec4(1.0); } void main() { f(gl_FragData); gl_SecondaryFragDataEXT[0] = vec4(0.0); } - Parsing Phase - L-Value Check Bypass:
TParseContext::checkCanBeLValue(ParseContext.cpp:1081) checks ifgl_FragDatais a valid l-value for theoutparameter. Theswitch(node->getQualifier())lacks anEvqFragDatacase, falling through todefaultand returningtrue(ParseContext.cpp:1118-1231). The whole-array usage is permitted. - Parsing Phase - Bounds Tracking Bypass: ANGLE tracks the highest index of
gl_FragDatainmMaxFragDataArrayIndexUsed(ParseContext.cpp:555). Because it only updates onEOpIndexDirect(ParseContext.cpp:7352), passinggl_FragDataas a whole array leavesmMaxFragDataArrayIndexUsedat0. The post-parse bounds check evaluates0 >= 1(MaxDualSourceDrawBuffers), which isfalse, bypassing validation (ParseContext.cpp:10500). - Translation Phase - Tree Transformation:
TranslatorSPIRV::translatedetectsgl_SecondaryFragDataEXTand triggers theEmulateFragColorDataAST transformation (TranslatorSPIRV.cpp:995). - Tree Operation - Incomplete Shrinking:
EmulateFragColorDataTraverser::visitSymbolshrinks the call-sitegl_FragDataarray size toMaxDualSourceDrawBuffers(size 1) and replaces it withwebgl_FragData(EmulateFragColorData.cpp:76-99). However, it drops the old argument node without updating the enclosingTFunctiondefinition. The function formal parameter remainsout vec4 d[MaxDrawBuffers]. - SPIR-V Emission - Temporary Variable Creation:
OutputSPIRVTraverser::createFunctionCall(OutputSPIRV.cpp:2260) processesf(webgl_FragData). It fetches the formal parameter typeparamType(vec4[MaxDrawBuffers]) and argument typeargType(vec4[1]). For theoutparameter, it allocates a function-local temporary variable matching the formal parameter type (%_arr_v4float_NwhereN=MaxDrawBuffers) (OutputSPIRV.cpp:2286-2289). - SPIR-V Emission - Copy-Out Initiation: After
OpFunctionCall, ANGLE copies the temporary variable back towebgl_FragData.nodeDataInitLValue(OutputSPIRV.cpp:778-793) bindspreSwizzleTypeIdto%_arr_v4float_N. - SPIR-V Emission - OpLoad Generation:
accessChainLoademits anOpLoadusing thepreSwizzleTypeId, yielding an object value%tempVarValueof sizeMaxDrawBuffers(OutputSPIRV.cpp:1100-1107). - SPIR-V Emission - Target Pointer Resolution:
accessChainStore(¶m, tempVarValue, ...)evaluates the AST reference, resolving it to the pointer ID%webgl_FragData(OutputSPIRV.cpp:1167->943-974). This pointer type is%_ptr_Output_arr_v4float_1(size 1 array in theOutputstorage class). - SPIR-V Emission - Spec Violation:
spirv::WriteStore(OutputSPIRV.cpp:1222) emitsOpStore %webgl_FragData %tempVarValue. This stores a value of sizeMaxDrawBuffersinto a pointer pointing to size1, violating SPIR-V Spec §3.42.8. - Release Execution - Validation Excision: In release builds,
ANGLE_ENABLE_ASSERTSis disabled. TheASSERT(spirv::Validate(*spirvBlobOut))macro (spv_utils.cpp:6201andlog_utils.h:263-271) evaluates to a no-op. The invalid SPIR-V is passed unvalidated tovkCreateShaderModule(vk_wrapper.h:1931), leading to potential driver-side GPU OOB memory corruption.
Suggested Fix:
Update TParseContext::checkCanBeLValue in ParseContext.cpp to properly reject whole-array l-value usage of gl_FragData (preventing EvqFragData from falling through to the permissive default case). Alternatively, ensure EmulateFragColorData updates/monomorphizes TFunction parameters when replacing the symbol argument.
3. Technical Verification Details (Automated Audit Logs)
Critic model verdict (2026-07-18 14:16:20):
- Severity: High (S1)
- Brief Notes / Reasoning: The vulnerability accurately describes a bug in ANGLE’s SPIR-V translator. In the
EmulateFragColorDatapass,gl_FragDatais shrunk tovec4[MaxDualSourceDrawBuffers](typically size 1) when dual-source blending is enabled. However, this is done via a symbol-only replacement, leaving enclosingTFunctionparameters (which remainvec4[MaxDrawBuffers]) unchanged. WhenOutputSPIRVTraverser::createFunctionCallperforms the copy-out for anoutparameter, it emits anOpStorewhere the source object has sizeMaxDrawBuffersand the target pointer has size 1, violating SPIR-V specification §3.42.8. Becauseangle::spirv::Validate()is gated behindASSERT()(angle_spirv_utils.cpp:68), the spec-invalid SPIR-V reachesvkCreateShaderModule(vk_wrapper.h:1931) unvalidated in release builds. The size mismatch on theOpStoreinto theOutputstorage class is bounds-bearing (acting as an implicit write size), allowing for driver-side GPU memory OOB writes. Per the severity guidelines, an ANGLE validating-layer gap emitting spec-invalid SPIR-V reaching the driver with a bounds-bearing parameter (or an OOB array operation in translator IR) is rated High (S1). It is not Critical (S0) because the corruption is strictly driver-side/GPU-memory, not in the GPU process host memory. The S2 Metal-only cap does not apply as this is the Vulkan backend.
Environmental Assumptions & Constraints:
- The target platform runs ANGLE with the Vulkan backend (default on Android, Linux, ChromeOS).
- The Vulkan device reports
dualSrcBlend == VK_TRUE. - WebGL contexts successfully expose both
WEBGL_draw_buffersandWEBGL_blend_func_extended. - The compilation environment uses release configuration where
ANGLE_ENABLE_ASSERTSis undefined, omittingspirv::Validate()checks. - The GPU driver lowers composite
OpStoreinstructions element-wise based on the right-hand object type rather than aborting entirely (behavior is technically undefined).
Codebase Investigator Verification Ledger:
- Query: “Check if OutputSPIRVTraverser emits OpLoad using the preSwizzleTypeId as the result type when accessChainLoad is called on a function-local temp variable.”
- Result: “Confirmed that spirv::WriteLoad is indeed called with accessChain.preSwizzleTypeId as the result type of the OpLoad instruction…
nodeDataInitLValuesetsdata->accessChain.preSwizzleTypeIdto the variable’stypeId…accessChainLoadcallsspirv::WriteLoadwithaccessChain.preSwizzleTypeId.”
- Result: “Confirmed that spirv::WriteLoad is indeed called with accessChain.preSwizzleTypeId as the result type of the OpLoad instruction…
- Query: “Verify if SPIR-V validation logic spv_utils.cpp calls spirv::Validate inside an ASSERT macro. Also verify that vk_wrapper.h calls vkCreateShaderModule without validating the blob in release builds.”
- Result: “In ‘spv_utils.cpp’ (line 6201), the SPIR-V validation logic calls ‘spirv::Validate’ inside the ‘ASSERT’ macro… Since the ‘ASSERT’ macro evaluates to a no-op in release builds (defined in ’log_utils.h’ when ‘ANGLE_ENABLE_ASSERTS’ is not set), any validation inside an ‘ASSERT’ is completely bypassed/compiled out in release builds…”
- Query: “Check if ValidateAST.cpp compares parameter types against argument types in visitFunctionCall.”
- Result: “No,
ValidateAST::visitFunctionCallinValidateAST.cpp(lines 454-478) does not compare parameter types against argument types… Furthermore, inShader.cpp,options.validateAST = true;is gated behind#if defined(ANGLE_ENABLE_ASSERTS).”
- Result: “No,
Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4
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.