High firefox Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionMemory safety bugs present in Firefox ESR 115.36, Firefox ESR 140.11, Thunderbird ESR 140.11, Firefox 151 and Thunderbird 151. Some of these bugs showed evidence of memory corruption and we presume that with enough effort some of these could have been exploited to run arbitrary code.
ComponentGraphics
Bug ClassMemory Corruption
Tracker2029402
Fix commit30c9b7c8ee95 (firefox) +28/-0
CISA KEVNot listed
CreditedAndrew McCreight, Randell Jesup, Tom Ritter and the Mozilla Fuzzing Team
Disclosed2026-06-16

Background

ANGLE
Firefox’s translation layer that compiles WebGL/GLSL shaders down to the host platform’s native graphics API.
`EmulateGLFragColorBroadcast`
an ANGLE tree-transformation pass that rewrites the deprecated gl_FragColor output into writes across the gl_FragData array for backends lacking native broadcast.
`TIntermGlobalQualifierDeclaration`
an AST node representing a bare global qualifier statement such as invariant gl_FragColor; that decorates a built-in without assigning it.
`ReferenceBuiltInVariable`
an ANGLE helper that manufactures a fresh, correctly typed symbol node for a named built-in from the symbol table.

Root Cause Analysis

The GLFragColorBroadcastTraverser pass rewrote every ordinary reference to gl_FragColor (and gl_SecondaryFragColorEXT) into gl_FragData array accesses via visitSymbol, but it never inspected TIntermGlobalQualifierDeclaration nodes, so a global qualifier statement like invariant gl_FragColor; left an un-rewritten gl_FragColor symbol in the AST. The invariant the pass is supposed to guarantee — that no gl_FragColor symbol survives into later stages once the built-in has been emulated away — was therefore violated for the qualifier-declaration code path. Downstream compiler passes and code generation then operate on a symbol whose backing built-in has been eliminated/reshaped, producing an inconsistent, corrupted intermediate state that manifests as memory corruption.

The fix adds an explicit visitGlobalQualifierDeclaration override that detects the BuiltIn symbols gl_FragColor and gl_SecondaryFragColorEXT inside these declarations and rewrites them to gl_FragData / gl_SecondaryFragDataEXT with queueReplacementWithParent. By covering the previously unhandled node type, the pass now upholds its total-rewrite invariant so no stale built-in reference reaches later stages.

Key insight
The single mistake was an incomplete AST traversal: the broadcast-emulation pass rewrote gl_FragColor in expression positions but ignored the same built-in appearing inside a global qualifier declaration, leaving a stale reference. The fix closes the gap by overriding visitGlobalQualifierDeclaration so the built-in is rewritten in that node type too.

Attack Path

  1. Craft a malicious shader An attacker authors a WebGL fragment shader that both uses gl_FragColor output emulation and declares it with a bare global qualifier, e.g. invariant gl_FragColor;.
  2. Submit to WebGL The shader is delivered to a victim via any page with WebGL and compiled through ANGLE’s translator.
  3. Bypass rewrite The EmulateGLFragColorBroadcast pass rewrites the assignment uses but leaves the gl_FragColor symbol inside the TIntermGlobalQualifierDeclaration untouched.
  4. Trigger corruption Later translator passes and code generation dereference the stale built-in whose emulation has already removed its backing, corrupting compiler memory state.

Impact Assessment

An attacker gains memory corruption within the process that runs the ANGLE shader translator when a victim renders a crafted WebGL shader, which Mozilla assesses as potentially exploitable for arbitrary code execution. The only precondition is that the target can be induced to compile the malicious shader (visiting a web page with WebGL enabled). No special privileges are required beyond serving the shader.

Changed Functions

FunctionChangeNotes
GLFragColorBroadcastTraverser
gfx/angle/checkout/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
modified

Files Changed

  • gfx/angle/checkout/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp

Audit Directions

  • Incomplete visitor coverage
    When a tree-transform pass must eliminate a symbol entirely, audit every AST node type that can hold that symbol (visitSymbol, visitGlobalQualifierDeclaration, and other overrides), not just the common expression cases.
  • Built-in emulation invariants
    For any pass that rewrites or removes a built-in, verify no residual reference to the original built-in can survive into later stages, including declaration-only and qualifier contexts.
  • Secondary/extension built-ins
    Check that companion built-ins like gl_SecondaryFragColorEXT and gl_SecondaryFragDataEXT receive the same rewriting treatment as their primary counterparts across all node types.
diff --git a/gfx/angle/checkout/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp b/gfx/angle/checkout/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
index 25e1fde29a2..606a94ff9b9 100644
--- a/gfx/angle/checkout/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
+++ b/gfx/angle/checkout/src/compiler/translator/tree_ops/EmulateGLFragColorBroadcast.cpp
@@ -26,6 +26,7 @@ namespace
 {
 
 constexpr const ImmutableString kGlFragDataString("gl_FragData");
+constexpr const ImmutableString kGlSecondaryFragDataString("gl_SecondaryFragDataEXT");
 
 class GLFragColorBroadcastTraverser : public TIntermTraverser
 {
@@ -42,6 +43,8 @@ class GLFragColorBroadcastTraverser : public TIntermTraverser
     bool isGLFragColorUsed() const { return mGLFragColorUsed; }
 
   protected:
+    bool visitGlobalQualifierDeclaration(Visit visit,
+                                         TIntermGlobalQualifierDeclaration *node) override;
     void visitSymbol(TIntermSymbol *node) override;
 
     TIntermBinary *constructGLFragDataNode(int index) const;
@@ -71,6 +74,31 @@ TIntermBinary *GLFragColorBroadcastTraverser::constructGLFragDataAssignNode(int
     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 && node->getName() == "gl_FragColor")
Loading diff…