Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker536531630
Fix commit8521722b7ebc (angle/angle) +78/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
ClampIndirectIndicesTraverser
src/compiler/translator/tree_ops/ClampIndirectIndices.cpp
modified

Files Changed

  • src/compiler/translator/Compiler.cpp
  • src/compiler/translator/tree_ops/ClampIndirectIndices.cpp
  • src/compiler/translator/tree_ops/ClampIndirectIndices.h
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/GLSLTest.cpp
From 8521722b7ebc715adb1cec75c722fa51942c73d2 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Mon, 20 Jul 2026 16:23:13 -0400
Subject: [PATCH] Translator: Fix gl_FragData clamp without EXT_draw_buffers

For AST, this change clamps the indices to [0,0] if EXT_draw_buffers is
not enabled.

For IR, this was already correct because gl_FragData is sized early to
one element during parse.

Bug: chromium:536531630
Change-Id: I02d3ba21ab0f5fef1119967538f6c66fbeedd7d8
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8121717
Reviewed-by: Geoff Lang <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/compiler/translator/Compiler.cpp b/src/compiler/translator/Compiler.cpp
index fadb64c..450d2a8 100644
--- a/src/compiler/translator/Compiler.cpp
+++ b/src/compiler/translator/Compiler.cpp
@@ -1047,7 +1047,7 @@
 
     if (compileOptions.clampIndirectArrayBounds)
     {
-        if (!ClampIndirectIndices(this, root, &mSymbolTable))
+        if (!ClampIndirectIndices(this, root, &mSymbolTable, mExtensionBehavior))
         {
             return false;
         }
diff --git a/src/compiler/translator/tree_ops/ClampIndirectIndices.cpp b/src/compiler/translator/tree_ops/ClampIndirectIndices.cpp
index 2bcc698..3872a33 100644
--- a/src/compiler/translator/tree_ops/ClampIndirectIndices.cpp
+++ b/src/compiler/translator/tree_ops/ClampIndirectIndices.cpp
@@ -19,13 +19,28 @@
 {
 namespace
 {
+enum class ExtDrawBuffers
+{
+    Disabled,
+    Enabled,
+};
+
+bool ClampIndirectIndicesImpl(TCompiler *compiler,
+                              TIntermNode *root,
+                              TSymbolTable *symbolTable,
+                              ExtDrawBuffers extDrawBuffers);
+
 // Traverser that finds EOpIndexIndirect nodes and applies a clamp to their right-hand side
 // expression.
 class ClampIndirectIndicesTraverser : public TIntermTraverser
 {
   public:
-    ClampIndirectIndicesTraverser(TCompiler *compiler, TSymbolTable *symbolTable)
-        : TIntermTraverser(true, false, false, symbolTable), mCompiler(compiler)
+    ClampIndirectIndicesTraverser(TCompiler *compiler,
+                                  TSymbolTable *symbolTable,
+                                  ExtDrawBuffers extDrawBuffers)
+        : TIntermTraverser(true, false, false, symbolTable),
+          mCompiler(compiler),
+          mExtDrawBuffers(extDrawBuffers)
     {
         mIsSecondaryFragDataUsed = symbolTable->isSecondaryFragDataUsed();
     }
@@ -41,9 +56,11 @@
         }
 
         // Apply the transformation to the left and right nodes
-        bool valid = ClampIndirectIndices(mCompiler, node->getLeft(), mSymbolTable);
+        bool valid =
+            ClampIndirectIndicesImpl(mCompiler, node->getLeft(), mSymbolTable, mExtDrawBuffers);
         ASSERT(valid);
-        valid = ClampIndirectIndices(mCompiler, node->getRight(), mSymbolTable);
+        valid =
+            ClampIndirectIndicesImpl(mCompiler, node->getRight(), mSymbolTable, mExtDrawBuffers);
         ASSERT(valid);
 
         // Generate clamp(right, 0, N), where N is the size of the array being indexed minus 1.  If
@@ -72,7 +89,13 @@
         if (leftType.isArray())
         {
             int arraySize = static_cast<int>(leftType.getOutermostArraySize());
-            if (leftType.getQualifier() == EvqFragData && mIsSecondaryFragDataUsed)
+            if (leftType.getQualifier() == EvqFragData &&
+                mExtDrawBuffers == ExtDrawBuffers::Disabled)
+            {
+                // When EXT_draw_buffers is disabled, only element 0 of gl_FragData may be accessed.
+                arraySize = 1;
+            }
+            else if (leftType.getQualifier() == EvqFragData && mIsSecondaryFragDataUsed)
             {
                 // When gl_SecondaryFragDataEXT is used, only indices up to MaxDualSourceDrawBuffers
                 // of gl_FragData may be accessed.
@@ -133,15 +156,32 @@
     }
 
     TCompiler *mCompiler;
+    const ExtDrawBuffers mExtDrawBuffers;
     bool mIsSecondaryFragDataUsed = false;
 };
-}  // anonymous namespace
 
-bool ClampIndirectIndices(TCompiler *compiler, TIntermNode *root, TSymbolTable *symbolTable)
+bool ClampIndirectIndicesImpl(TCompiler *compiler,
+                              TIntermNode *root,
+                              TSymbolTable *symbolTable,
+                              ExtDrawBuffers extDrawBuffers)
 {
-    ClampIndirectIndicesTraverser traverser(compiler, symbolTable);
+    ClampIndirectIndicesTraverser traverser(compiler, symbolTable, extDrawBuffers);
     root->traverse(&traverser);
     return traverser.updateTree(compiler, root);
 }
 
+}  // anonymous namespace
+
+bool ClampIndirectIndices(TCompiler *compiler,
+                          TIntermNode *root,
+                          TSymbolTable *symbolTable,
+                          const TExtensionBehavior &extensionBehavior)
+{
+    const ExtDrawBuffers extDrawBuffers =
+        IsExtensionEnabled(extensionBehavior, TExtension::EXT_draw_buffers)
+            ? ExtDrawBuffers::Enabled
+            : ExtDrawBuffers::Disabled;
+    return ClampIndirectIndicesImpl(compiler, root, symbolTable, extDrawBuffers);
+}
+
 }  // namespace sh
diff --git a/src/compiler/translator/tree_ops/ClampIndirectIndices.h b/src/compiler/translator/tree_ops/ClampIndirectIndices.h
index eebbab0..64cb203 100644
--- a/src/compiler/translator/tree_ops/ClampIndirectIndices.h
+++ b/src/compiler/translator/tree_ops/ClampIndirectIndices.h
@@ -10,6 +10,7 @@
 #define COMPILER_TRANSLATOR_TREEOPS_CLAMPINDIRECTINDICES_H_
 
 #include "common/angleutils.h"
+#include "compiler/translator/ExtensionBehavior.h"
 
 namespace sh
 {
@@ -20,7 +21,8 @@
 
 [[nodiscard]] bool ClampIndirectIndices(TCompiler *compiler,
                                         TIntermNode *root,
-                                        TSymbolTable *symbolTable);
+                                        TSymbolTable *symbolTable,
+                                        const TExtensionBehavior &extensionBehavior);
 
 }  // namespace sh
 
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index e6d48b8..ceea19b 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -465,6 +465,7 @@
 524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP
 534815900 MAC METAL : TextureCubeTestES3.RedefinedCubemapLevelsOnlyFaceZeroCompatible/* = SKIP
 536936861 MAC METAL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
+537814733 MAC METAL : WebGLGLSLTest.FragDataIndexClampWithoutDrawBuffers/* = SKIP
 
 // The workaround is not intended to be enabled in this configuration so
 // skip it as the failure is likely a driver bug.
@@ -1264,6 +1265,7 @@
 535213600 PIXEL10 GLES : PbufferTestES3.BindTexImageAndCopyTexSubTextureInvertedDst/* = SKIP
 535213600 PIXEL10 GLES : PbufferTestES3.BindTexImageAndCopyTextureInvertedSrc/* = SKIP
 535213600 PIXEL10 GLES : PbufferTestES3.BindTexImageAndCopyTextureSrc/* = SKIP
+537235694 PIXEL10 GLES : WebGLGLSLTest.FragDataIndexClampWithoutDrawBuffers/* = SKIP
 
 480069042 PIXEL10 VULKAN : ClearTextureEXTTestES31Unrenderable.Clear/ES3_1_Vulkan*__GL_RG16_SNORM_EXT_GL_RG_GL_SHORT_GL_TEXTURE_2D_ARRAY = SKIP
 480069042 PIXEL10 VULKAN : ClearTextureEXTTestES31Unrenderable.Clear/ES3_1_Vulkan*__GL_RGB32UI_GL_RGB_INTEGER_GL_UNSIGNED_INT_GL_TEXTURE_2D_ARRAY = SKIP
@@ -2395,6 +2397,7 @@
 42267100 WGPU : GLSLTest.FragCoordConsistency/* = SKIP
 42267100 WGPU : GLSLTest.FragData/* = SKIP
 42267100 WGPU : GLSLTest.FragData_AlphaToCoverage/* = SKIP
+42267100 WGPU : WebGLGLSLTest.FragDataIndexClampWithoutDrawBuffers/* = SKIP
 42267100 WGPU : GLSLTest.SeparateStructDeclaratorStructInStruct/* = SKIP
 42267100 WGPU : GLSLTest.ConstructorinSequenceOperator/* = SKIP
 42267100 WGPU : GLSLTest.VectorAndMatrixScalarizationDoesNotAffectRendering/* = SKIP
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 6cad6e2..e91b6a4 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24588,6 +24588,29 @@
 }
 
 // Test that indirect indices to gl_FragData get clamped to the right bounds when
+// GL_EXT_draw_buffers is not enabled.
+//
+// The same test for ES3 is not needed, because unlike gl_FragData in ESSL 100, it's not allowed to
+// index a fragment output variable with a non-constant index in ESSL 300+.
+TEST_P(WebGLGLSLTest, FragDataIndexClampWithoutDrawBuffers)
+{
+    constexpr char kFS[] = R"(precision mediump float;
+void main() {
+    // GL_EXT_draw_buffers is not enabled, which means only one output is valid.  Make sure all the
+    // following writes in the loop end up writing to gl_FragData[0].
+    gl_FragData[0] = vec4(1, 0, 0, 1);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index e6d48b8..ceea19b 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -465,6 +465,7 @@
 524008572 MAC METAL : VertexAttributeTestES3.LargeAttribPointerOffsetNoCrash/* = SKIP
 534815900 MAC METAL : TextureCubeTestES3.RedefinedCubemapLevelsOnlyFaceZeroCompatible/* = SKIP
 536936861 MAC METAL : ClipDistance*.ThreeClipDistancesRedeclaredAndPassedToFunction/* = SKIP
+537814733 MAC METAL : WebGLGLSLTest.FragDataIndexClampWithoutDrawBuffers/* = SKIP
 
 // The workaround is not intended to be enabled in this configuration so
 // skip it as the failure is likely a driver bug.
@@ -1264,6 +1265,7 @@
 535213600 PIXEL10 GLES : PbufferTestES3.BindTexImageAndCopyTexSubTextureInvertedDst/* = SKIP
 535213600 PIXEL10 GLES : PbufferTestES3.BindTexImageAndCopyTextureInvertedSrc/* = SKIP
 535213600 PIXEL10 GLES : PbufferTestES3.BindTexImageAndCopyTextureSrc/* = SKIP
+537235694 PIXEL10 GLES : WebGLGLSLTest.FragDataIndexClampWithoutDrawBuffers/* = SKIP
 
 480069042 PIXEL10 VULKAN : ClearTextureEXTTestES31Unrenderable.Clear/ES3_1_Vulkan*__GL_RG16_SNORM_EXT_GL_RG_GL_SHORT_GL_TEXTURE_2D_ARRAY = SKIP
 480069042 PIXEL10 VULKAN : ClearTextureEXTTestES31Unrenderable.Clear/ES3_1_Vulkan*__GL_RGB32UI_GL_RGB_INTEGER_GL_UNSIGNED_INT_GL_TEXTURE_2D_ARRAY = SKIP
@@ -2395,6 +2397,7 @@
 42267100 WGPU : GLSLTest.FragCoordConsistency/* = SKIP
 42267100 WGPU : GLSLTest.FragData/* = SKIP
 42267100 WGPU : GLSLTest.FragData_AlphaToCoverage/* = SKIP
+42267100 WGPU : WebGLGLSLTest.FragDataIndexClampWithoutDrawBuffers/* = SKIP
 42267100 WGPU : GLSLTest.SeparateStructDeclaratorStructInStruct/* = SKIP
 42267100 WGPU : GLSLTest.ConstructorinSequenceOperator/* = SKIP
 42267100 WGPU : GLSLTest.VectorAndMatrixScalarizationDoesNotAffectRendering/* = SKIP
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 6cad6e2..e91b6a4 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24588,6 +24588,29 @@
 }
 
 // Test that indirect indices to gl_FragData get clamped to the right bounds when
+// GL_EXT_draw_buffers is not enabled.
+//
+// The same test for ES3 is not needed, because unlike gl_FragData in ESSL 100, it's not allowed to
+// index a fragment output variable with a non-constant index in ESSL 300+.
+TEST_P(WebGLGLSLTest, FragDataIndexClampWithoutDrawBuffers)
+{
+    constexpr char kFS[] = R"(precision mediump float;
+void main() {
+    // GL_EXT_draw_buffers is not enabled, which means only one output is valid.  Make sure all the
+    // following writes in the loop end up writing to gl_FragData[0].
+    gl_FragData[0] = vec4(1, 0, 0, 1);
+    for (int i = 0; i < 8; i++) {
+        gl_FragData[i] += vec4(-0.1, 0.05, 0.0, 0.0);
+    }
+})";
+
+    ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f, 1.0f, true);
+    EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(51, 102, 0, 255), 1);
+    ASSERT_GL_NO_ERROR();
+}
+
+// Test that indirect indices to gl_FragData get clamped to the right bounds when
 // gl_SecondaryFragDataEXT is used.
 //
 // The same test for ES3 is not needed, because unlike gl_FragData in ESSL 100, it's not allowed to
Loading diff…

Original Bug Report

reported by [email protected]

Potential OOB HLSL Array Write via gl_FragData Dynamic Index Mismatch in ANGLE D3D11

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 HLSL translator generates out-of-bounds array accesses in emitted HLSL when a WebGL 1 shader dynamically indexes gl_FragData without enabling GL_EXT_draw_buffers. The translator declares a size-1 array but clamps dynamic indices to [0, 7], allowing out-of-bounds index writes to be passed to D3DCompile. This exposes the sandboxed GPU process to potential memory corruption within the shader compiler.

Affected files:

  • third_party/angle/src/compiler/translator/hlsl/OutputHLSL.cpp

Estimated timestamp from git blame: 2013-06-20

1. Summary of the Issue (Meant for Human Triage)

An issue in ANGLE’s D3D11 HLSL translator allows untrusted shaders (e.g., from WebGL 1 content) to cause out-of-bounds array writes in generated HLSL code, which is then passed directly to the D3DCompile API in the sandboxed GPU process.

When an ESSL 1.00 shader dynamically indexes gl_FragData without enabling the GL_EXT_draw_buffers extension, the OutputHLSL generator declares the backing static array gl_Color with a size of 1. However, the AST representation of gl_FragData maintains an array size equal to the backend’s MaxDrawBuffers (which is 8 on D3D11). Consequently, the ClampIndirectIndices pass clamps the indirect access index to the range [0, 7] instead of [0, 0].

When this HLSL is emitted, it results in code like gl_Color[clamped_index] = ... where clamped_index can be up to 7 while gl_Color has a size of only 1. This malformed HLSL is sent to D3DCompile, exposing the underlying text-based shader compiler to memory corruption within the sandboxed GPU process on Windows (High Severity / S1).

2. Proof-of-Concept & Detailed Execution Flow

Potential Trigger Path (WebGL 1): An attacker can trigger this vulnerability by serving a web page that creates a WebGL 1 context and compiles a specific fragment shader:

const gl = canvas.getContext('webgl');
const fs = gl.createShader(gl.FRAGMENT_SHADER);
// The shader uses a dynamic loop index to access gl_FragData without enabling GL_EXT_draw_buffers
gl.shaderSource(fs,
  'precision mediump float;\n' +
  'uniform sampler2D s; varying vec2 v;\n' +
  'void main(){\n' +
  '  for(int i=0;i<8;i++)\n' +
  '    gl_FragData[i] = texture2D(s, v);\n' +
  '}');
gl.compileShader(fs);

(Note: These are potential steps to trigger the vulnerability based on static code analysis; our tooling has not executed this code.)

Detailed Step-by-Step Execution Trace:

  1. Attacker Setup: The attacker serves a web page that creates a WebGL 1 context (gl = canvas.getContext('webgl')) and supplies a fragment shader.
  2. Shader Characteristics: The attacker’s shader dynamically indexes the built-in output array gl_FragData using a loop counter variable (e.g., gl_FragData[i]).
  3. Extension Omission: Crucially, the attacker does not enable the multiple render targets extension in the shader source (i.e., they omit #extension GL_EXT_draw_buffers : enable).
  4. Context Initialization: When the context is initialized on the D3D11 backend (Windows), libANGLE/renderer/d3d/d3d11/renderer11_utils.cpp:1148 sets caps->maxDrawBuffers to GetMaximumSimultaneousRenderTargets(), which is 8 on feature level 10+.
  5. Context Extensions: Additionally, :1305 sets extensions->drawBuffersEXT to true because the underlying hardware supports it, even though the shader hasn’t explicitly enabled it.
  6. Compiler Initialization: In libANGLE/Compiler.cpp:67, mResources.MaxDrawBuffers is initialized to caps.maxDrawBuffers (8). The fallback to 1 at :247 (which checks !extensions.drawBuffersEXT) is skipped because the hardware extension is enabled. Thus, resources.MaxDrawBuffers remains 8.
  7. Type Assignment: When the ANGLE parser runs (third_party/angle/src/compiler/translator/SymbolTable_autogen.cpp:23784), it initializes the AST type for gl_FragData. Because it is WebGL 1 (SH_WEBGL_SPEC), it sets the array size to resources.MaxDrawBuffers, which is 8.
  8. Constant Index Check: The attacker’s dynamic index i passes the WebGL 1 ESSL 100 constant-index validation (ParseContext::checkESSL100ConstantIndex at ParseContext.cpp:4123) because it is a loop counter (permitted per Appendix A). The AST node EOpIndexIndirect is successfully created.
  9. Built-in Desync Flaw: During the first use of gl_FragData, TParseContext::declareBuiltInOnFirstUse (ParseContext.cpp:3469) is called. It checks if the EXT_draw_buffers extension is enabled in the shader. Since it isn’t, it creates a new type with size 1 (:3494) to declare the variable in the IR builder. However, it fails to mutate the TType on the original TVariable. The AST TIntermSymbol for gl_FragData retains the original array size of 8.
  10. AST Clamping Pass: After parsing, ANGLE runs the ClampIndirectIndices pass over the AST to enforce bounds-checking on dynamic array accesses (Compiler.cpp:1041).
  11. Clamp Bound Evaluation: ClampIndirectIndicesTraverser examines the gl_FragData node. It calls leftType.getOutermostArraySize() (tree_ops/ClampIndirectIndices.cpp:74), which reads the size from the unmodified TIntermSymbol type. This evaluates to 8.
  12. Clamp Insertion: The pass clamps the dynamic index to arraySize - 1 (:82). The AST for i is replaced with int(clamp(float(i), 0.0, 7.0)).
  13. HLSL Header Generation: The clamped AST is handed to OutputHLSL to generate the D3D11 HLSL string. In the header generation (OutputHLSL.cpp:696), it checks IsExtensionEnabled(..., TExtension::EXT_draw_buffers) for the shader. Since it’s false, numColorValues evaluates to 1.
  14. OOB Declaration: OutputHLSL emits the static backing array for gl_FragData named gl_Color: static float4 gl_Color[1] = ... (OutputHLSL.cpp:698).
  15. HLSL Body Generation: OutputHLSL traverses the AST body. It emits gl_Color for EvqFragData (OutputHLSL.cpp:1195) and emits the clamped index for the array access.
  16. Malformed Array Access: The resulting HLSL statement emitted is gl_Color[int(clamp(float(_i), 0.0, 7.0))] = ..., demonstrating an out-of-bounds access up to index 7 on the size-1 array gl_Color.
  17. HLSL Compilation: This malformed HLSL string is dispatched to D3DCompile in the GPU process (libANGLE/renderer/d3d/HLSLCompiler.cpp).
  18. Unrolling Prevention: To prevent static analysis from folding the loop and emitting an X3504 compile-time error, Renderer11.cpp:3108 unconditionally passes the macro ANGLE_ENABLE_LOOP_FLATTEN=1 to D3DCompile, which expands LOOP to [loop].
  19. Vulnerability Realized: D3DCompile is forced to parse, optimize, and compile a dynamic out-of-bounds array write into a static size-1 array. Per the Knowledge Base, feeding such out-of-bounds arrays to D3DCompile from attacker GLSL is an established vector for triggering memory corruption within the text-based shader compiler itself (running inside the sandboxed GPU process).

Suggested Fix: In third_party/angle/src/compiler/translator/ParseContext.cpp, declareBuiltInOnFirstUse should correctly propagate the shrunken array size to the TVariable or TIntermSymbol nodes when EXT_draw_buffers is missing. Alternatively, ClampIndirectIndices should correctly account for the runtime-declared size of gl_FragData based on extension presence, rather than unconditionally trusting the MaxDrawBuffers-sized AST type.

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

Prior Critic Verdict Verbatim: > The report identifies a bug where ANGLE’s translator generates HLSL with an out-of-bounds array access. In WebGL 1, when a shader dynamically indexes gl_FragData[i] without the GL_EXT_draw_buffers extension, OutputHLSL declares gl_Color with size 1. However, the AST node retains its array size of 8 (MaxDrawBuffers), causing the ClampIndirectIndices pass to only clamp the index to [0, 7]. This results in the emitted HLSL accessing up to index 7 on a size-1 array, which is then handed to D3DCompile. > > The AI rewriter incorrectly downgraded the severity from High to Medium (S2) by arguing that the runtime execution of the resulting DXBC is bounded to shader-local registers per the D3D11.3 spec, and therefore doesn’t corrupt host memory. > > However, this overlooks the explicit rule in the Knowledge Base under High Severity (S1): ‘ANGLE shader translator emits SPIR-V/HLSL with an OOB OpAccessChain / array index from attacker GLSL (built-in redeclared with a smaller size, ClampIndirectIndices pass not run for a backend, constant-fold past declared bound). The emitted IR reaches vkCreateShaderModule/D3DCompile in the GPU process. This is web-content-reachable.’ > > Our finding is an exact match for this S1 rule (a built-in redeclared with a smaller size resulting in an OOB array index in the HLSL fed to D3DCompile). The vulnerability is that the text-based shader compiler (D3DCompile) is forced to parse and optimize malformed arrays, which is a known vector for triggering memory corruption within the compiler itself (running in the GPU process). > > Because D3D11 is Windows-only, the GPU process is sandboxed, so this avoids the Critical (S0) rating applied to unsandboxed Android GPU issues, and correctly caps at High (S1).

Mitigations / Validators Checked:

  • ANGLE_ENABLE_LOOP_FLATTEN: This is unconditionally passed as "1" to D3DCompile inside Renderer11.cpp:3108 which confirms that LOOP expands to [loop] and forces the compiler to parse dynamic indices without compile-time folding.
  • D3DCompile OOB Memory Limits: While runtime bounds might be constrained by DXBC specifications, parsing and optimization of such OOB indices in the HLSL compiler frontend or optimizer represents a high-risk attack surface within the GPU process.
  • WebGL 2 Check (ParseContext.cpp:7169): This check only prevents dynamic gl_FragData[i] in WebGL 2 contexts, leaving WebGL 1 contexts fully vulnerable to this out-of-bounds code generation.
  • ClampIndirectIndices Enforcement: Verified that isWebGL() || isHardenedContext() enforces the execution of the clamp pass (ShaderD3D.cpp:289-293), leading directly to the 7.0 max bound.

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.

View on issue tracker