CVE-2026-9928
Overview
Background
- ANGLE translator
- the shader compiler inside ANGLE that parses ESSL into an AST (
TIntermNodetree) 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-voidfunction whose body does not already end in a return. Previously it lived only undertree_ops/hlsl/; the patch promotes it to the sharedtree_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 = truewhen it lexes areturninside a constant-false branch, butaddIfElse/appendStatementthen 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.
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
- 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).
- Craft the empty body
write a non-
voidfunction whose onlyreturnsits inside a constant-false branch, e.g.if (false) { return 1; }, somFunctionReturnsValueis set during lexing. - Prune
constant folding drops the dead branch;
appendStatementskips the resultingnullptr, leaving the function body with zero statements while the returns-value flag stays true. - OOB read
AddDefaultReturnStatements→EndsInReturncallsgetSequence()->back()on the empty vector, reading out-of-bounds memory as aTIntermNode*. - Wild virtual call (inferred)
that garbage pointer is used for the virtual
getAsBranchNode()dispatch, the documented path toward GPU-process code execution.
Impact Assessment
Files Changed
src/compiler.gnisrc/compiler/translator/glsl/TranslatorESSL.cppsrc/compiler/translator/glsl/TranslatorGLSL.cppsrc/compiler/translator/hlsl/TranslatorHLSL.cppsrc/compiler/translator/tree_ops/AddDefaultReturnStatements.cppsrc/compiler/translator/tree_ops/AddDefaultReturnStatements.h
Audit Directions
- Unchecked `getSequence()->back()`/`front()`grep the translator tree-ops for container
back()/front()/[0]onTIntermBlock/TIntermSequencewithout 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 paritynow 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 pruningreview
addIfElse/appendStatementand similar for other transforms that assume a non-empty body after a branch was folded away.
Patch
From 917bb0e1d008c28be3f9ae70c7b71272997c2f1a Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
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 <syoussefi@chromium.org>
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
---
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 @@
Regression Test / PoC
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(
Original Bug Report
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
Is the security issue related to a crash?
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