CVE-2026-11051
Overview
Files Changed
include/GLSLANG/ShaderLang.hinclude/platform/autogen/FeaturesGL_autogen.hinclude/platform/gl_features.jsonsrc/compiler.gnisrc/compiler/translator/Compiler.cppsrc/compiler/translator/glsl/TranslatorESSL.cppsrc/compiler/translator/glsl/TranslatorGLSL.cppsrc/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
Patch
From c1db297b2554ec7939f0234a3d53446f6a3c2ba7 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Thu, 30 Apr 2026 14:21:51 -0400
Subject: [PATCH] GL: Expand non-vec4 fragment outputs to vec4
As a workaround for driver bugs.
Bug: chromium:498828605
Change-Id: I88e2b33c807f92e8aa512bb99353f885b86d37c0
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7806928
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Reviewed-by: Kenneth Russell <kbr@chromium.org>
---
diff --git a/include/GLSLANG/ShaderLang.h b/include/GLSLANG/ShaderLang.h
index 43887b1..d7a9349 100644
--- a/include/GLSLANG/ShaderLang.h
+++ b/include/GLSLANG/ShaderLang.h
@@ -26,7 +26,7 @@
// Version number for shader translation API.
// It is incremented every time the API changes.
-#define ANGLE_SH_VERSION 408
+#define ANGLE_SH_VERSION 409
enum ShShaderSpec
{
@@ -480,6 +480,9 @@
// Whether the ANGLE IR should be used. Ineffective if ANGLE is built without IR support.
uint64_t useIR : 1;
+ // Whether ESSL300 fragment outputs should be expanded to vec4s.
+ uint64_t expandFragmentOutputsToVec4 : 1;
+
ShCompileOptionsMetal metal;
ShPixelLocalStorageOptions pls;
};
diff --git a/include/platform/autogen/FeaturesGL_autogen.h b/include/platform/autogen/FeaturesGL_autogen.h
index bac58e9..d5e9061 100644
--- a/include/platform/autogen/FeaturesGL_autogen.h
+++ b/include/platform/autogen/FeaturesGL_autogen.h
@@ -674,6 +674,12 @@
&members,
};
+ FeatureInfo expandFragmentOutputsToVec4 = {
+ "expandFragmentOutputsToVec4",
+ FeatureCategory::OpenGLWorkarounds,
+ &members,
+ };
+
FeatureInfo recreateMipmapLevelsBeforeGenerate = {
"recreateMipmapLevelsBeforeGenerate",
FeatureCategory::OpenGLWorkarounds,
diff --git a/include/platform/gl_features.json b/include/platform/gl_features.json
index 41f6567..6c1a696 100644
--- a/include/platform/gl_features.json
+++ b/include/platform/gl_features.json
@@ -878,6 +878,14 @@
"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.gni b/src/compiler.gni
index 29c9c8e..9fad188 100644
--- a/src/compiler.gni
+++ b/src/compiler.gni
@@ -193,6 +193,7 @@
"src/compiler/translator/tree_ops/SimplifyLoopConditions.h",
"src/compiler/translator/tree_ops/SplitSequenceOperator.cpp",
"src/compiler/translator/tree_ops/SplitSequenceOperator.h",
+ "src/compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.h",
"src/compiler/translator/tree_ops/glsl/RegenerateStructNames.h",
"src/compiler/translator/tree_ops/glsl/RewriteRepeatedAssignToSwizzled.h",
"src/compiler/translator/tree_ops/glsl/UseInterfaceBlockFields.h",
@@ -260,6 +261,7 @@
"src/compiler/translator/glsl/ExtensionGLSL.h",
"src/compiler/translator/glsl/TranslatorGLSL.cpp",
"src/compiler/translator/glsl/VersionGLSL.cpp",
+ "src/compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.cpp",
"src/compiler/translator/tree_ops/glsl/RegenerateStructNames.cpp",
"src/compiler/translator/tree_ops/glsl/RewriteRepeatedAssignToSwizzled.cpp",
"src/compiler/translator/tree_ops/glsl/UseInterfaceBlockFields.cpp",
diff --git a/src/compiler/translator/Compiler.cpp b/src/compiler/translator/Compiler.cpp
index 28e6869..ca1c249 100644
--- a/src/compiler/translator/Compiler.cpp
+++ b/src/compiler/translator/Compiler.cpp
@@ -55,6 +55,7 @@
#include "compiler/translator/tree_ops/msl/EnsureLoopForwardProgress.h"
#include "compiler/translator/tree_util/FindSymbolNode.h"
#include "compiler/translator/tree_util/IntermNodePatternMatcher.h"
+#include "compiler/translator/tree_util/IntermNode_util.h"
#include "compiler/translator/tree_util/ReplaceShadowingVariables.h"
#include "compiler/translator/tree_util/ReplaceVariable.h"
#include "compiler/translator/util.h"
@@ -1062,6 +1063,9 @@
}
mValidateASTOptions.validateMultiDeclarations = true;
+ // Move declarations before functions to simplify transformations.
+ MoveDeclarationsBeforeFunctions(root);
+
if (!SplitSequenceOperator(this, root, IntermNodePatternMatcher::kArrayLengthMethod,
&getSymbolTable()))
{
@@ -1258,6 +1262,7 @@
{
compileOptions.clampFragDepth = false;
compileOptions.retainInactiveFragmentOutputs = false;
+ compileOptions.expandFragmentOutputsToVec4 = false;
}
#if !defined(ANGLE_IR)
diff --git a/src/compiler/translator/glsl/TranslatorESSL.cpp b/src/compiler/translator/glsl/TranslatorESSL.cpp
index b059caf..7ad85d0 100644
--- a/src/compiler/translator/glsl/TranslatorESSL.cpp
+++ b/src/compiler/translator/glsl/TranslatorESSL.cpp
@@ -17,6 +17,7 @@
#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"
@@ -122,7 +123,14 @@
if (compileOptions.removeDynamicIndexingOfSwizzledVector)
{
- if (!sh::RemoveDynamicIndexingOfSwizzledVector(this, root, &getSymbolTable(), nullptr))
+ if (!RemoveDynamicIndexingOfSwizzledVector(this, root, &getSymbolTable(), nullptr))
+ {
+ return false;
+ }
+ }
+ if (compileOptions.expandFragmentOutputsToVec4)
+ {
+ if (!ExpandFragmentOutputsToVec4(this, root, &getSymbolTable()))
{
return false;
}
diff --git a/src/compiler/translator/glsl/TranslatorGLSL.cpp b/src/compiler/translator/glsl/TranslatorGLSL.cpp
index f63098a..6ee3f56 100644
--- a/src/compiler/translator/glsl/TranslatorGLSL.cpp
+++ b/src/compiler/translator/glsl/TranslatorGLSL.cpp
@@ -18,6 +18,7 @@
#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
@@ -137,6 +138,13 @@
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/tree_ops/MonomorphizeUnsupportedFunctions.cpp b/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
index 8a82f64..11db4e5 100644
--- a/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
+++ b/src/compiler/translator/tree_ops/MonomorphizeUnsupportedFunctions.cpp
@@ -509,34 +509,6 @@
const FunctionMap &mFunctionMap;
};
-void SortDeclarations(TIntermBlock *root)
-{
- TIntermSequence *original = root->getSequence();
-
- TIntermSequence replacement;
- TIntermSequence functionDefs;
-
- // Accumulate non-function-definition declarations in |replacement| and function definitions in
- // |functionDefs|.
- for (TIntermNode *node : *original)
- {
- if (node->getAsFunctionDefinition() || node->getAsFunctionPrototypeNode())
- {
- functionDefs.push_back(node);
Regression Test / PoC
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index 3f1d59f..eed6360 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -23241,6 +23241,116 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
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)
+{
+ 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);
+ // The result cannot be known given A_s is undefined, however it must be at least 0x10 given the
+ // additive blend and that A_d must act as 1.
+ // For future reference, A_s has been observed to be 0, 1 and 0.2 with various drivers (0.2
+ // being the value of the component that is present).
+ GLColor value;
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &value);
+ EXPECT_GE(value.R, 0x10);
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test alpha blend where the framebuffer misses the alpha channel, but the shader writes to alpha.
+TEST_P(GLSLTest_ES3_Blend, AlphaBlendNoAlphaChannelInDst)
+{
+ 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
+// Use an array for extra testing
+out mediump vec4 color[1];
+void main() {
+ color[0] = vec4(0.2, 0, 0, 0.5);
+})";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_DST_ALPHA);
+ glBlendEquation(GL_FUNC_ADD);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.0f);
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0x10 + 255 / 10, 0, 0, 255), 1);
+ ASSERT_GL_NO_ERROR();
+}
+
+// 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)
+{
+ GLTexture color;
+ glBindTexture(GL_TEXTURE_2D, color);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, 1, 1);
+ constexpr uint8_t kInitialValue = 0xC0;
+ 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
+// Declare the output after main for extra testing
+void f();
+void main() {
+ f();
+}
+out mediump float color;
+void f() {
+ color = 0.2;
+}
+)";
+
+ ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_DST_COLOR, GL_ZERO);
+ glBlendEquation(GL_FUNC_ADD);
+ drawQuad(program, essl3_shaders::PositionAttrib(), 0.0f);
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(0xC0 / 5, 0, 0, 255), 1);
+ ASSERT_GL_NO_ERROR();
+}
} // anonymous namespace
ANGLE_INSTANTIATE_TEST_ES2_AND_ES3_AND_ES31_AND_ES32(
@@ -23312,3 +23422,8 @@
ES3_OPENGLES(),
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));
Original Bug Report
Heap buffer over-read in Mesa RadeonSI noop-blend analysis for scalar fragment outputs leads to GPU process memory disclosure
Heap buffer over-read in Mesa RadeonSI noop-blend analysis for scalar fragment outputs leads to GPU process memory disclosure
Summary
A heap buffer over-read exists in Mesa’s RadeonSI driver during the noop-blend shader analysis, reachable from Chrome’s GPU process via WebGL2 on Linux. When a fragment shader declares a scalar output (e.g. out float) and the blend state is DST_COLOR * src + ZERO * dst, the driver’s get_output_as_const_value function unconditionally reads four nir_const_value elements from a nir_load_const_instr whose flexible array was allocated with only one element. This results in a 24-byte over-read on every first draw call with that shader and blend combination. The bug affects Linux systems running Mesa OpenGL drivers with RadeonSI (AMD GPUs).
Platform: Linux (Mesa OpenGL drivers, tested with radeonsi / AMD Radeon RX 6600 XT).
Root Cause
RadeonSI implements a draw-time optimization for the blend mode DST_COLOR * src + ZERO * dst. When this blend state is active, the driver analyzes the fragment shader to determine whether it always outputs vec4(1.0) when fed an all-ones texture, which would make the blend a no-op. The analysis function si_check_blend_dst_sampler_noop clones the shader, replaces the sole texture fetch with a constant vec4(1.0), runs constant folding, and then reads the resulting output value through get_output_as_const_value.
The problem is in get_output_as_const_value, which reads the constant output by hardcoding a 4-component access:
// src/gallium/drivers/radeonsi/si_nir_optim.c
nir_const_value *c = nir_src_as_const_value(intrin->src[0]);
if (!c)
return false;
if (intrin->src[0].ssa->bit_size == 16) {
uint16_t half_values[4];
nir_const_value_to_array(half_values, c, 4, u16);
for (unsigned i = 0; i < 4; i++)
values[i] = _mesa_half_to_float(half_values[i]);
} else {
nir_const_value_to_array(values, c, 4, f32);
}
The macro nir_const_value_to_array expands to a simple loop that accesses c[0] through c[3]:
// src/compiler/nir/nir.h
#define nir_const_value_to_array(arr, c, components, m) \
do { for (unsigned i = 0; i < components; ++i) arr[i] = c[i].m; } while (false)
The pointer c comes from nir_src_as_const_value, which returns a pointer to the value[] flexible array member of nir_load_const_instr. This array is allocated with exactly num_components entries:
// src/compiler/nir/nir.c
nir_load_const_instr *
nir_load_const_instr_create(nir_shader *shader, unsigned num_components,
unsigned bit_size)
{
nir_load_const_instr *instr =
nir_instr_create(shader, nir_instr_type_load_const,
sizeof(nir_load_const_instr) + sizeof(nir_const_value) * num_components);
WebGL2 permits fragment shaders with scalar, vec2, or vec3 outputs such as layout(location=0) out float o. ANGLE’s desktop GL backend preserves the declared type verbatim in the translated GLSL; it does not pad outputs to vec4. When such a shader’s texture fetch is replaced with vec4(1.0) and constant-folded, the swizzle operation .r collapses the result to a single-component nir_load_const_instr with num_components = 1, meaning value[] has space for exactly one 8-byte nir_const_value. The subsequent call to nir_const_value_to_array(values, c, 4, f32) then reads three elements past the end of the allocation, a 24-byte over-read.
No ANGLE workaround covers this path; the existing Mesa-related workarounds (recreateMipmapLevelsBeforeGenerate, disableRenderSnorm, etc.) are unrelated.
Reproduce
Tested at Chromium commit ab3f3f8b586d6 on Linux x86_64 with an AMD Radeon RX 6600 XT (radeonsi driver).
The bug is confirmed present in Mesa 26.0.4 (latest stable as of 2026-04-02). The over-read object is a nir_load_const_instr allocated through Mesa’s NIR GC slab allocator (gc_alloc). This allocator obtains 32 KB slabs from malloc and carves individual objects from within; ASAN only sees the outer slab boundary, not the boundaries between slab-internal objects, so the 24-byte over-read into adjacent slab data is invisible. To make it visible to ASAN, the slab is disabled by setting the compile-time constant MAX_FREELIST_SIZE to 0 in src/util/ralloc.c, which forces every gc_alloc to fall through to ralloc_size and thus to an individual malloc with its own ASAN redzone. This change affects only the allocation strategy; no application logic is modified, and the over-read occurs identically with or without the slab.
0. Prerequisites
sudo apt-get build-dep mesa
pip install mako
1. Build libdrm (>= 2.4.121 required by Mesa 26.x)
git clone https://gitlab.freedesktop.org/mesa/drm.git /home/user/libdrm-new
cd /home/user/libdrm-new
git checkout libdrm-2.4.131
CC=~/chromium/src/third_party/llvm-build/Release+Asserts/bin/clang \
CXX=~/chromium/src/third_party/llvm-build/Release+Asserts/bin/clang++ \
meson setup build \
--prefix=$PWD/install \
-Dbuildtype=debug \
-Db_sanitize=address \
-Db_lundef=false
ninja -C build -j$(nproc) && ninja -C build install
2. Build Mesa with ASAN (slab allocator disabled)
Both Mesa and libdrm must be compiled with Chromium’s Clang so the ASAN instrumentation resolves against the runtime statically linked into Chrome’s ASAN binary. Using GCC’s libasan.so will conflict at load time. The -Db_lundef=false flag is required because the shared libraries leave __asan_* symbols unresolved; they are provided by Chrome at runtime.
Mesa must be built as a release build (-Dbuildtype=release -Db_ndebug=true). Debug builds contain asserts in NIR that may abort before the overflow occurs.
Before building, apply the slab-disable patch to make the over-read visible to ASAN:
git clone https://gitlab.freedesktop.org/mesa/mesa.git /home/user/mesa-asan
cd /home/user/mesa-asan
git checkout mesa-26.0.4
# Disable the NIR GC slab allocator so each gc_alloc goes through malloc
sed -i 's/#define MAX_FREELIST_SIZE 512/#define MAX_FREELIST_SIZE 0/' src/util/ralloc.c
Then build:
PKG_CONFIG_PATH=/home/user/libdrm-new/install/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH \
CMAKE_PREFIX_PATH=/usr/lib/llvm-18 \
CC=~/chromium/src/third_party/llvm-build/Release+Asserts/bin/clang \
CXX=~/chromium/src/third_party/llvm-build/Release+Asserts/bin/clang++ \
meson setup build \
--prefix=$PWD/install \
-Dbuildtype=release \
-Db_sanitize=address \
-Db_lundef=false \
-Db_ndebug=true \
-Dglx=dri \
-Degl=enabled \
-Dplatforms=x11 \
-Dllvm=enabled \
-Dcpp_std=c++17 \
-Dgallium-rusticl=false \
-Dgallium-va=disabled \
-Dcmake_prefix_path=/usr/lib/llvm-18
ninja -C build -j$(nproc) && ninja -C build install
3. Build Chrome with ASAN
cd ~/chromium/src
cat > out/asan-release/args.gn << 'EOF'
is_asan = true
is_debug = false
is_component_build = true
dcheck_always_on = false
EOF
gn gen out/asan-release
autoninja -C out/asan-release chrome
4. Run
MESA_SHADER_CACHE_DISABLE=true \
LD_LIBRARY_PATH=/home/user/mesa-asan/install/lib/x86_64-linux-gnu:/home/user/libdrm-new/install/lib/x86_64-linux-gnu:out/asan-release \
LIBGL_DRIVERS_PATH=/home/user/mesa-asan/install/lib/x86_64-linux-gnu/dri \
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:detect_odr_violation=0:redzone=128" \
out/asan-release/chrome \
--no-sandbox \
--disable-gpu-sandbox \
--user-data-dir=/tmp/poc-$(date +%s) \
"file:///path/to/poc.html"
The GPU process crashes within seconds of the page loading. ASAN reports:
==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c4d3925a400
READ of size 4 at 0x7c4d3925a400 thread T44
SCARINESS: 24 (4-byte-read-heap-buffer-overflow)
#0 in get_output_as_const_value si_nir_optim.c
#1 in si_nir_is_output_const_if_tex_is_const si_nir_optim.c
#2 in si_check_blend_dst_sampler_noop si_state.c
#3 in si_draw_blend_dst_sampler_noop si_state.c
#4 in tc_call_draw_single u_threaded_context.c
...
0x7c4d3925a400 is located 0 bytes after 128-byte region [0x7c4d3925a380,0x7c4d3925a400)
allocated by thread T44 here:
#0 in malloc
#1 in ralloc_size ralloc.c
#2 in gc_alloc_size ralloc.c
#3 in nir_load_const_instr_create nir.c
#4 in nir_opt_constant_folding nir_opt_constant_folding.c
#5 in si_nir_is_output_const_if_tex_is_const si_nir_optim.c
#6 in si_check_blend_dst_sampler_noop si_state.c
#7 in si_draw_blend_dst_sampler_noop si_state.c
The complete ASAN log is in asan.log.
Standalone Mesa reproducer
The bug can also be reproduced without Chrome using the standalone C program repro.c. It uses GBM and EGL to create a headless OpenGL 3.2 core context on the hardware driver, compiles the same scalar-output fragment shader, sets the noop blend state, and issues a draw call. It requires the same ASAN-instrumented Mesa build with the slab allocator disabled (step 2 above).
cd /home/user/mesa-asan
# Compile the reproducer with the same Clang used for Mesa
~/chromium/src/third_party/llvm-build/Release+Asserts/bin/clang \
-fsanitize=address -O1 -g \
-o repro repro.c \
-I install/include \
-L install/lib/x86_64-linux-gnu \
-L /home/user/libdrm-new/install/lib/x86_64-linux-gnu \
-lEGL -lGL -lgbm \
-Wl,-rpath,install/lib/x86_64-linux-gnu \
-Wl,-rpath,/home/user/libdrm-new/install/lib/x86_64-linux-gnu
# Run
LD_LIBRARY_PATH=install/lib/x86_64-linux-gnu:/home/user/libdrm-new/install/lib/x86_64-linux-gnu \
LIBGL_DRIVERS_PATH=install/lib/x86_64-linux-gnu/dri \
MESA_SHADER_CACHE_DISABLE=true \
ASAN_OPTIONS="detect_leaks=0:detect_odr_violation=0" \
./repro /dev/dri/renderD128
ASAN reports the same heap-buffer-overflow in get_output_as_const_value.
Suggested Fix
The fix is to read only num_components values from the flexible array instead of the hardcoded 4, and zero-initialize the remaining slots. The patch applies cleanly to Mesa 26.0.4:
--- a/src/gallium/drivers/radeonsi/si_nir_optim.c
+++ b/src/gallium/drivers/radeonsi/si_nir_optim.c
@@ -91,13 +91,15 @@ get_output_as_const_value(nir_shader *shader, float values[4])
!(nir_intrinsic_src_type(intrin) & nir_type_float))
return false;
+ unsigned nc = intrin->src[0].ssa->num_components;
+ memset(values, 0, 4 * sizeof(float));
if (intrin->src[0].ssa->bit_size == 16) {
- uint16_t half_values[4];
- nir_const_value_to_array(half_values, c, 4, u16);
- for (unsigned i = 0; i < 4; i++)
+ uint16_t half_values[4] = {0};
+ nir_const_value_to_array(half_values, c, nc, u16);
+ for (unsigned i = 0; i < nc; i++)
values[i] = _mesa_half_to_float(half_values[i]);
} else {
- nir_const_value_to_array(values, c, 4, f32);
+ nir_const_value_to_array(values, c, nc, f32);
}
return true;
}
With this patch applied, the reproducer completes without any ASAN report.
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.