High CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Sub-componentTranslator
Bug ClassOOB
Tracker501125002
Fix commit917bb0e1d008 (angle/angle) +60/-14
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedJeff Muizelaar - Mozilla
Reported2026-04-09 (per vendor advisory)
Fix landed2026-04-23 (public git)
Disclosed2026-05-27
Patch gap34 days — fix public in source before the advisory

Background

ANGLE translator
the shader compiler inside ANGLE that parses ESSL into an AST (TIntermNode tree) and re-emits it for a target backend (HLSL/GLSL/MSL/SPIR-V). Runs in the GPU process when a page compiles a WebGL shader.
`AddDefaultReturnStatements`
a tree-op (AST transform) that appends a synthetic return <zero>; to any non-void function whose body does not already end in a return. Previously it lived only under tree_ops/hlsl/; the patch promotes it to the shared tree_ops/ directory and runs it for the GLSL/ESSL generators too.
`TIntermBlock::getSequence()`
returns the std::vector<TIntermNode*> of statements in a block. back() on an empty vector is undefined behaviour — it reads one element before/at the end of a zero-length buffer (heap-buffer underflow/OOB read).
`getAsBranchNode()`
a virtual-dispatch downcast on a TIntermNode*. Calling it on a garbage pointer obtained from an OOB read is a “wild” virtual call.
Constant-folding pruning
the parser may set mFunctionReturnsValue = true when it lexes a return inside a constant-false branch, but addIfElse/appendStatement then prune the dead block, leaving the body empty while the “returns value” flag stays set.

Root Cause Analysis

The invariant is that AddDefaultReturnStatements must tolerate a function with an empty body: a non-void function can legitimately end up with zero statements after dead-code pruning even though the parser’s mFunctionReturnsValue flag was set. Per the report, the old HLSL-only EndsInReturn helper read bodyNode->getSequence()->back() without first checking that the sequence is non-empty. When a constant-false if whose then-branch contained the only return is pruned to nullptr and skipped, the function body is empty but addFunctionDefinition’s check passes (the flag is already true), so AddDefaultReturnStatements runs and back() reads out of bounds. The result is dereferenced as a TIntermNode* and a virtual getAsBranchNode() is invoked on it — a wild virtual call, potentially GPU-process RCE on the HLSL/D3D path.

The patch rewrites the transform in the newly created src/compiler/translator/tree_ops/AddDefaultReturnStatements.cpp. Its EndsInReturn(TIntermBlock*) now begins with an explicit guard — if (body->getSequence()->empty()) return false; — before ever calling back(), so an empty body is correctly treated as “does not end in return” and a default return is appended (or at minimum the OOB back() is never reached). NeedsReturnStatement recomputes the need from the actual body via EndsInReturn rather than trusting a stale flag. The change also deletes the old tree_ops/hlsl/AddDefaultReturnStatements.{cpp,h} from compiler.gni, adds the shared file, and wires sh::AddDefaultReturnStatements(this, root) into TranslatorESSL and TranslatorGLSL (guarded by !compileOptions.useIR); TranslatorHLSL now also runs it only on the non-IR path. The advisory’s “Out of bounds read” (CWE-125) is corroborated by the diff: the empty-sequence guard is exactly the missing bounds check.

Key insight
EndsInReturn called getSequence()->back() on a function body that dead-code pruning could legitimately leave empty — a missing empty-vector check that turned into an out-of-bounds read feeding a wild virtual call.

Attack Path

  1. Attacker page serve a WebGL context and submit a crafted ESSL shader for compilation (reaches the ANGLE translator in the GPU process; on Windows the HLSL path was the originally vulnerable one).
  2. Craft the empty body write a non-void function whose only return sits inside a constant-false branch, e.g. if (false) { return 1; }, so mFunctionReturnsValue is set during lexing.
  3. Prune constant folding drops the dead branch; appendStatement skips the resulting nullptr, leaving the function body with zero statements while the returns-value flag stays true.
  4. OOB read AddDefaultReturnStatements → EndsInReturn calls getSequence()->back() on the empty vector, reading out-of-bounds memory as a TIntermNode*.
  5. Wild virtual call (inferred) that garbage pointer is used for the virtual getAsBranchNode() dispatch, the documented path toward GPU-process code execution.

Impact Assessment

Runs in the GPU process, reachable by any origin that can compile a WebGL shader (no special privilege, requires a user to load the page). The diff substantiates an out-of-bounds (heap-underflow) read of a vector past its bounds; the subsequent “wild virtual call” and RCE are the reporter’s analysis of how the OOB-read pointer is consumed and are (inferred) relative to this diff, which only adds the empty-check. Strength is an attacker-crafted-input-triggered OOB read with a plausible control-flow-hijack follow-on on the HLSL/Windows path; on other backends the same empty body would also have been mis-handled, which is why the transform is now shared.

Files Changed

  • src/compiler.gni
  • src/compiler/translator/glsl/TranslatorESSL.cpp
  • src/compiler/translator/glsl/TranslatorGLSL.cpp
  • src/compiler/translator/hlsl/TranslatorHLSL.cpp
  • src/compiler/translator/tree_ops/AddDefaultReturnStatements.cpp
  • src/compiler/translator/tree_ops/AddDefaultReturnStatements.h

Audit Directions

  • Unchecked `getSequence()->back()`/`front()`
    grep the translator tree-ops for container back()/front()/[0] on TIntermBlock/TIntermSequence without a prior empty check; the same dead-code-pruned-to-empty case can recur elsewhere.
  • Stale `mFunctionReturnsValue`
    audit every consumer of the parser’s “function returns a value” flag; it can be true while the body is empty after folding, so any logic trusting the flag over the actual AST is suspect.
  • Backend parity
    now that the transform is shared, confirm the GLSL/ESSL/MSL/SPIR-V generators all tolerate empty bodies identically; the bug was long masked because only HLSL ran the transform.
  • Constant-folding + statement pruning
    review addIfElse/appendStatement and similar for other transforms that assume a non-empty body after a branch was folded away.
From 917bb0e1d008c28be3f9ae70c7b71272997c2f1a Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <[email protected]>
Date: Wed, 22 Apr 2026 13:39:06 -0400
Subject: [PATCH] Translator: Fix AddDefaultReturnStatements vs empty function body

This change simultaneously applies this transform for the GLSL generator
as well to make the test pass on mesa.

Bug: chromium:501125002
Change-Id: I11640d494eee79387649140bde07868614e61206
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7786014
Commit-Queue: Shahbaz Youssefi <[email protected]>
Reviewed-by: Yuxin Hu <[email protected]>
---

diff --git a/src/compiler.gni b/src/compiler.gni
index d96767c..29c9c8e 100644
--- a/src/compiler.gni
+++ b/src/compiler.gni
@@ -121,6 +121,8 @@
   "src/compiler/translator/msl/TranslatorMSL.h",
   "src/compiler/translator/null/TranslatorNULL.h",
   "src/compiler/translator/spirv/TranslatorSPIRV.h",
+  "src/compiler/translator/tree_ops/AddDefaultReturnStatements.cpp",
+  "src/compiler/translator/tree_ops/AddDefaultReturnStatements.h",
   "src/compiler/translator/tree_ops/ClampFragDepth.cpp",
   "src/compiler/translator/tree_ops/ClampFragDepth.h",
   "src/compiler/translator/tree_ops/ClampIndirectIndices.cpp",
@@ -287,8 +289,6 @@
   "src/compiler/translator/hlsl/UtilsHLSL.h",
   "src/compiler/translator/hlsl/blocklayoutHLSL.cpp",
   "src/compiler/translator/hlsl/emulated_builtin_functions_hlsl_autogen.cpp",
-  "src/compiler/translator/tree_ops/hlsl/AddDefaultReturnStatements.cpp",
-  "src/compiler/translator/tree_ops/hlsl/AddDefaultReturnStatements.h",
   "src/compiler/translator/tree_ops/hlsl/ArrayReturnValueToOutParameter.cpp",
   "src/compiler/translator/tree_ops/hlsl/ArrayReturnValueToOutParameter.h",
   "src/compiler/translator/tree_ops/hlsl/BreakVariableAliasingInInnerLoops.cpp",
diff --git a/src/compiler/translator/glsl/TranslatorESSL.cpp b/src/compiler/translator/glsl/TranslatorESSL.cpp
index dc0c72e..b059caf 100644
--- a/src/compiler/translator/glsl/TranslatorESSL.cpp
+++ b/src/compiler/translator/glsl/TranslatorESSL.cpp
@@ -12,6 +12,7 @@
 #include "compiler/translator/StaticType.h"
 #include "compiler/translator/glsl/BuiltInFunctionEmulatorGLSL.h"
 #include "compiler/translator/glsl/OutputESSL.h"
+#include "compiler/translator/tree_ops/AddDefaultReturnStatements.h"
 #include "compiler/translator/tree_ops/DeclarePerVertexBlocks.h"
 #include "compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.h"
 #include "compiler/translator/tree_ops/RecordConstantPrecision.h"
@@ -104,6 +105,11 @@
             return false;
         }
 
+        if (!sh::AddDefaultReturnStatements(this, root))
+        {
+            return false;
+        }
+
         // anglebug.com/42265954: The ESSL spec has a bug with images as function arguments. The
         // recommended workaround is to inline functions that accept image arguments.
         if (shaderVer >= 310 && !MonomorphizeUnsupportedFunctions(
diff --git a/src/compiler/translator/glsl/TranslatorGLSL.cpp b/src/compiler/translator/glsl/TranslatorGLSL.cpp
index 9c82efa..f63098a 100644
--- a/src/compiler/translator/glsl/TranslatorGLSL.cpp
+++ b/src/compiler/translator/glsl/TranslatorGLSL.cpp
@@ -12,6 +12,7 @@
 #include "compiler/translator/glsl/ExtensionGLSL.h"
 #include "compiler/translator/glsl/OutputGLSL.h"
 #include "compiler/translator/glsl/VersionGLSL.h"
+#include "compiler/translator/tree_ops/AddDefaultReturnStatements.h"
 #include "compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.h"
 #include "compiler/translator/tree_ops/PreTransformTextureCubeGradDerivatives.h"
 #include "compiler/translator/tree_ops/RemoveDynamicIndexing.h"
@@ -97,6 +98,11 @@
 
     if (!compileOptions.useIR)
     {
+        if (!sh::AddDefaultReturnStatements(this, root))
+        {
+            return false;
+        }
+
         // anglebug.com/42265954: The ESSL spec has a bug with images as function arguments. The
         // recommended workaround is to inline functions that accept image arguments.
         if (getShaderVersion() >= 310 &&
diff --git a/src/compiler/translator/hlsl/TranslatorHLSL.cpp b/src/compiler/translator/hlsl/TranslatorHLSL.cpp
index 9a061cc..f44552c 100644
--- a/src/compiler/translator/hlsl/TranslatorHLSL.cpp
+++ b/src/compiler/translator/hlsl/TranslatorHLSL.cpp
@@ -7,11 +7,11 @@
 #include "compiler/translator/hlsl/TranslatorHLSL.h"
 
 #include "compiler/translator/hlsl/OutputHLSL.h"
+#include "compiler/translator/tree_ops/AddDefaultReturnStatements.h"
 #include "compiler/translator/tree_ops/RemoveDynamicIndexing.h"
 #include "compiler/translator/tree_ops/RewriteTexelFetchOffset.h"
 #include "compiler/translator/tree_ops/SimplifyLoopConditions.h"
 #include "compiler/translator/tree_ops/SplitSequenceOperator.h"
-#include "compiler/translator/tree_ops/hlsl/AddDefaultReturnStatements.h"
 #include "compiler/translator/tree_ops/hlsl/ArrayReturnValueToOutParameter.h"
 #include "compiler/translator/tree_ops/hlsl/BreakVariableAliasingInInnerLoops.h"
 #include "compiler/translator/tree_ops/hlsl/ExpandIntegerPowExpressions.h"
@@ -57,9 +57,12 @@
     int maxDualSourceDrawBuffers =
         resources.EXT_blend_func_extended ? resources.MaxDualSourceDrawBuffers : 0;
 
-    if (!sh::AddDefaultReturnStatements(this, root))
+    if (!compileOptions.useIR)
     {
-        return false;
+        if (!sh::AddDefaultReturnStatements(this, root))
+        {
+            return false;
+        }
     }
 
     // Note that SimplifyLoopConditions needs to be run before any other AST transformations that
diff --git a/src/compiler/translator/tree_ops/AddDefaultReturnStatements.cpp b/src/compiler/translator/tree_ops/AddDefaultReturnStatements.cpp
new file mode 100644
index 0000000..92fa40b
--- /dev/null
+++ b/src/compiler/translator/tree_ops/AddDefaultReturnStatements.cpp
@@ -0,0 +1,71 @@
+//
+// Copyright 2016 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.
+//
+// AddDefaultReturnStatements.cpp: Add default return statements to functions that do not end in a
+//                                 return.
+//
+
+#include "compiler/translator/tree_ops/AddDefaultReturnStatements.h"
+
+#include "compiler/translator/Compiler.h"
+#include "compiler/translator/IntermNode.h"
+#include "compiler/translator/tree_util/IntermNode_util.h"
+#include "compiler/translator/util.h"
+
+namespace sh
+{
+
+namespace
+{
+bool EndsInReturn(TIntermBlock *body)
+{
+    if (body->getSequence()->empty())
+    {
+        return false;
+    }
+
+    TIntermNode *lastStatement = body->getSequence()->back();
+    if (lastStatement->getAsBlock())
+    {
+        return EndsInReturn(lastStatement->getAsBlock());
+    }
+
+    TIntermBranch *returnNode = lastStatement->getAsBranchNode();
+    return returnNode != nullptr && returnNode->getFlowOp() == EOpReturn;
+}
+
+bool NeedsReturnStatement(TIntermFunctionDefinition *node, TType *returnType)
+{
+    *returnType = node->getFunctionPrototype()->getType();
+    if (returnType->getBasicType() == EbtVoid)
+    {
+        return false;
+    }
+
+    TIntermBlock *bodyNode = node->getBody();
+    return !EndsInReturn(bodyNode);
+}
+
+}  // anonymous namespace
+
+bool AddDefaultReturnStatements(TCompiler *compiler, TIntermBlock *root)
+{
+    TType returnType;
+    for (TIntermNode *node : *root->getSequence())
+    {
+        TIntermFunctionDefinition *definition = node->getAsFunctionDefinition();
+        if (definition != nullptr && NeedsReturnStatement(definition, &returnType))
+        {
+            TIntermBranch *branch = new TIntermBranch(EOpReturn, CreateZeroNode(returnType));
+
+            TIntermBlock *bodyNode = definition->getBody();
+            bodyNode->getSequence()->push_back(branch);
+        }
+    }
+
+    return compiler->validateAST(root);
+}
+
+}  // namespace sh
diff --git a/src/compiler/translator/tree_ops/AddDefaultReturnStatements.h b/src/compiler/translator/tree_ops/AddDefaultReturnStatements.h
new file mode 100644
index 0000000..885b9a1
--- /dev/null
+++ b/src/compiler/translator/tree_ops/AddDefaultReturnStatements.h
@@ -0,0 +1,24 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 82c07b6..a6cef45 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -23121,6 +23121,27 @@
     EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 127, 127, 127), 1);
     ASSERT_GL_NO_ERROR();
 }
+
+// Regression test for a transformation bug where a function has |return| only in dead code.
+TEST_P(GLSLTest_ES3, EmptyBodyAfterPrunedIfWithReturn)
+{
+    constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 color;
+
+int foo() {
+    if (false) { return 1; }
+}
+
+void main() {
+    color = vec4(float(foo()), 0, 1, 1);
+})";
+
+    ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+    drawQuad(program, essl3_shaders::PositionAttrib(), 0.0f);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+    ASSERT_GL_NO_ERROR();
+}
 }  // anonymous namespace
 
 ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND_ES31_AND_ES32(
Loading diff…

Original Bug Report

reported by [email protected]

Heap buffer underflow read with wild virtual call in [@ sh::AddDefaultReturnStatements] via empty function body in TranslatorHLSL


Report description

Heap buffer underflow read with wild virtual call in [@ sh::AddDefaultReturnStatements] via empty function body in TranslatorHLSL


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/angle/angle


The problem

Please describe the technical details of the vulnerability

The parser sets mFunctionReturnsValue=true when it lexes “return 1;” inside the if-block, but then addIfElse sees the constant false condition and prunes the entire block. appendStatement skips the resulting nullptr, leaving the function body empty. The addFunctionDefinition check passes because mFunctionReturnsValue is already true. AddDefaultReturnStatements then calls bodyNode->getSequence()->back() without an empty check, reading out of bounds. The out of bounds read is then used a TIntermNode* with a virtual call to TIntermBranch *getAsBranchNode()

See attached for a shader compilation test that crashes.

Our bug for this is https://bugzilla.mozilla.org/show_bug.cgi?id=2029814. It has a bunch more detail that might only be relevant to Firefox but I can add you if you’d like.

Impact analysis

Potentially remote execution in the GPU process on Windows.


The cause

What version of Chrome have you found the security issue in?

Stable

No, it is not related to a crash.

Choose the type of vulnerability

Remote Code Execution (RCE)

How would you like to be publicly acknowledged for your report?

Jeff Muizelaar - Mozilla

View on issue tracker