CVE-2026-113157
Overview
Background
- Fragment outputs narrower than vec4
- unwritten components (e.g. alpha) are undefined.
- Blending / read-back
- consumes the output components, so an undefined component can surface in readable pixels.
- ExpandFragmentOutputsToVec4
- the new transform that zero-fills missing components.
Root Cause Analysis
The commit “Always expand non-vec4 fragment outputs to vec4” makes ANGLE expand any fragment shader output narrower than 4 components (e.g. float/vec2/vec3 outputs) to a full gvec4, filling the missing components with 0, on hardened contexts for all backends. The commit message states the problem: “The alpha channel is undefined if unwritten, which leads to undefined values produced when blending.” Previously this was only a per-driver workaround (expand_fragment_outputs_to_vec4); the fix promotes it to a general transform (new ExpandFragmentOutputsToVec4 C++ tree-op and expand_fragment_outputs_to_vec4 Rust IR transform) run whenever expandFragmentOutputsToVec4 is set and the shader is ESSL 3.00+.
The root cause is therefore use of undefined/uninitialized fragment-output components: an unwritten alpha (or other missing component) carries an undefined value that is then consumed by blending and read back, which can expose uninitialized GPU register/memory contents. This CVE (bug chromium:563352515) is the information-leak facet of the shared fix, and the advisory’s “Information leak” label matches the mechanism directly: undefined alpha/components read back through blending can leak uninitialized GPU memory. Kept as InfoDisclosure.
Attack Path
- ENTRY WebGL fragment shader (ESSL 3.00+) with a non-vec4 output, hardened context.
- CONTROLLED INPUT Leave a component unwritten (undefined).
- MISSING CHECK The undefined component is not zero-filled before blending/read-back.
- IMPACT The attacker reads back the undefined component, disclosing uninitialized GPU memory/register values.
Impact Assessment
Files Changed
src/compiler/translator/ir/src/transform.rssrc/libANGLE/renderer/gl/ShaderGL.cppsrc/libANGLE/renderer/gl/renderergl_utils.cppinclude/platform/autogen/FeaturesGL_autogen.hsrc/compiler.gniutil/autogen/angle_features_autogen.cppsrc/compiler/translator/Compiler.cppsrc/compiler/translator/glsl/TranslatorGLSL.cppsrc/compiler/translator/ir/src/transform/expand_fragment_outputs_to_vec4.rssrc/tests/angle_end2end_tests_expectations.txtsrc/compiler/translator/ir/src/ir.rssrc/tests/gl_tests/WebGLCompatibilityTest.cppsrc/compiler/translator/ir/src/transform/emulate_multi_draw.rssrc/compiler/translator/glsl/TranslatorESSL.cppsrc/compiler/translator/ir/BUILD.gnsrc/libANGLE/Shader.cpputil/autogen/angle_features_autogen.hsrc/compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.hsrc/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.cppsrc/compiler/translator/ir/src/compile.cppsrc/compiler/translator/ir/src/compile.rssrc/tests/gl_tests/GLSLTest.cppinclude/platform/gl_features.jsonsrc/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.hsrc/compiler/translator/ir/src/transform/emulate_instanced_multiview.rs
Audit Directions
- Readable undefined stateAudit paths where unwritten/undefined shader outputs become observable (blending, glReadPixels, copies) for uninitialized-data disclosure.
- Hardened-context guaranteesConfirm all undefined-value sources are zero-filled on hardened contexts.
Patch
From 4979a49c3fdb872fcac93c6bfc02480ef14c5faf Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Sun, 20 Sep 2026 16:40:25 -0400
Subject: [PATCH] Always expand non-vec4 fragment outputs to vec4
... on hardened contexts. The alpha channel is undefined if unwritten,
which leads to undefined values produced when blending. Previously,
this was implemented as a driver bug workaround. which is now done for
all backends and drivers.
The MSL generator already does this during codegen regardless of
hardened contexts. That's left alone for now and can be rethought
during the IR port.
Bug: chromium:563350676
Bug: chromium:563352451
Bug: chromium:563352515
Change-Id: Iafc64548b97811620d54bb195eb2b43a3212951e
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8427088
Reviewed-by: Yuxin Hu <yuxinhu@google.com>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index 080bf18..6557442 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -940,14 +940,6 @@
"issue": "http://crbug.com/475877320"
},
{
- "name": "expand_fragment_outputs_to_vec4",
- "category": "Workarounds",
- "description": [
- "Expand fragment shader outputs to 4 components to work around compiler bugs."
- ],
- "issue": "http://crbug.com/491732188"
- },
- {
"name": "recreate_mipmap_levels_before_generate",
"category": "Workarounds",
"description": [
diff --git a/src/compiler/translator/Compiler.cpp b/src/compiler/translator/Compiler.cpp
index d76cffe..8b15540 100644
--- a/src/compiler/translator/Compiler.cpp
+++ b/src/compiler/translator/Compiler.cpp
@@ -31,6 +31,7 @@
#include "compiler/translator/tree_ops/DeferGlobalInitializers.h"
#include "compiler/translator/tree_ops/EmulateGLFragColorBroadcast.h"
#include "compiler/translator/tree_ops/EmulateMultiDrawShaderBuiltins.h"
+#include "compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.h"
#include "compiler/translator/tree_ops/FoldExpressions.h"
#include "compiler/translator/tree_ops/InitializeVariables.h"
#include "compiler/translator/tree_ops/PruneEmptyCases.h"
@@ -1172,6 +1173,14 @@
}
}
+ if (compileOptions.expandFragmentOutputsToVec4 && mShaderVersion >= 300)
+ {
+ if (!ExpandFragmentOutputsToVec4(this, root, &getSymbolTable()))
+ {
+ return false;
+ }
+ }
+
return true;
}
diff --git a/src/compiler/translator/glsl/TranslatorESSL.cpp b/src/compiler/translator/glsl/TranslatorESSL.cpp
index 69889c4..5c65939 100644
--- a/src/compiler/translator/glsl/TranslatorESSL.cpp
+++ b/src/compiler/translator/glsl/TranslatorESSL.cpp
@@ -17,7 +17,6 @@
#include "compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.h"
#include "compiler/translator/tree_ops/RecordConstantPrecision.h"
#include "compiler/translator/tree_ops/RemoveDynamicIndexing.h"
-#include "compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.h"
#include "compiler/translator/tree_util/FindSymbolNode.h"
#include "compiler/translator/tree_util/ReplaceClipCullDistanceVariable.h"
#include "compiler/translator/tree_util/RunAtTheEndOfShader.h"
@@ -128,13 +127,6 @@
return false;
}
}
- if (compileOptions.expandFragmentOutputsToVec4)
- {
- if (!ExpandFragmentOutputsToVec4(this, root, &getSymbolTable()))
- {
- return false;
- }
- }
// Write emulated built-in functions if needed.
BuiltInFunctionEmulator builtInFunctionEmulator;
diff --git a/src/compiler/translator/glsl/TranslatorGLSL.cpp b/src/compiler/translator/glsl/TranslatorGLSL.cpp
index 6ee3f56..f63098a 100644
--- a/src/compiler/translator/glsl/TranslatorGLSL.cpp
+++ b/src/compiler/translator/glsl/TranslatorGLSL.cpp
@@ -18,7 +18,6 @@
#include "compiler/translator/tree_ops/RemoveDynamicIndexing.h"
#include "compiler/translator/tree_ops/RemoveInvariantDeclaration.h"
#include "compiler/translator/tree_ops/RewriteTexelFetchOffset.h"
-#include "compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.h"
#include "compiler/translator/tree_ops/glsl/apple/RewriteRowMajorMatrices.h"
namespace sh
@@ -138,13 +137,6 @@
return false;
}
}
- if (compileOptions.expandFragmentOutputsToVec4)
- {
- if (!ExpandFragmentOutputsToVec4(this, root, &getSymbolTable()))
- {
- return false;
- }
- }
// Write emulated built-in functions if needed.
BuiltInFunctionEmulator builtInFunctionEmulator;
diff --git a/src/compiler/translator/ir/src/compile.cpp b/src/compiler/translator/ir/src/compile.cpp
index dafb127..dfe05dc 100644
--- a/src/compiler/translator/ir/src/compile.cpp
+++ b/src/compiler/translator/ir/src/compile.cpp
@@ -155,6 +155,7 @@
opt->retain_inactive_fragment_outputs = options.retainInactiveFragmentOutputs;
opt->scalarize_vec_and_mat_constructor_args = options.scalarizeVecAndMatConstructorArgs;
opt->clamp_indirect_indices = options.clampIndirectArrayBounds;
+ opt->expand_fragment_outputs_to_vec4 = options.expandFragmentOutputsToVec4;
opt->rewrite_pixel_local_storage = compiler->hasPixelLocalStorageUniforms();
opt->pls_options.implementation = static_cast<ffi::PixelLocalStorageImpl>(options.pls.type);
diff --git a/src/compiler/translator/ir/src/compile.rs b/src/compiler/translator/ir/src/compile.rs
index 6dbfd5c..39411f0 100644
--- a/src/compiler/translator/ir/src/compile.rs
+++ b/src/compiler/translator/ir/src/compile.rs
@@ -219,6 +219,8 @@
scalarize_vec_and_mat_constructor_args: bool,
// Clamp non-constant indices to the bounds of the entity being indexed for robustness.
clamp_indirect_indices: bool,
+ // Whether ESSL300 fragment outputs should be expanded to vec4s.
+ expand_fragment_outputs_to_vec4: bool,
// Whether the ANGLE_pixel_local_storage extension has been used and there are PLS uniforms
// to rewrite.
@@ -673,6 +675,11 @@
};
transform::run!(localized_workarounds, ir, &transform_options);
}
+
+ if options.expand_fragment_outputs_to_vec4 && options.shader_version >= 300 {
+ debug_assert!(ir.meta.get_shader_type() == ShaderType::Fragment);
+ transform::run!(expand_fragment_outputs_to_vec4, ir);
+ }
}
fn initialize_global_pool_index_workaround() {
diff --git a/src/compiler/translator/ir/src/ir.rs b/src/compiler/translator/ir/src/ir.rs
index 68e95c4..48e2443 100644
--- a/src/compiler/translator/ir/src/ir.rs
+++ b/src/compiler/translator/ir/src/ir.rs
@@ -3354,17 +3354,18 @@
// variable is replaced with the global variable and a new id is assigned to the interface
// variable and returned. This way, the shader does not need to be modified except for
// possibly writing to the cache variable at the start of shader and reading from it at the
- // end.
+ // end. If requested, the original variable's type is overridden with a given type.
pub fn declare_cached_global_for_variable(
&mut self,
variable_id: VariableId,
cache_name: &'static str,
+ original_variable_type_override: Option<TypeId>,
) -> (VariableId, TypedId) {
let variable = self.get_variable_mut(variable_id);
// Replace the variable with a private global.
let original_name = std::mem::replace(&mut variable.name, Name::new_temp(cache_name));
- let type_id = variable.type_id;
+ let type_id = original_variable_type_override.unwrap_or(variable.type_id);
let precision = variable.precision;
let precise = variable.precise;
let original_decorations =
diff --git a/src/compiler/translator/ir/src/transform.rs b/src/compiler/translator/ir/src/transform.rs
index cf13c07..4fb0668 100644
--- a/src/compiler/translator/ir/src/transform.rs
+++ b/src/compiler/translator/ir/src/transform.rs
@@ -9,6 +9,7 @@
pub mod dealias;
pub mod emulate_instanced_multiview;
pub mod emulate_multi_draw;
+pub mod expand_fragment_outputs_to_vec4;
pub mod initialize_uninitialized_variables;
pub mod localized_workarounds;
pub mod monomorphize_unsupported_functions;
diff --git a/src/compiler/translator/ir/src/transform/emulate_instanced_multiview.rs b/src/compiler/translator/ir/src/transform/emulate_instanced_multiview.rs
index 39ea3f1..7f8bbf0 100644
--- a/src/compiler/translator/ir/src/transform/emulate_instanced_multiview.rs
+++ b/src/compiler/translator/ir/src/transform/emulate_instanced_multiview.rs
@@ -84,7 +84,10 @@
// Make a duplicate of gl_InstanceID to add to globals, and let the shader use the replacement.
(
- state.ir_meta.declare_cached_global_for_variable(replaced_instance_id, "InstanceID").1,
+ state
+ .ir_meta
+ .declare_cached_global_for_variable(replaced_instance_id, "InstanceID", None)
+ .1,
replaced_instance_id_typed,
)
}
diff --git a/src/compiler/translator/ir/src/transform/emulate_multi_draw.rs b/src/compiler/translator/ir/src/transform/emulate_multi_draw.rs
index a942cef..d3d37ba 100644
--- a/src/compiler/translator/ir/src/transform/emulate_multi_draw.rs
+++ b/src/compiler/translator/ir/src/transform/emulate_multi_draw.rs
@@ -64,7 +64,7 @@
fn add_base_vertex_to_vertex_id(ir: &mut IR) {
if let Some(vertex_id) = ir.meta.get_built_in_variable(BuiltIn::VertexID) {
let replaced_vertex_id = TypedId::from_variable_id(&ir.meta, vertex_id);
- let vertex_id = ir.meta.declare_cached_global_for_variable(vertex_id, "VertexID").1;
+ let vertex_id = ir.meta.declare_cached_global_for_variable(vertex_id, "VertexID", None).1;
let base_vertex = ir.meta.get_or_declare_built_in_variable(BuiltIn::BaseVertex).1;
diff --git a/src/compiler/translator/ir/src/transform/expand_fragment_outputs_to_vec4.rs b/src/compiler/translator/ir/src/transform/expand_fragment_outputs_to_vec4.rs
new file mode 100644
index 0000000..294f53e
--- /dev/null
+++ b/src/compiler/translator/ir/src/transform/expand_fragment_outputs_to_vec4.rs
@@ -0,0 +1,153 @@
+// Copyright 2026 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.
+//
+// Turn gvecN (N < 4) fragment outputs to gvec4, filling the missing components with 0.
+
+use crate::ir::*;
+use crate::*;
+
+pub fn run(ir: &mut IR) {
+ for variable_id in ir.meta.all_global_variables().clone() {
+ // Must be an `out` variable
+ let variable = ir.meta.get_variable(variable_id);
+ if !variable.decorations.has(Decoration::Output) {
+ continue;
+ }
+
+ // Must have fewer than 4 components.
+ let base_type_id = ir.meta.get_pointee_type(variable.type_id);
+ let (base_type_id, array_size) =
+ if let &Type::Array(base_type_id, size) = ir.meta.get_type(base_type_id) {
+ (base_type_id, Some(size))
+ } else {
+ (base_type_id, None)
+ };
+
+ if let Some((expanded_type_id, component_count, zero)) = expand_type_to_vec4(base_type_id) {
+ replace_output_variable(
+ ir,
+ variable_id,
+ variable.name.name,
+ expanded_type_id,
+ component_count,
+ array_size,
+ zero,
+ );
+ }
+ }
+}
+
+fn expand_type_to_vec4(type_id: TypeId) -> Option<(TypeId, u32, TypedId)> {
+ match type_id {
+ TYPE_ID_FLOAT => Some((TYPE_ID_VEC4, 1, TYPED_CONSTANT_ID_FLOAT_ZERO)),
+ TYPE_ID_VEC2 => Some((TYPE_ID_VEC4, 2, TYPED_CONSTANT_ID_FLOAT_ZERO)),
+ TYPE_ID_VEC3 => Some((TYPE_ID_VEC4, 3, TYPED_CONSTANT_ID_FLOAT_ZERO)),
+ TYPE_ID_INT => Some((TYPE_ID_IVEC4, 1, TYPED_CONSTANT_ID_INT_ZERO)),
+ TYPE_ID_IVEC2 => Some((TYPE_ID_IVEC4, 2, TYPED_CONSTANT_ID_INT_ZERO)),
+ TYPE_ID_IVEC3 => Some((TYPE_ID_IVEC4, 3, TYPED_CONSTANT_ID_INT_ZERO)),
+ TYPE_ID_UINT => Some((TYPE_ID_UVEC4, 1, TYPED_CONSTANT_ID_UINT_ZERO)),
+ TYPE_ID_UVEC2 => Some((TYPE_ID_UVEC4, 2, TYPED_CONSTANT_ID_UINT_ZERO)),
+ TYPE_ID_UVEC3 => Some((TYPE_ID_UVEC4, 3, TYPED_CONSTANT_ID_UINT_ZERO)),
+ _ => None,
+ }
+}
+
+fn replace_output_variable(
+ ir: &mut IR,
+ variable_id: VariableId,
+ name: &'static str,
+ expanded_type_id: TypeId,
+ component_count: u32,
+ array_size: Option<u32>,
+ basic_type_zero: TypedId,
+) {
+ let complete_type = if let Some(array_size) = array_size {
+ ir.meta.get_array_type_id(expanded_type_id, array_size)
+ } else {
+ expanded_type_id
+ };
+
+ // Use a global instead of the fragment output, declare a gvec4 fragment output instead and
+ // and at the end of the shader copy it over to the expanded variable.
+ let new_output =
+ ir.meta.declare_cached_global_for_variable(variable_id, name, Some(complete_type)).1;
+ let global = TypedId::from_variable_id(&ir.meta, variable_id);
+
+ let mut postamble = Block::new();
+
+ if let Some(array_size) = array_size {
+ for index in 0..array_size {
+ // globalN = AccessArrayElement global N
+ // outputN = AccessArrayElement output N
+ let index = ir.meta.get_constant_uint_typed(index, Precision::Low);
+ let global_indexed =
+ postamble.add_typed_instruction(instruction::index(&mut ir.meta, global, index));
+ let output_indexed = postamble.add_typed_instruction(instruction::index(
+ &mut ir.meta,
+ new_output,
+ index,
+ ));
+ copy_and_expand(
+ &mut ir.meta,
+ &mut postamble,
+ global_indexed,
+ output_indexed,
+ expanded_type_id,
+ component_count,
+ basic_type_zero,
+ );
+ }
+ } else {
+ copy_and_expand(
+ &mut ir.meta,
+ &mut postamble,
+ global,
+ new_output,
+ expanded_type_id,
+ component_count,
+ basic_type_zero,
+ );
+ }
+
+ ir.append_to_main(postamble);
+}
+
+fn copy_and_expand(
+ ir_meta: &mut IRMeta,
+ postamble: &mut Block,
+ global: TypedId,
+ output: TypedId,
+ expanded_type_id: TypeId,
+ component_count: u32,
+ basic_type_zero: TypedId,
+) {
+ // Produce statements like output = vec4(global.x, global.y, zero, zero).
+ let mut constructor_args = if component_count > 1 {
+ // x = AccessVectorComponent global 0
+ // x' = Load x
+ // y = AccessVectorComponent global 1
+ // y' = Load y
+ // expanded = ConstructVectorFromMultiple (x, y, zero, zero)
+ (0..component_count)
+ .map(|component| {
+ let swizzle = postamble.add_typed_instruction(instruction::vector_component(
+ ir_meta, global, component,
+ ));
+ postamble.add_typed_instruction(instruction::load(ir_meta, swizzle))
+ })
+ .collect::<Vec<_>>()
+ } else {
+ // x = Load global
+ // expanded = ConstructVectorFromMultiple (x, zero, zero, zero)
+ vec![postamble.add_typed_instruction(instruction::load(ir_meta, global))]
+ };
+ constructor_args.resize(4, basic_type_zero);
+ let expanded = postamble.add_typed_instruction(instruction::construct(
+ ir_meta,
+ expanded_type_id,
+ constructor_args,
+ None,
+ ));
+ postamble.add_void_instruction(OpCode::Store(output, expanded));
+}
diff --git a/src/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.cpp b/src/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.cpp
new file mode 100644
index 0000000..2875551
--- /dev/null
+++ b/src/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.cpp
@@ -0,0 +1,151 @@
+// Copyright 2026 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.
+
+#include "compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.h"
+
+#include "compiler/translator/tree_util/IntermNode_util.h"
+#include "compiler/translator/tree_util/ReplaceVariable.h"
+#include "compiler/translator/tree_util/RunAtTheEndOfShader.h"
+
+namespace sh
... (file diff truncated)
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 2382235e..76a3a9f 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -219,6 +219,9 @@
535213600 WIN NVIDIA OPENGL : PbufferTestES3.BindTexImageAndCopyTextureInvertedSrc/* = SKIP
535213600 WIN NVIDIA GLES : PbufferTestES3.BindTexImageAndCopyTextureInvertedSrc/* = SKIP
535213600 WIN NVIDIA GLES : PbufferTestES3.BindTexImageAndCopyTexSubTextureInvertedDst/* = SKIP
+564530612 WIN INTEL D3D11 : HardenedContextBlendTest.AlphaBlendNoAlphaChannelInSrcAndDst/* = SKIP
+564530612 WIN INTEL D3D11 : HardenedContextBlendTest.AlphaBlendNoAlphaChannelInSrcAndDst2/* = SKIP
+564530612 WIN INTEL D3D11 : HardenedContextBlendTest.AlphaBlendNoAlphaChannelInSrcAndDstArray/* = SKIP
42266241 PIXEL6 VULKAN : VertexAttributeTestES31.MismatchingSignsChangingAttributeType/* = SKIP
42266241 PIXEL6 VULKAN : VertexAttributeTestES31.MismatchingSignsChangingProgramType/* = SKIP
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 74b7fa4..8ccb1e5 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -24362,16 +24362,13 @@
ASSERT_GL_NO_ERROR();
}
-class GLSLTest_ES3_Blend : public GLSLTest_ES3
-{};
-
// Test alpha blend where both the framebuffer and shader miss the alpha channel. The spec says
// that:
//
// > If a color buffer has no A value, then A_d is taken to be 1.
//
// But it says nothing about what happens if the shader does not write to alpha and A_s.
-TEST_P(GLSLTest_ES3_Blend, AlphaBlendNoAlphaChannelInSrcAndDst)
+TEST_P(GLSLTest_ES3, AlphaBlendNoAlphaChannelInSrcAndDst)
{
GLTexture color;
glBindTexture(GL_TEXTURE_2D, color);
@@ -24407,7 +24404,7 @@
}
// Test alpha blend where the framebuffer misses the alpha channel, but the shader writes to alpha.
-TEST_P(GLSLTest_ES3_Blend, AlphaBlendNoAlphaChannelInDst)
+TEST_P(GLSLTest_ES3, AlphaBlendNoAlphaChannelInDst)
{
GLTexture color;
glBindTexture(GL_TEXTURE_2D, color);
@@ -24438,7 +24435,7 @@
// Test blend where the framebuffer misses the alpha channel. Uses (GL_DST_COLOR, GL_ZERO) blend
// that hits an optimization path in the mesa/Radeon driver.
-TEST_P(GLSLTest_ES3_Blend, ColorBlendNoAlphaChannelInDst)
+TEST_P(GLSLTest_ES3, ColorBlendNoAlphaChannelInDst)
{
GLTexture color;
glBindTexture(GL_TEXTURE_2D, color);
@@ -26101,10 +26098,5 @@
ES3_METAL(),
ES3_VULKAN());
-GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(GLSLTest_ES3_Blend);
-ANGLE_INSTANTIATE_TEST_ES3_AND(GLSLTest_ES3_Blend,
- ES3_OPENGL().enable(Feature::ExpandFragmentOutputsToVec4),
- ES3_OPENGLES().enable(Feature::ExpandFragmentOutputsToVec4));
-
ANGLE_INSTANTIATE_TEST_ES2_AND(GLSLTestPassthrough,
ES2_OPENGLES().enable(Feature::ForcePassthroughShaders));
diff --git a/src/tests/gl_tests/WebGLCompatibilityTest.cpp b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
index f00c1b5..0c21744 100644
--- a/src/tests/gl_tests/WebGLCompatibilityTest.cpp
+++ b/src/tests/gl_tests/WebGLCompatibilityTest.cpp
@@ -368,6 +368,9 @@
class HardenedContextTestES31 : public HardenedContextTest
{};
+class HardenedContextBlendTest : public HardenedContextTest
+{};
+
// Context creation would fail if EGL_ANGLE_create_context_webgl_compatibility was not available so
// the GL extension should always be present
TEST_P(WebGLCompatibilityTest, ExtensionStringExposed)
@@ -9058,6 +9061,107 @@
}
}
+// Test alpha blend where both the framebuffer and shader miss the alpha channel. The spec says
+// that:
+//
+// > If a color buffer has no A value, then A_d is taken to be 1.
+//
+// But it says nothing about what happens if the shader does not write to alpha and A_s.
+TEST_P(HardenedContextBlendTest, AlphaBlendNoAlphaChannelInSrcAndDst)
+{
+ GLTexture color;
+ glBindTexture(GL_TEXTURE_2D, color);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, 1, 1);
+ constexpr uint8_t kInitialValue = 0x10;
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RED, GL_UNSIGNED_BYTE, &kInitialValue);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
+ ANGLE_SKIP_TEST_IF(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE);
+
+ constexpr char kFS[] = R"(#version 300 es
+out mediump float color;
+void main() {
+ color = 0.2;
+})";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ // A_d is implicitly one. But A_s is undefined.
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);
+ glBlendEquation(GL_FUNC_ADD);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.0f);
+ // Normally, the result cannot be known given A_s is undefined, but in hardened contexts it's
+ // set to 0.
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0x10, 0, 0, 255), 1);
+ ASSERT_GL_NO_ERROR();
+}
+
+// Similar test as AlphaBlendNoAlphaChannelInSrcAndDst, but with a vec2
+TEST_P(HardenedContextBlendTest, AlphaBlendNoAlphaChannelInSrcAndDst2)
+{
+ GLTexture color;
+ glBindTexture(GL_TEXTURE_2D, color);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, 1, 1);
+ constexpr std::array<uint8_t, 2> kInitialValue = {0x10, 0x30};
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RG, GL_UNSIGNED_BYTE, kInitialValue.data());
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
+ ANGLE_SKIP_TEST_IF(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE);
+
+ constexpr char kFS[] = R"(#version 300 es
+out mediump vec2 color;
+void main() {
+ color = vec2(0.2, 0.4);
+})";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ // A_d is implicitly one. But A_s is undefined.
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);
+ glBlendEquation(GL_FUNC_ADD);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.0f);
+ // Normally, the result cannot be known given A_s is undefined, but in hardened contexts it's
+ // set to 0.
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0x10, 0x30, 0, 255), 1);
+ ASSERT_GL_NO_ERROR();
+}
+
+// Similar test as AlphaBlendNoAlphaChannelInSrcAndDst, but with an array
+TEST_P(HardenedContextBlendTest, AlphaBlendNoAlphaChannelInSrcAndDstArray)
+{
+ GLTexture color;
+ glBindTexture(GL_TEXTURE_2D, color);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, 1, 1);
+ constexpr uint8_t kInitialValue = 0x10;
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RED, GL_UNSIGNED_BYTE, &kInitialValue);
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0);
+ ANGLE_SKIP_TEST_IF(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE);
+
+ constexpr char kFS[] = R"(#version 300 es
+out mediump float color[1];
+void main() {
+ color[0] = 0.2;
+})";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ // A_d is implicitly one. But A_s is undefined.
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);
+ glBlendEquation(GL_FUNC_ADD);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.0f);
+ // Normally, the result cannot be known given A_s is undefined, but in hardened contexts it's
+ // set to 0.
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0x10, 0, 0, 255), 1);
+ ASSERT_GL_NO_ERROR();
+}
+
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3(WebGLCompatibilityTest);
ANGLE_INSTANTIATE_TEST_ES2(WebGL1CompatibilityTest);
@@ -9068,6 +9172,9 @@
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(HardenedContextTest);
ANGLE_INSTANTIATE_TEST_ES3(HardenedContextTest);
+GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(HardenedContextBlendTest);
+ANGLE_INSTANTIATE_TEST_ES3(HardenedContextBlendTest);
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(HardenedContextTestRestrictBaseLevel);
ANGLE_INSTANTIATE_TEST(
HardenedContextTestRestrictBaseLevel,
Original Bug Report
Potential GPU data leak via sub-vec4 fragment outputs in ANGLE D3D11
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: ANGLE’s D3D11 backend (Chrome’s default WebGL stack on Windows) generates pixel shaders whose SV_TARGET outputs keep the GLSL variable’s original width (e.g. float2 for out vec2), while the blend state’s render-target write mask is derived solely from the attachment format and glColorMask, enabling all channels. Per the D3D11.3 Functional Spec §17.15, the Output Merger then writes undefined values into the enabled-but-unsupplied channels, and web content reads those bytes back via readPixels. Runtime testing on stock Chromium confirmed the undefined-write path executes end-to-end on the default Windows configuration; whether the undefined values carry stale GPU data is driver-family-dependent.
Affected files:
third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp:316-325— PS output struct member declared at original GL width; copy assigns only the declared componentsthird_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp:1380,1411— GL type captured verbatim at link time (primary and EXT_blend_func_extended secondary outputs)third_party/angle/src/libANGLE/renderer/d3d/d3d11/RenderStateCache.cpp:86-87,181—RenderTargetWriteMaskfrom attachment format +glColorMaskonly; shader output width never consultedthird_party/angle/src/libANGLE/validationES.cpp:445-471— draw validation checks output presence and base-type class only, never component count
Estimated timestamp from git blame: 2014-06-23
Summary
A WebGL2 fragment shader with a sub-vec4 output (out float/vec2/vec3, integer variants, or an EXT_blend_func_extended secondary output) drawn into a 4-channel attachment (RGBA8, or RGBA32F/RGBA32UI via EXT_color_buffer_float) produces:
- a compiled D3D11 pixel shader whose output signature declares only the GL-width components (
o0.xyforout vec2), and - a blend state with all four channels write-enabled.
D3D11.3 Functional Spec §17.15 (Output Write Masks): failing to supply data for all write-enabled components “results in undefined values being written out.” §16.9.1 names write-mask narrowing as the sanctioned remedy; ANGLE’s D3D path implements neither narrowing nor output widening. The draw is legal under OpenGL ES 3.2 §14.2.3 and WebGL 2.0 §Draw Buffers (output incompatibility is defined by base type only), so no validation rejects it.
Both sibling backends were already patched for this exact class:
- Metal zero-fills widened outputs “to avoid indeterminate GPU register residue leaking into color attachments” (
third_party/angle/src/compiler/translator/msl/ModifyStruct.cpp:885-896). - GL runs
ExpandFragmentOutputsToVec4(compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.cpp) — but only under a FeaturesGL driver workaround gated to AMD+Mesa (libANGLE/renderer/gl/renderergl_utils.cpp:2725-2726, crbug.com/498828605). - No equivalent exists for D3D11: TranslatorHLSL does not invoke the pass, and there is no FeaturesD3D counterpart.
Executed evidence (stock Chromium 156 release build, default ANGLE-on-D3D11, on both NVIDIA L4 hardware and D3D11-on-WARP): the runtime-translated HLSL confirms the narrow float2 out_c : SV_TARGET0 signature, and the Output Merger overwrote the never-written .ba channels of every covered pixel — sentinel clear values were destroyed across all ~10.5M sampled channel values per run, and JavaScript read the resulting bytes back. On these two driver families the undefined lanes deterministically pad with zeros, so no stale data was disclosed there. The demonstrated-leaky families from the sibling bugs (AMD per crbug.com/498828605; Apple GPUs per the Metal fix) were not available for testing, so actual residue disclosure on Windows remains driver-dependent and undemonstrated.
Root Cause Analysis
-
Output declared at GL width:
third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp:316-325—GeneratePixelShaderForOutputSignaturedeclares eachPS_OUTPUTmember with the GLSL type recorded at link time (:1380,:1411), and copies only those components:// DynamicHLSL.cpp:316-324 if (outputVariable) { declarationStream << " "; HLSLTypeString(declarationStream, outputVariable->type); // <-- float2 for `out vec2` declarationStream << " " << outputVariable->name << " : " << targetSemantic << static_cast<int>(layoutIndex) << ";\n"; copyStream << " output." << outputVariable->name << " = " << outputVariable->source << ";\n"; // <-- 2-component copyFXC compiles this to a shader exporting
o0.xyonly. The translator’s zero-init (compiler/translator/hlsl/OutputHLSL.cpp:693-702,static float2 out_c = {0, 0};) covers only the declared width. The ES-spec comment at:313-315addresses the distinct whole-output-missing case, which is safe per §16.9.1 (“not modified”); the partial-width case is not. -
Write mask derived from format only:
renderer/d3d/d3d11/RenderStateCache.cpp:86-87sets the mask togl_d3d11::GetColorMask(internalFormat) & colorMask(RGBA for RGBA-format attachments with defaultglColorMask), and:181copies it intoRenderTargetWriteMask. The shader’s declared output width is never an input to the blend key. -
No validation rejects the mismatch:
libANGLE/validationES.cpp:445-471(invoked at:4211-4225) checks output presence per draw buffer and base-type class only.out vec2→ RGBA8 is legal per ES 3.2 §14.2.3 (“the result is undefined”, no error) and WebGL 2.0 §Draw Buffers. -
Undefined OM write, web-readable: per D3D11.3 §17.15 the Output Merger writes undefined values into the write-enabled
.bachannels;readPixels(or canvas readback/compositing) returns them to JavaScript. Confirmed executing on stock Chromium on Windows: cleared sentinels in.baare overwritten on every covered pixel. -
No mitigation on the D3D path:
ExpandFragmentOutputsToVec4is referenced only by the GLSL/ESSL translators under the AMD+Mesa-gated feature; nothing undercompiler/translator/hlsl/or the D3D renderer widens outputs or narrows the mask.
Reproduction
- On Windows Chrome (default backend, no flags), create a WebGL2 context.
- Link a program whose fragment shader declares
out vec2 c;and writesc = vec2(0.25, 0.5);, with a fullscreen-triangle vertex shader. - Attach an RGBA8 texture to an FBO (or RGBA32F/RGBA32UI via EXT_color_buffer_float for 32-bit readback fidelity); clear to a known sentinel color.
drawArrayscovering the viewport, thenreadPixelsand inspect the.b/.achannels of each pixel.- Expected:
.baretain the cleared sentinel, or the zero default WebGL 2.0 specifies for unwritten outputs.
Observed: .ba are overwritten by the Output Merger on every covered pixel — the §17.15 undefined-write configuration is active and web-observable on the default Windows stack (verified on NVIDIA L4 hardware D3D11 and on WARP; runtime-translated HLSL from the production binary shows the 2-component SV_TARGET0 declaration). On those two driver families the undefined values materialize as constant zeros; disclosure of stale GPU register residue was not observed there and would require driver families that export stale register lanes, as AMD and Apple GPUs demonstrably did in the sibling GL/Metal bugs.
Severity
medium — Web-triggerable on the default Windows configuration with a trivial shader, no user interaction, no compromised renderer: the page reads back bytes in channels it never wrote, violating WebGL 1.0’s initialized-data guarantee (§Resource Restrictions) and WebGL 2.0’s zero-default intent for unwritten outputs. It is not memory corruption, and the affected bytes are GPU register/export residue rather than GPU-process address-space memory; the two tested driver families pad with benign zeros, so actual disclosure is contingent on driver families (e.g. AMD) where the sibling bugs demonstrated real residue. This matches Chrome’s rating of the identical class in the June 2026 ANGLE batch (Medium — “Uninitialized Use in ANGLE … leak cross-origin data”: CVE-2026-11109, -11110, -11087, -11090, -11138). If cross-context residue is demonstrated on a major Windows driver family, this escalates to high, since the GPU hardware is shared by all origins’ work in the GPU process.
Suggested Fix Direction
Mirror the sibling fixes: declare PS_OUTPUT color members at 4 components and zero-fill the lanes the GLSL shader does not produce in GeneratePixelShaderForOutputSignature (a change of this shape was verified locally to compile and render correctly across float/int formats with no regression), or narrow RenderTargetWriteMask to the shader’s declared components (the §16.9.1-sanctioned remedy), or run ExpandFragmentOutputsToVec4 before HLSL translation.
Unreproduced POC: ANGLE D3D11: GeneratePixelShaderForOutputSignature declares sub-vec4 SV_TARGET outputs at original GL width; D3D11 Output Merger writes spec-‘undefined’ values into the unwritten RTV channels, readable by web content via WebGL2 readPixels (Windows default backend)
Strategy: web-content Bug Type: uninitialized/undefined GPU data written into web-readable render-target channels (CWE-908 class)
Status summary — what reproduced and what did not:
- Reproduced end-to-end (unmodified web content, stock Chromium on Windows,
default ANGLE-on-D3D11 stack, on BOTH available device families —
hardware NVIDIA L4 and D3D11-on-WARP): a WebGL2 fragment shader with a
sub-vec4 output (
out vec2 c;) draws into an RGBA target whose blend-state write mask enables all 4 channels while the pixel shader supplies only 2. The Output Merger overwrites the.b/.achannels the shader never wrote — a sentinel clear value is destroyed in all 10,518,528 channel-samples per run (preserved: 0), and the resulting bytes are read back by JavaScript viareadPixels. The runtime-captured translated HLSL from the production binary (viaWEBGL_debug_shaders) shows the narrow pipeline:static float2 out_c = {0, 0};+ the@@ PIXEL OUTPUT @@stub thatDynamicHLSL::GeneratePixelShaderForOutputSignaturefills at draw time withfloat2 out_c : SV_TARGET0. - Not reproduced — the information-disclosure payoff: on the two device families available in this lab (NVIDIA L4, driver 596.86; Microsoft WARP, 10.0.20348) the D3D-spec-undefined values deterministically materialize as zeros. No non-constant residue and none of the seeded 32-bit signature constants (same-context seeds or cross-origin victim seeds) ever surfaced in the unwritten channels — confirmed independently at the raw D3D11 level by a native probe that replays ANGLE’s exact generated HLSL shape with aggressive prior-draw register seeding. These drivers pad the missing output lanes. The leak therefore remains confined to driver families that export stale register lanes instead of padding — precisely the pattern of the sibling bugs (GL backend garbage on AMD hardware, crbug.com/498828605, and the Metal backend’s “indeterminate GPU register residue” fix). No AMD/Intel Windows GPU was available to test the leak endpoint.
The vulnerable configuration (2-component PS output signature vs 4-channel write mask) is fully demonstrated on the default Windows stack; whether the resulting undefined write discloses stale data is a per-driver property that could not be realized on the available hardware.
Files
| file | role |
|---|---|
poc.html |
attacker page: sub-vec4-output draws (RGBA8/RGBA32F/RGBA32UI × out vec2/out vec3/out uvec2), .ba readback classification, seeded-residue tests, translated-HLSL capture |
victim.html |
cross-origin iframe (different port) continuously rendering signature constants 0xFEEDFACE 0xF0CACC1A 0xB0BACAFE 0xABAD1DEA through its own WebGL2 context |
server.py |
loopback server providing the two origins (:8077 attacker, :8078 victim) and collecting the page’s JSON report |
run_poc.ps1 |
Windows driver: server + stock headful Chrome + wait for report + desktop screenshot |
run-on-windows.md |
exact reproduction steps / payload notes |
d3d_probe.cpp |
native D3D11 probe replaying ANGLE’s exact generated HLSL shape (narrow float2 SV_TARGET0 PS, default full write mask) with prior-draw register seeding, on every adapter + WARP |
results-d3d11-nvidia-l4.json |
Chrome run on hardware D3D11, ANGLE (NVIDIA, NVIDIA L4 ... D3D11) — includes translated_fs_vec2 |
results-d3d11-warp.json |
Chrome run on ANGLE (Microsoft, Microsoft Basic Render Driver ... D3D11) (D3D11-on-WARP) |
screenshot-d3d11-nvidia-l4.png, screenshot-d3d11-warp.png |
desktop screenshots of the runs |
translated-hlsl-out-vec2.txt |
runtime-captured translated HLSL for out vec2 (narrow zero-init + output stub) |
probe-output-nvidia-l4-and-warp.txt |
full native-probe output on NVIDIA L4 and WARP |
bandaid-fix.patch |
minimal fix in DynamicHLSL.cpp: declare every PS color output at 4 components and zero-fill the missing lanes (mirrors the Metal/GL sibling fixes) |
results-d3d11-warp-fixed-dll.json, fix-verification-log.txt |
fix verification: md5-verified fixed gl_init.dll swapped in, POC re-run — no regression, .ba now shader-written zeros |
Prerequisites
- Windows (Server 2022 / Windows 10+). Default GPU stack — no GPU flags:
stock Chrome uses ANGLE-on-D3D11 (hardware adapter, or D3D11-on-WARP on
GPU-less machines; both drive the vulnerable
GeneratePixelShaderForOutputSignaturepath). - Any Chrome/Chromium build (evidence produced with a Chromium 156 release
build, component build,
out/win). - Python 3 (stdlib) for
server.py.
Reproduction
python server.py
chrome.exe --no-first-run --no-default-browser-check --no-proxy-server ^
--user-data-dir=%TEMP%\poc-profile http://127.0.0.1:8077/poc.html
The page runs its battery (~10-40 s), shows a verdict line, and POSTs the
JSON report to the server (saved as results.json). Automated variant:
powershell -ExecutionPolicy Bypass -File run_poc.ps1 (see
run-on-windows.md).
Native probe (optional corroboration): build with
clang-cl /c /O2 d3d_probe.cpp + lld-link d3d_probe.obj d3d11.lib dxgi.lib d3dcompiler.lib /subsystem:console and run d3d_probe.exe on the Windows
machine.
Expected Result
For every test the report classifies each .b/.a channel value (the
fragment shader wrote only .rg/.rgb):
SENTINEL_*— clear value survived → driver narrowed the write (safe on that device).ZERO/ONE/MAX— the Output Merger overwrote channels the shader never supplied (the D3D11.3 §17.15 undefined-write configuration is active) with a constant pad on that device. This is what both available devices produce.OTHER/SEED_HIT/VICTIM_HIT— non-constant residue / seeded constants from a previous draw / from the cross-origin victim context = actual information disclosure. Expected on driver families that export stale register lanes (per the AMD GL-backend and Apple Metal siblings); not observed on NVIDIA L4 or WARP, which pad with zeros.
Output
Chrome on hardware D3D11 (NVIDIA L4), unfixed build — results-d3d11-nvidia-l4.json:
renderer: ANGLE (NVIDIA, NVIDIA L4 (0x000027B8) Direct3D11 vs_5_0 ps_5_0, D3D11)
verdict : UNDEFINED-WRITE: OM overwrote unwritten channels (constant pad on this device)
summary : {"residue":0,"seedHits":0,"victimHits":0,"undefWrites":10518528,"preserved":0}
T1_rgba8_outvec2 px0=[0x7f 0xbf 0x00 0x00] b:{ZERO:65536} a:{ZERO:65536} (cleared to 64)
T3_rgba32ui_outuvec2 px0=[0x11111111 0x22222222 0 0] b/a:{ZERO:65536} (cleared to 0xAAAAAAAA)
...
Every .b/.a sample had been cleared to a sentinel (64 / 0x3E800000 /
0xAAAAAAAA) and came back overwritten after a draw whose pixel shader
provably declares only 2 output components — the translated HLSL captured at
runtime from this exact binary (translated-hlsl-out-vec2.txt):
static float2 out_c = {0, 0};
...
@@ PIXEL OUTPUT @@ // patched at draw time by DynamicHLSL with:
// struct PS_OUTPUT { float2 out_c : SV_TARGET0; };
PS_OUTPUT main(...){ (out_c = float2(0.5, 0.75)); return generateOutput(); }
Native probe on the same hardware (probe-output-nvidia-l4-and-warp.txt),
exact ANGLE HLSL shape + full write mask + seeded prior draws:
=== device: NVIDIA L4 ===
format R32G32B32A32_UINT + uint2 PS
E2 seed x8 then narrow .b: sent=0 zero=65536 ... SEED=0 other=0
E4 8 rounds seed/narrow .b: sent=0 zero=524288 ... SEED=0 other=0 (sentinel 0xAAAAAAAA destroyed,
pad is always 0x00000000)
Bandaid fix
bandaid-fix.patch (third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp,
GeneratePixelShaderForOutputSignature): declare every color output member
of PS_OUTPUT at full 4-component width and zero-fill the lanes the GLSL
shader does not produce (output.out_c = float4(out_c, 0, 0);), mirroring
the Metal backend’s zero-fill and the GL backend’s
ExpandFragmentOutputsToVec4 pass. With the write mask fully covered by
shader-supplied values, D3D11.3 §17.15’s undefined-write clause no longer
applies on any driver.
Verified (fix-verification-log.txt): rebuilt gl_init.dll (the component
DLL containing ANGLE’s D3D11 backend), md5-verified the swap inside the job,
re-ran the POC — page fully functional, all written channels correct, no
shader compile/link errors across float/uint formats, .ba now
deterministically shader-written zeros.
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.
- http://127.0.0.1:8077/poc.html
- https://crbug.com/498828605
- https://source.chromium.org/chromium/chromium/src/+/main:compiler/translator/hlsl/OutputHLSL.cpp;l=693
- https://source.chromium.org/chromium/chromium/src/+/main:compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.cpp
- https://source.chromium.org/chromium/chromium/src/+/main:libANGLE/renderer/gl/renderergl_utils.cpp;l=2725
- https://source.chromium.org/chromium/chromium/src/+/main:libANGLE/validationES.cpp;l=445
- https://source.chromium.org/chromium/chromium/src/+/main:renderer/d3d/d3d11/RenderStateCache.cpp;l=86
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/compiler/translator/msl/ModifyStruct.cpp;l=885
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp;l=1380
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/DynamicHLSL.cpp;l=316
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/renderer/d3d/d3d11/RenderStateCache.cpp;l=86
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/angle/src/libANGLE/validationES.cpp;l=445