CVE-2026-78978
Overview
Background
- StructuredBuffer vs cbuffer (HLSL)
- the ANGLE D3D11 backend can emit a large uniform-block array either as an HLSL
StructuredBuffer(tightly packed, element-strided access) or fall back to a constant buffer (cbuffer, std140-padded); the two use different element layouts. - `RecordUniformBlocksWithLargeArrayMember`
- the HLSL translator tree-op that decides which uniform blocks with large array members may use the packed
StructuredBufferpath, based on whether each field type is supported. - Supported field type
- for a struct element to be
StructuredBuffer-eligible, every field must be a non-array, non-struct scalar/vector, or a matrix whose relevant dimension is 4; anything else must fall back tocbuffer.
Root Cause Analysis
The field-validation helper iterated a struct’s fields to decide eligibility, but its loop logic was inverted: on encountering a field that was a supported type it immediately return true (declaring the whole struct eligible), and only returned false after the loop. This meant that as soon as the first field was a supported scalar/vector/matrix, the function reported the struct StructuredBuffer-eligible without inspecting the remaining fields, so a struct containing an unsupported field (an array, a nested struct, or a non-4-dimension matrix) after a supported one would still be routed through the packed StructuredBuffer layout. Because StructuredBuffer and cbuffer lay out and stride elements differently, indexing such a wrongly-classified buffer computes offsets against the wrong element size, producing an out-of-bounds read.
The fix inverts the loop so that any unsupported field triggers return false (forcing the safe cbuffer fallback), and only a struct all of whose fields are supported reaches the final return true.
false on the first unsupported field and true only after every field passes.Attack Path
- Author a crafted uniform block Attacker-controlled GLSL (e.g. WebGL on the D3D11 backend) declares a uniform block with a large array of a struct whose first field is a supported scalar/vector but a later field is an array, nested struct, or non-4 matrix.
- Misclassification as StructuredBuffer
RecordUniformBlocksWithLargeArrayMember’s field check returnstrueafter seeing only the first supported field, so the block is emitted as a packed HLSLStructuredBufferinstead of falling back tocbuffer. - Layout/stride mismatch
The generated HLSL indexes the buffer using the
StructuredBufferelement stride, which does not match the actual (cbuffer-style) layout expected for the unsupported member. - Out-of-bounds read
Indexing the array reads memory outside the intended element bounds, disclosing adjacent GPU buffer contents (further use of the leaked data is
(inferred)).
Impact Assessment
StructuredBuffer path. The advisory classifies this as an out-of-bounds read in ANGLE; exploited-in-the-wild status is not stated, and escalation beyond information disclosure is (inferred). Preconditions are use of the D3D11/HLSL backend and a uniform block with a large struct array mixing supported and unsupported field types.Changed Functions
| Function | Change | Notes |
|---|---|---|
HLSLOutputTestsrc/tests/compiler_tests/HLSLOutput_test.cpp |
modified | |
HLSL41VertexOutputTestsrc/tests/compiler_tests/HLSLOutput_test.cpp |
modified | |
forsrc/tests/compiler_tests/HLSLOutput_test.cpp |
modified |
Files Changed
src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cppsrc/tests/compiler_tests/HLSLOutput_test.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/GLSLTest.cppsrc/tests/angle_unittests.gni
Audit Directions
- All-vs-any loop predicatesAudit validation loops that should require every element to satisfy a condition; a
return trueinside the loop (instead ofreturn falseon failure withreturn trueafter) silently accepts on the first passing element. - Layout-selection correctnessReview other translator decisions that choose between differently-strided/padded buffer layouts (StructuredBuffer vs cbuffer, std140 vs std430); a misclassification directly yields wrong offsets and OOB access.
- Mixed-field structsTest uniform/struct blocks whose supported and unsupported field types are interleaved, since bugs often hide when the first field passes but a later one should force a fallback.
Patch
From eb1f49baeb1c153f3f1fed3adeef952680b2d9db Mon Sep 17 00:00:00 2001
From: wangra <wangra@google.com>
Date: Sun, 19 Jul 2026 22:14:08 -0400
Subject: [PATCH] D3D11: Fix StructuredBuffer field type validation
Invert the StructuredBuffer field validation loop to correctly
fallback to cbuffer. Delete the deprecated HLSLOutput_test compiler
tests and migrate them as functional E2E tests in GLSLTest.
Test: angle_end2end_tests
--gtest_filter=
"*UniformBlockWithUnsupportedFieldStructuredBufferFallback*
:*UniformBlockWithSupportedFieldsStructuredBuffer*"
Bug: b/520464738
Change-Id: I9dfeee7d7c7bed173e33becabd3008b145f7d1ab
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8122296
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Ran Wang <wangra@google.com>
---
diff --git a/src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cpp b/src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cpp
index 4bce9c5..fe6daa1 100644
--- a/src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cpp
+++ b/src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cpp
@@ -80,16 +80,16 @@
{
const TType &fieldType = *fields[i]->type();
// Do not allow the structure's member is array or structure.
- if (!fieldType.isArray() && !fieldType.getStruct() &&
- (fieldType.isScalar() || fieldType.isVector() ||
- (fieldType.isMatrix() &&
- ((matrixPacking != EmpRowMajor && fieldType.getRows() == 4) ||
- (matrixPacking == EmpRowMajor && fieldType.getCols() == 4)))))
+ if (fieldType.isArray() || fieldType.getStruct() ||
+ !(fieldType.isScalar() || fieldType.isVector() ||
+ (fieldType.isMatrix() &&
+ ((matrixPacking != EmpRowMajor && fieldType.getRows() == 4) ||
+ (matrixPacking == EmpRowMajor && fieldType.getCols() == 4)))))
{
- return true;
+ return false;
}
}
- return false;
+ return true;
}
else if (type.isMatrix())
{
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index f979c7f..0242efa 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -551,6 +551,8 @@
// Android
42264624 ANDROID GLES : GLSLTest_ES3.InitGlobalComplexConstant/* = SKIP
+520464738 ANDROID GLES : GLSLTest_ES3.UniformBlockWithSupportedFieldsStructuredBuffer/* = SKIP
+520464738 ANDROID GLES : GLSLTest_ES3.UniformBlockWithUnsupportedFieldStructuredBufferFallback/* = SKIP
42264646 ANDROID GLES : GLSLTestLoops.ForNoCondition/* = SKIP
42264756 ANDROID GLES : BufferDataTestES3.DrawWithNotCallingBufferData/* = SKIP
42265529 ANDROID : EGLReadinessCheckTest.IsRunningOnANGLE/* = SKIP
@@ -2279,6 +2281,7 @@
376553328 LINUX INTEL WGPU : BasicUniformUsageTest.* = SKIP
// Can't use a uniform struct outside of uniform address space.
376553328 WGPU : SimpleUniformTestES3.UseUniformStructOutsideOfUniformAddressSpace/* = SKIP
+520464738 WGPU : GLSLTest.ParameterWithNoName/* = SKIP
42267012 WGPU : ClearTest.ChangeFramebufferAttachmentFromRGBAtoRGB* = SKIP
42267012 WGPU : ClearTestES3.TextureArrayRGB8/* = SKIP
42267012 WGPU : ClearTestES3.ClearAndReadPixels3DTexture/* = SKIP
diff --git a/src/tests/compiler_tests/HLSLOutput_test.cpp b/src/tests/compiler_tests/HLSLOutput_test.cpp
deleted file mode 100644
index 67b3b71..0000000
--- a/src/tests/compiler_tests/HLSLOutput_test.cpp
+++ /dev/null
@@ -1,283 +0,0 @@
-//
-// Copyright 2017 The ANGLE Project Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-//
-// HLSLOutput_test.cpp:
-// Tests for HLSL output.
-//
-
-#include <regex>
-#include "GLSLANG/ShaderLang.h"
-#include "angle_gl.h"
-#include "gtest/gtest.h"
-#include "tests/test_utils/compiler_test.h"
-
-using namespace sh;
-
-class HLSLOutputTest : public MatchOutputCodeTest
-{
- public:
- HLSLOutputTest() : MatchOutputCodeTest(GL_FRAGMENT_SHADER, SH_HLSL_4_1_OUTPUT) {}
-};
-
-class HLSL41VertexOutputTest : public MatchOutputCodeTest
-{
- public:
- HLSL41VertexOutputTest() : MatchOutputCodeTest(GL_VERTEX_SHADER, SH_HLSL_4_1_OUTPUT) {}
-};
-
-// Test that having dynamic indexing of a vector inside the right hand side of logical or doesn't
-// trigger asserts in HLSL output.
-TEST_F(HLSLOutputTest, DynamicIndexingOfVectorOnRightSideOfLogicalOr)
-{
- const std::string &shaderString =
- "#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";
- compile(shaderString);
-}
-
-// Test that rewriting else blocks in a function that returns a struct doesn't use the struct name
-// without a prefix.
-TEST_F(HLSL41VertexOutputTest, RewriteElseBlockReturningStruct)
-{
- const std::string &shaderString =
- "struct foo\n"
- "{\n"
- " float member;\n"
- "};\n"
- "uniform bool b;\n"
- "foo getFoo()\n"
- "{\n"
- " if (b)\n"
- " {\n"
- " return foo(0.0);\n"
- " }\n"
- " else\n"
- " {\n"
- " return foo(1.0);\n"
- " }\n"
- "}\n"
- "void main()\n"
- "{\n"
- " gl_Position = vec4(getFoo().member);\n"
- "}\n";
- compile(shaderString);
- EXPECT_TRUE(foundInCode("_foo"));
- EXPECT_FALSE(foundInCode("(foo)"));
- EXPECT_FALSE(foundInCode(" foo"));
-}
-
-// Regression test for RemoveDynamicIndexing transformation producing invalid AST, based on fuzzer
-// test.
-TEST_F(HLSL41VertexOutputTest, RemoveDynamicingIndexIndexPrecisionBug)
-{
- const char shaderString[] = R"(void main()
-{
- mat3 tmp;
- vec3 res = vec3(0);
- for (int i = 0; res += 0., ivec3(0)[i], ivec3(tmp)[i], i < 0;);
-})";
- compile(shaderString);
-}
-
-// Test that having an array constructor as a statement doesn't trigger an assert in HLSL output.
-// This test has a constant array constructor statement.
-TEST_F(HLSLOutputTest, ConstArrayConstructorStatement)
-{
- const std::string &shaderString =
- R"(#version 300 es
- void main()
- {
- int[1](0);
- })";
- compile(shaderString);
-}
-
-// Test that having an array constructor as a statement doesn't trigger an assert in HLSL output.
-TEST_F(HLSLOutputTest, ArrayConstructorStatement)
-{
- const std::string &shaderString =
- 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]++);
- })";
- compile(shaderString);
-}
-
-// Test an array of arrays constructor as a statement.
-TEST_F(HLSLOutputTest, ArrayOfArraysStatement)
-{
- const std::string &shaderString =
- 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));
- })";
- compile(shaderString);
-}
-
-// Test dynamic indexing of a vector. This makes sure that helper functions added for dynamic
-// indexing have correct data that subsequent traversal steps rely on.
-TEST_F(HLSLOutputTest, VectorDynamicIndexing)
-{
- const std::string &shaderString =
- R"(#version 300 es
- precision mediump float;
- out vec4 outColor;
- uniform int i;
- void main()
- {
- vec4 foo = vec4(0.0, 0.0, 0.0, 1.0);
- foo[i] = foo[i + 1];
- outColor = foo;
- })";
- compile(shaderString);
-}
-
-// Test returning an array from a user-defined function. This makes sure that function symbols are
-// changed consistently when the user-defined function is changed to have an array out parameter.
-TEST_F(HLSLOutputTest, ArrayReturnValue)
-{
- const std::string &shaderString =
- R"(#version 300 es
- precision mediump float;
- uniform float u;
- out vec4 outColor;
-
- float[2] getArray(float f)
- {
- return float[2](f, f + 1.0);
- }
-
- void main()
- {
- float[2] arr = getArray(u);
- outColor = vec4(arr[0], arr[1], 0.0, 1.0);
- })";
- compile(shaderString);
-}
-
-// Test that writing parameters without a name doesn't assert.
-TEST_F(HLSLOutputTest, ParameterWithNoName)
-{
- const std::string &shaderString =
- R"(precision mediump float;
-
- uniform vec4 v;
-
- vec4 s(vec4)
- {
- return v;
- }
- void main()
- {
- gl_FragColor = s(v);
- })";
- compile(shaderString);
-}
-
-// Test that array dimensions are written out correctly.
-TEST_F(HLSLOutputTest, Array)
-{
- const std::string &shaderString =
- 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 * 2.0;
- my_FragColor.x += arr[i];
- }
- })";
- compile(shaderString);
- EXPECT_TRUE(foundInCodeRegex(std::regex("_arr(\\d)*\\[2\\]")));
-}
-
-// Test that initializing array with previously declared array will not be overwritten
-TEST_F(HLSLOutputTest, SameNameArray)
-{
- const std::string &shaderString =
- R"(#version 300 es
- precision highp float;
- out vec4 my_FragColor;
-
- void main()
- {
- float arr[2] = float[2](1.0, 1.0);
- {
- float arr[2] = arr;
- my_FragColor = vec4(0.0, arr[0], 0.0, arr[1]);
- }
- })";
- compile(shaderString);
- // There should be two different arr defined, e.g. _arr1000 and _arr1001
- // Use Workaround for now.
- // Once the build team fixes libc++ we could use the following one line solution instead.
- // EXPECT_TRUE(foundInCodeRegex(std::regex("_arr(\\d*)\\[2\\](.|\\r|\\n)*_arr(?!\\1)\\d*\\[2\\]")));
- std::smatch m;
- EXPECT_TRUE(foundInCodeRegex(std::regex("_arr(\\d)*\\[2\\]"), &m));
- EXPECT_TRUE(m.size() == 2);
- EXPECT_TRUE(m[0].str() != m[1].str());
-}
-
-// Test that passing a non-struct member of a std140 structure to a function won't trigger the
-// struct mapping.
-TEST_F(HLSLOutputTest, NonStructMemberAsFunctionArgument)
-{
- constexpr char shaderString[] = R"(#version 300 es
-precision highp float;
-out vec4 my_FragColor;
-
-struct InstancingData
-{
- vec4 data;
-};
-
-layout(std140) uniform InstanceBlock
-{
- InstancingData instances[8];
-};
-
-void main()
-{
- int index = int(gl_FragCoord.x);
- float result = dot(instances[index].data, vec4(1.0, 1.0, 1.0, 1.0));
- my_FragColor = vec4(result, 0.0, 0.0, 1.0);
-})";
-
- compile(shaderString);
- EXPECT_FALSE(foundInCode("map_instances"));
-}
-
-// Test that default hlsl warnings are suppressed.
-TEST_F(HLSLOutputTest, DefaultHLSLWarningsSuppressed)
-{
- const std::string &shaderString =
- "precision mediump float;\n"
- "void main() {\n"
- " gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);\n"
- "}\n";
- compile(shaderString);
- EXPECT_TRUE(foundInCode("#pragma warning( disable: 3081 3556 3557 3571 )"));
-}
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 9c89d28..77f340f 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -25037,6 +25037,445 @@
drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::white);
}
+
+// Test that having dynamic indexing of a vector inside the right hand side of logical or works
+// 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());
+ glUseProgram(program);
+ GLint u1Loc = glGetUniformLocation(program, "u1");
+ ASSERT_NE(-1, u1Loc);
+ glUniform1i(u1Loc, 2); // v[3] || v[2] -> false || true -> true
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::white);
+}
+
+// Test that rewriting else blocks in a function that returns a struct doesn't use the struct name
+// 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"
... (file diff truncated)
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index f979c7f..0242efa 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -551,6 +551,8 @@
// Android
42264624 ANDROID GLES : GLSLTest_ES3.InitGlobalComplexConstant/* = SKIP
+520464738 ANDROID GLES : GLSLTest_ES3.UniformBlockWithSupportedFieldsStructuredBuffer/* = SKIP
+520464738 ANDROID GLES : GLSLTest_ES3.UniformBlockWithUnsupportedFieldStructuredBufferFallback/* = SKIP
42264646 ANDROID GLES : GLSLTestLoops.ForNoCondition/* = SKIP
42264756 ANDROID GLES : BufferDataTestES3.DrawWithNotCallingBufferData/* = SKIP
42265529 ANDROID : EGLReadinessCheckTest.IsRunningOnANGLE/* = SKIP
@@ -2279,6 +2281,7 @@
376553328 LINUX INTEL WGPU : BasicUniformUsageTest.* = SKIP
// Can't use a uniform struct outside of uniform address space.
376553328 WGPU : SimpleUniformTestES3.UseUniformStructOutsideOfUniformAddressSpace/* = SKIP
+520464738 WGPU : GLSLTest.ParameterWithNoName/* = SKIP
42267012 WGPU : ClearTest.ChangeFramebufferAttachmentFromRGBAtoRGB* = SKIP
42267012 WGPU : ClearTestES3.TextureArrayRGB8/* = SKIP
42267012 WGPU : ClearTestES3.ClearAndReadPixels3DTexture/* = SKIP
diff --git a/src/tests/angle_unittests.gni b/src/tests/angle_unittests.gni
index b17edfa..4465a93 100644
--- a/src/tests/angle_unittests.gni
+++ b/src/tests/angle_unittests.gni
@@ -126,11 +126,7 @@
angle_unittests_glsl_mac_sources =
[ "compiler_tests/UnfoldShortCircuitAST_test.cpp" ]
-# TODO(jmadill): should probably call this windows sources
-angle_unittests_hlsl_sources = [
- "compiler_tests/HLSLOutput_test.cpp",
- "compiler_tests/UnrollFlatten_test.cpp",
-]
+angle_unittests_hlsl_sources = [ "compiler_tests/UnrollFlatten_test.cpp" ]
angle_unittests_gl_sources = [
"../libANGLE/renderer/gl/DisplayGL_unittest.cpp",
diff --git a/src/tests/compiler_tests/HLSLOutput_test.cpp b/src/tests/compiler_tests/HLSLOutput_test.cpp
deleted file mode 100644
index 67b3b71..0000000
--- a/src/tests/compiler_tests/HLSLOutput_test.cpp
+++ /dev/null
@@ -1,283 +0,0 @@
-//
-// Copyright 2017 The ANGLE Project Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-//
-// HLSLOutput_test.cpp:
-// Tests for HLSL output.
-//
-
-#include <regex>
-#include "GLSLANG/ShaderLang.h"
-#include "angle_gl.h"
-#include "gtest/gtest.h"
-#include "tests/test_utils/compiler_test.h"
-
-using namespace sh;
-
-class HLSLOutputTest : public MatchOutputCodeTest
-{
- public:
- HLSLOutputTest() : MatchOutputCodeTest(GL_FRAGMENT_SHADER, SH_HLSL_4_1_OUTPUT) {}
-};
-
-class HLSL41VertexOutputTest : public MatchOutputCodeTest
-{
- public:
- HLSL41VertexOutputTest() : MatchOutputCodeTest(GL_VERTEX_SHADER, SH_HLSL_4_1_OUTPUT) {}
-};
-
-// Test that having dynamic indexing of a vector inside the right hand side of logical or doesn't
-// trigger asserts in HLSL output.
-TEST_F(HLSLOutputTest, DynamicIndexingOfVectorOnRightSideOfLogicalOr)
-{
- const std::string &shaderString =
- "#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";
- compile(shaderString);
-}
-
-// Test that rewriting else blocks in a function that returns a struct doesn't use the struct name
-// without a prefix.
-TEST_F(HLSL41VertexOutputTest, RewriteElseBlockReturningStruct)
-{
- const std::string &shaderString =
- "struct foo\n"
- "{\n"
- " float member;\n"
- "};\n"
- "uniform bool b;\n"
- "foo getFoo()\n"
- "{\n"
- " if (b)\n"
- " {\n"
- " return foo(0.0);\n"
- " }\n"
- " else\n"
- " {\n"
- " return foo(1.0);\n"
- " }\n"
- "}\n"
- "void main()\n"
- "{\n"
- " gl_Position = vec4(getFoo().member);\n"
- "}\n";
- compile(shaderString);
- EXPECT_TRUE(foundInCode("_foo"));
- EXPECT_FALSE(foundInCode("(foo)"));
- EXPECT_FALSE(foundInCode(" foo"));
-}
-
-// Regression test for RemoveDynamicIndexing transformation producing invalid AST, based on fuzzer
-// test.
-TEST_F(HLSL41VertexOutputTest, RemoveDynamicingIndexIndexPrecisionBug)
-{
- const char shaderString[] = R"(void main()
-{
- mat3 tmp;
- vec3 res = vec3(0);
- for (int i = 0; res += 0., ivec3(0)[i], ivec3(tmp)[i], i < 0;);
-})";
- compile(shaderString);
-}
-
-// Test that having an array constructor as a statement doesn't trigger an assert in HLSL output.
-// This test has a constant array constructor statement.
-TEST_F(HLSLOutputTest, ConstArrayConstructorStatement)
-{
- const std::string &shaderString =
- R"(#version 300 es
- void main()
- {
- int[1](0);
- })";
- compile(shaderString);
-}
-
-// Test that having an array constructor as a statement doesn't trigger an assert in HLSL output.
-TEST_F(HLSLOutputTest, ArrayConstructorStatement)
-{
- const std::string &shaderString =
- 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]++);
- })";
- compile(shaderString);
-}
-
-// Test an array of arrays constructor as a statement.
-TEST_F(HLSLOutputTest, ArrayOfArraysStatement)
-{
- const std::string &shaderString =
- 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));
- })";
- compile(shaderString);
-}
-
-// Test dynamic indexing of a vector. This makes sure that helper functions added for dynamic
-// indexing have correct data that subsequent traversal steps rely on.
-TEST_F(HLSLOutputTest, VectorDynamicIndexing)
-{
- const std::string &shaderString =
- R"(#version 300 es
- precision mediump float;
- out vec4 outColor;
- uniform int i;
- void main()
- {
- vec4 foo = vec4(0.0, 0.0, 0.0, 1.0);
- foo[i] = foo[i + 1];
- outColor = foo;
- })";
- compile(shaderString);
-}
-
-// Test returning an array from a user-defined function. This makes sure that function symbols are
-// changed consistently when the user-defined function is changed to have an array out parameter.
-TEST_F(HLSLOutputTest, ArrayReturnValue)
-{
- const std::string &shaderString =
- R"(#version 300 es
- precision mediump float;
- uniform float u;
- out vec4 outColor;
-
- float[2] getArray(float f)
- {
- return float[2](f, f + 1.0);
- }
-
- void main()
- {
- float[2] arr = getArray(u);
- outColor = vec4(arr[0], arr[1], 0.0, 1.0);
- })";
- compile(shaderString);
-}
-
-// Test that writing parameters without a name doesn't assert.
-TEST_F(HLSLOutputTest, ParameterWithNoName)
-{
- const std::string &shaderString =
- R"(precision mediump float;
-
- uniform vec4 v;
-
- vec4 s(vec4)
- {
- return v;
- }
- void main()
- {
- gl_FragColor = s(v);
- })";
- compile(shaderString);
-}
-
-// Test that array dimensions are written out correctly.
-TEST_F(HLSLOutputTest, Array)
-{
- const std::string &shaderString =
- 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 * 2.0;
- my_FragColor.x += arr[i];
- }
- })";
- compile(shaderString);
- EXPECT_TRUE(foundInCodeRegex(std::regex("_arr(\\d)*\\[2\\]")));
-}
-
-// Test that initializing array with previously declared array will not be overwritten
-TEST_F(HLSLOutputTest, SameNameArray)
-{
- const std::string &shaderString =
- R"(#version 300 es
- precision highp float;
- out vec4 my_FragColor;
-
- void main()
- {
- float arr[2] = float[2](1.0, 1.0);
- {
- float arr[2] = arr;
- my_FragColor = vec4(0.0, arr[0], 0.0, arr[1]);
- }
- })";
- compile(shaderString);
- // There should be two different arr defined, e.g. _arr1000 and _arr1001
- // Use Workaround for now.
- // Once the build team fixes libc++ we could use the following one line solution instead.
- // EXPECT_TRUE(foundInCodeRegex(std::regex("_arr(\\d*)\\[2\\](.|\\r|\\n)*_arr(?!\\1)\\d*\\[2\\]")));
- std::smatch m;
- EXPECT_TRUE(foundInCodeRegex(std::regex("_arr(\\d)*\\[2\\]"), &m));
- EXPECT_TRUE(m.size() == 2);
- EXPECT_TRUE(m[0].str() != m[1].str());
-}
-
-// Test that passing a non-struct member of a std140 structure to a function won't trigger the
-// struct mapping.
-TEST_F(HLSLOutputTest, NonStructMemberAsFunctionArgument)
-{
- constexpr char shaderString[] = R"(#version 300 es
-precision highp float;
-out vec4 my_FragColor;
-
-struct InstancingData
-{
- vec4 data;
-};
-
... (truncated)
Original Bug Report
Potential GPU OOB read via logic inversion in ANGLE's StructuredBuffer validation
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: An inverted loop condition in ANGLE allows structures containing unsupported types to be translated to D3D11 StructuredBuffers. This leads to a mismatch between the calculated D3D11 buffer stride and the compiled HLSL structure size. The resulting layout discrepancy can potentially trigger an out-of-bounds memory read on the GPU.
Affected files:
third_party/angle/src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cppthird_party/angle/src/compiler/translator/hlsl/StructureHLSL.cpp
Estimated timestamp from git blame: 2020-11-20
Description
A potential vulnerability exists in ANGLE’s D3D11 translation path due to an existential-instead-of-universal check in IsSupportedTypeForStructuredBuffer. When translating std140 uniform blocks to StructuredBuffers, ANGLE is supposed to ensure that all fields inside the block structure are supported. However, the loop logic returns true on the very first field that satisfies the compatibility criteria, leaving subsequent unsupported fields (such as column-major mat3 matrices) unvalidated.
Root Cause
In third_party/angle/src/compiler/translator/tree_ops/hlsl/RecordUniformBlocksWithLargeArrayMember.cpp at lines 79-92:
const TFieldList &fields = structure->fields();
for (size_t i = 0; i < fields.size(); i++)
{
const TType &fieldType = *fields[i]->type();
if (!fieldType.isArray() && !fieldType.getStruct() &&
(fieldType.isScalar() || fieldType.isVector() ||
(fieldType.isMatrix() &&
((matrixPacking != EmpRowMajor && fieldType.getRows() == 4) ||
(matrixPacking == EmpRowMajor && fieldType.getCols() == 4)))))
{
return true;
}
}
return false;
If the first field is compatible (e.g., a vec4), the function immediately returns true, and any subsequent fields are never evaluated.
Downstream Stride Mismatch
Because the unsupported fields are not filtered out:
- The shader translator uses HLSL structure generation logic that forces padding (
std140ForcePaddingString), yielding an HLSL structure definition such as:struct std_rm_fp__S { float4 _a; float3x3 _b; float pad_0; }; - When compiled by
fxc, the structure size inside theStructuredBufferis calculated using HLSL default layout rules (e.g., 56 or 68 bytes). - On the host side, ANGLE’s CPU-side logic calculates the
std140stride as exactly 64 bytes and configures the D3D11StructureByteStridedescriptor using this value. - When a draw call executes, the discrepancy between the shader’s internal stride and the D3D11 descriptor’s stride can result in out-of-bounds indexing. At high indices, this divergence can cause the GPU to read past the allocated buffer bounds.
Potential Steps to Reproduce (Unverified)
These are potential/suggested steps to reproduce the issue, as our analysis has been conducted statically without runtime verification:
- On Windows 10+ using the D3D11 backend, create a WebGL2 context.
- Compile and link a fragment shader with a
std140uniform block containing a structure array of at least 50 elements where the structure is defined as{ vec4 a; mat3 b; }:#version 300 es precision highp float; struct S { vec4 a; mat3 b; }; layout(std140) uniform Block { S buf[50]; }; out vec4 fragColor; void main() { int i = int(gl_FragCoord.x) % 50; fragColor = buf[i].a + vec4(buf[i].b[0], 0.0); } - Set up the uniform buffer with a size matching the CPU-calculated size of $50 \times 64 = 3200$ bytes.
- Bind the buffer and initiate a draw call. Under a mismatched stride, dynamic lookups at higher array indices (e.g., index 49) may result in OOB memory reads that are observable via render-to-texture and
glReadPixelsoperations.
Suggested Fix
Correct the validation logic in IsSupportedTypeForStructuredBuffer so that the loop returns false if any field fails to meet the criteria. Only return true if all fields pass the checks successfully:
const TFieldList &fields = structure->fields();
for (size_t i = 0; i < fields.size(); i++)
{
const TType &fieldType = *fields[i]->type();
if (fieldType.isArray() || fieldType.getStruct() ||
!(fieldType.isScalar() || fieldType.isVector() ||
(fieldType.isMatrix() &&
((matrixPacking != EmpRowMajor && fieldType.getRows() == 4) ||
(matrixPacking == EmpRowMajor && fieldType.getCols() == 4)))))
{
return false;
}
}
return true;
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
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.