Chrome · ANGLE
CVE-2026-17697
Type Confusion in ANGLE
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
src/compiler/translator/ir/src/transform/broadcast_fragcolor.rssrc/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/GLSLTest.cpp
Patch
From 165fb9fc835fd7e381c131c995ab7568db1d323a Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Fri, 29 May 2026 11:07:06 -0400
Subject: [PATCH] GL: Fix `invariant gl_FragColor;` + draw buffers
When GL_EXT_draw_buffers is enabled, gl_FragColor is replaced by
gl_FragData. If it was globally qualified as `invariant`, the relevant
transformation did not handle it.
Bug: chromium:517575864
Change-Id: I3ec2a6629940a69070e5327abd8ce37c55d0efcd
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7883926
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/src/compiler/translator/ir/src/transform/broadcast_fragcolor.rs b/src/compiler/translator/ir/src/transform/broadcast_fragcolor.rs
index 517946c..0c40534 100644
--- a/src/compiler/translator/ir/src/transform/broadcast_fragcolor.rs
+++ b/src/compiler/translator/ir/src/transform/broadcast_fragcolor.rs
@@ -49,7 +49,8 @@
let replacement = ir.meta.get_variable_mut(original_id);
replacement.name = Name::new_temp(name);
replacement.built_in = None;
- debug_assert!(replacement.decorations.decorations.is_empty());
+ // The built-in might have decorations such as Invariant
+ let decorations = std::mem::replace(&mut replacement.decorations, Decorations::new_none());
debug_assert!(replacement.scope == VariableScope::Global);
debug_assert!(!replacement.is_const);
@@ -66,7 +67,9 @@
let type_id = ir.meta.get_array_type_id(type_id, array_size);
let (arrayed_built_in_id, arrayed_built_in) =
ir.meta.declare_built_in_variable(type_id, precision, broadcast_to);
- ir.meta.get_variable_mut(arrayed_built_in_id).is_static_use = true;
+ let new_built_in = ir.meta.get_variable_mut(arrayed_built_in_id);
+ new_built_in.is_static_use = true;
+ new_built_in.decorations = decorations;
// Since the variable ID is unchanged, the IR does not need further modification. The
// transformation only needs to replicate the value of the global variable in each element
diff --git a/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp b/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
index 99532a4..3100952 100644
--- a/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
+++ b/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
@@ -51,6 +51,8 @@
bool isGLSecondaryFragColorUsed() const { return mGLSecondaryFragColorUsed; }
protected:
+ bool visitGlobalQualifierDeclaration(Visit visit,
+ TIntermGlobalQualifierDeclaration *node) override;
void visitSymbol(TIntermSymbol *node) override;
TIntermBinary *constructGLFragDataNode(int index, bool secondary) const;
@@ -84,6 +86,31 @@
return new TIntermBinary(EOpAssign, fragDataIndex, fragDataZero);
}
+bool GLFragColorBroadcastTraverser::visitGlobalQualifierDeclaration(
+ Visit visit,
+ TIntermGlobalQualifierDeclaration *node)
+{
+ TIntermSymbol *symbol = node->getSymbol();
+ if (symbol->variable().symbolType() == SymbolType::BuiltIn)
+ {
+ if (symbol->getName() == "gl_FragColor")
+ {
+ queueReplacementWithParent(
+ node, node->getSymbol(),
+ ReferenceBuiltInVariable(kGlFragDataString, *mSymbolTable, mShaderVersion),
+ OriginalNode::IS_DROPPED);
+ }
+ else if (symbol->getName() == "gl_SecondaryFragColorEXT")
+ {
+ queueReplacementWithParent(
+ node, node->getSymbol(),
+ ReferenceBuiltInVariable(kGlSecondaryFragDataString, *mSymbolTable, mShaderVersion),
+ OriginalNode::IS_DROPPED);
+ }
+ }
+ return false;
+}
+
void GLFragColorBroadcastTraverser::visitSymbol(TIntermSymbol *node)
{
if (node->variable().symbolType() == SymbolType::BuiltIn)
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 446507c..9f95376 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -497,6 +497,8 @@
494341324 MAC OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
496604559 MAC METAL : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP
+518849408 MAC OPENGL : GLSLTest.EmulateGLFragColorBroadcastInvariantFragColor/* = SKIP
+518849408 MAC OPENGL : GLSLTest.EmulateGLFragColorBroadcastInvariantFragColorUnused/* = SKIP
// The workaround is not intended to be enabled in this configuration so
// skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 519ac59..79ed94c 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24164,6 +24164,40 @@
// (Note: 565 with no alpha, glReadPixels will pad alpha to 255).
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::white);
}
+
+// Make sure gl_FragColor can be marked invariant in presence of GL_EXT_draw_buffers.
+TEST_P(GLSLTest, EmulateGLFragColorBroadcastInvariantFragColor)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+ invariant gl_FragColor;
+ void main() {
+ gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
+ }
+ )";
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+}
+
+// Make sure gl_FragColor can be marked invariant in presence of GL_EXT_draw_buffers, even if
+// gl_FragColor is unused.
+TEST_P(GLSLTest, EmulateGLFragColorBroadcastInvariantFragColorUnused)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+ invariant gl_FragColor;
+ void main() {
+ // gl_FragColor is unused
+ }
+ )";
+
+ // Verify compilation only, as output is not written to.
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+}
} // anonymous namespace
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND_ES31_AND_ES32(
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 446507c..9f95376 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -497,6 +497,8 @@
494341324 MAC OPENGL : MipmapTestES3.MismatchingLevelFormats/* = SKIP
496604559 MAC METAL : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
515709506 MAC METAL : DrawBaseVertexBaseInstanceTest_ES3.BaseInstanceSmallDivisorClientMemory/* = SKIP
+518849408 MAC OPENGL : GLSLTest.EmulateGLFragColorBroadcastInvariantFragColor/* = SKIP
+518849408 MAC OPENGL : GLSLTest.EmulateGLFragColorBroadcastInvariantFragColorUnused/* = SKIP
// The workaround is not intended to be enabled in this configuration so
// skip it as the failure is likely a driver bug.
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 519ac59..79ed94c 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24164,6 +24164,40 @@
// (Note: 565 with no alpha, glReadPixels will pad alpha to 255).
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::white);
}
+
+// Make sure gl_FragColor can be marked invariant in presence of GL_EXT_draw_buffers.
+TEST_P(GLSLTest, EmulateGLFragColorBroadcastInvariantFragColor)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+ invariant gl_FragColor;
+ void main() {
+ gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
+ }
+ )";
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+}
+
+// Make sure gl_FragColor can be marked invariant in presence of GL_EXT_draw_buffers, even if
+// gl_FragColor is unused.
+TEST_P(GLSLTest, EmulateGLFragColorBroadcastInvariantFragColorUnused)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_buffers"));
+
+ constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
+ invariant gl_FragColor;
+ void main() {
+ // gl_FragColor is unused
+ }
+ )";
+
+ // Verify compilation only, as output is not written to.
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
+}
} // anonymous namespace
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND_ES31_AND_ES32(
Loading diff…
Original Bug Report
reported by ge...@chromium.org
Type confusion in the EmulateGLFragColorBroadcast workaround when using 'invariant gl_FragColor'
This bug was first reported to Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=2029402
The following test shows a crash:
TEST_P(GLSLTest, EmulateGLFragColorBroadcastInvariantFragColor)
{
ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_buffers"));
constexpr char kFS[] = R"(#extension GL_EXT_draw_buffers : require
invariant gl_FragColor;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)";
ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
glUseProgram(program);
}
This requires running on the GL backend any of the GLSL output types except SH_GLSL_420_CORE_OUTPUT which removes the invariant keyword.
Running it locally in a debug build generates the following:
IntermNode.cpp:401 (replaceChildNode): Replacing a node with a node of invalid type: calling replacement.getAsSymbolNode() should not return nullptr.
The following ASAN report is generated:
=================================================================
==7477==ERROR: AddressSanitizer: SEGV on unknown address 0x0000bfff8003 (pc 0x75940eb3cd26 bp 0x7ffd1d79b590 sp 0x7ffd1d79b520 T0)
==7477==The signal is caused by a READ memory access.
#0 0x75940eb3cd26 in sh::TSymbol::symbolType() const /firefox/gfx/angle/checkout/src/compiler/translator/Symbol.h:56:44
#1 0x75940eb3cd26 in sh::HashName(sh::TSymbol const*, unsigned long (*)(char const*, unsigned long), std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>>, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>>>>*) /firefox/gfx/angle/checkout/src/compiler/translator/HashNames.cpp:87:17
#2 0x75940eb7f1e1 in sh::TOutputGLSLBase::hashName(sh::TSymbol const*) /firefox/gfx/angle/checkout/src/compiler/translator/OutputGLSLBase.cpp:1130:12
#3 0x75940eb7f1e1 in sh::TOutputGLSLBase::visitGlobalQualifierDeclaration(sh::Visit, sh::TIntermGlobalQualifierDeclaration*) /firefox/gfx/angle/checkout/src/compiler/translator/OutputGLSLBase.cpp:898:63
#4 0x75940ed6fb4a in void sh::TIntermTraverser::traverse<sh::TIntermNode>(sh::TIntermNode*) /firefox/gfx/angle/checkout/src/compiler/translator/tree_util/IntermTraverse.cpp:33:23
#5 0x75940eb7ea91 in sh::TOutputGLSLBase::visitBlock(sh::Visit, sh::TIntermBlock*) /firefox/gfx/angle/checkout/src/compiler/translator/OutputGLSLBase.cpp:868:18
#6 0x75940ed6d39a in sh::TIntermBlock::visit(sh::Visit, sh::TIntermTraverser*) /firefox/gfx/angle/checkout/src/compiler/translator/tree_util/IntermTraverse.cpp:180:16
#7 0x75940ed6d39a in sh::TIntermTraverser::traverseBlock(sh::TIntermBlock*) /firefox/gfx/angle/checkout/src/compiler/translator/tree_util/IntermTraverse.cpp:467:23
#8 0x75940ec5f454 in sh::TranslatorGLSL::translate(sh::TIntermBlock*, ShCompileOptions const&, sh::PerformanceDiagnostics*) /firefox/gfx/angle/checkout/src/compiler/translator/TranslatorGLSL.cpp:213:11
#9 0x75940eb2ce9b in sh::TCompiler::compile(char const* const*, unsigned long, ShCompileOptions const&) /firefox/gfx/angle/checkout/src/compiler/translator/Compiler.cpp:1229:18
#10 0x75940e73dec9 in mozilla::webgl::ShaderValidator::ValidateAndTranslate(char const*) const /firefox/dom/canvas/WebGLShaderValidator.cpp:279:7
#11 0x75940e73c326 in mozilla::WebGLShader::CompileShader() /firefox/dom/canvas/WebGLShader.cpp:100:34
#12 0x75940e66ea25 in mozilla::WebGLContext::CompileShader(mozilla::WebGLShader&) /firefox/dom/canvas/WebGLContextGL.cpp:1496:10
#13 0x75940e5c612c in mozilla::HostWebGLContext::CompileShader(unsigned long) const /firefox/dom/canvas/HostWebGLContext.h:329:15
#14 0x75940e5a808e in void mozilla::ClientWebGLContext::Run_WithDestArgTypes<void (mozilla::HostWebGLContext::*)(unsigned long) const, unsigned long>(std::optional<JS::AutoCheckCannotGC>&&, void (mozilla::HostWebGLContext::*)(unsigned long) const, unsigned long, unsigned long const&) const /firefox/dom/canvas/ClientWebGLContext.cpp:449:5
#15 0x75940e4ba362 in void mozilla::ClientWebGLContext::Run_WithDestArgTypes_ConstnessHelper<unsigned long>(std::optional<JS::AutoCheckCannotGC>&&, void (mozilla::HostWebGLContext::*)(unsigned long) const, unsigned long, std::remove_reference<std::remove_const<unsigned long>::type>::type const&) const /firefox/dom/canvas/ClientWebGLContext.h:2388:5
#16 0x75940e4ba362 in void mozilla::ClientWebGLContext::Run<void (mozilla::HostWebGLContext::*)(unsigned long) const, &mozilla::HostWebGLContext::CompileShader(unsigned long) const, unsigned long>(unsigned long const&) const /firefox/dom/canvas/ClientWebGLContext.h:2358:5
#17 0x75940e4ba362 in mozilla::ClientWebGLContext::CompileShader(mozilla::WebGLShaderJS&) const /firefox/dom/canvas/ClientWebGLContext.cpp:6861:3
#18 0x75940d7dfd0e in mozilla::dom::WebGLRenderingContext_Binding::compileShader(JSContext*, JS::Handle<JSObject*>, void*, JSJitMethodCallArgs const&) /firefox/obj-firefox-asan/dom/bindings/./WebGLRenderingContextBinding.cpp:16268:24
#19 0x75940e2027bd in bool mozilla::dom::binding_detail::GenericMethod<mozilla::dom::binding_detail::NormalThisPolicy, mozilla::dom::binding_detail::ThrowExceptions>(JSContext*, unsigned int, JS::Value*) /firefox/dom/bindings/BindingUtils.cpp:3378:13
#20 0x759417db935a in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:490:13
#21 0x759417db935a in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:586:12
==7477==Register values:
rax = 0x0000000200000018 rbx = 0x00007ffd1d79b520 rcx = 0x0000000040000003 rdx = 0x000000008fff6fff
rdi = 0x0000000200000001 rsi = 0x0000000000000000 rbp = 0x00007ffd1d79b590 rsp = 0x00007ffd1d79b520
r8 = 0x0000000000000000 r9 = 0x00007fffffffff01 r10 = 0x0000765432c1d701 r11 = 0x0000000000000001
r12 = 0x00000eb28631ae10 r13 = 0x0000778432bc0d68 r14 = 0x00007594318d7080 r15 = 0x00007594318d7080
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/firefox/obj-firefox-asan/dist/bin/libxul.so+0x1a9c8d26) (BuildId: bc75d35c2d8c6b7624508c50301aeb52)
==7477==ABORTING
References
On This Page