CVE-2026-9944
Overview
Background
- ANGLE shader backends
- Per-API translators (
ShaderGL.cpp,ShaderMtl.mm,ShaderVk.cpp) that compile GLSL to the native shading language and decide which hardening/translation options the ANGLE translator applies. - `initOutputVariables`
- A translator compile option that forces zero-initialization of shader output variables (varyings / fragment outputs). It exists to stop a shader from emitting stale GPU memory contents by reading an output the program left uninitialized.
- `isWebGL()` vs `isHardenedContext()`
- Two context predicates.
isWebGL()is true only for WebGL-compatibility contexts;isHardenedContext()is true for native-GLES contexts that Chromium’s command-buffer service marks hardened (hardened_context = true) because they are driven by an untrusted renderer even thoughwebgl_compatibility_contextis false. Both should receive the anti-residue mitigations. - Front-end vs backend gating
- The front-end
gl::Shader::compilealready applies these mitigations when the context is WebGL or hardened; the per-backendcompilemethods were the enforcement gap, gatinginitOutputVariablesonisWebGL()alone.
Root Cause Analysis
The invariant is that every security-hardened context (WebGL or hardened native-GLES) must compile shaders with output-variable initialization so a renderer cannot read uninitialized GPU memory through a shader output. The three backend compile implementations violated it by gating initOutputVariables solely on context->isWebGL(). A compromised renderer can request a native GLES context (marked hardened_context = true, webgl_compatibility_context = false); for that context isWebGL() is false, so options->initOutputVariables stayed false and the translator skipped the output-initialization AST transform. A shader could then leave a vertex out/varying uninitialized and surface the stale interpolator/tile memory in the fragment output, disclosing residue from prior GPU operations (including cross-origin compositor content).
The patch introduces const bool isHardened = context->isWebGL() || context->isHardenedContext(); in ShaderGL.cpp and ShaderMtl.mm and uses isHardened in place of the bare isWebGL for the relevant options; ShaderVk.cpp changes the condition directly to context->isWebGL() || context->isHardenedContext(). In ShaderGL.cpp the same isHardened predicate is reused for disableARBTextureRectangle and clampIndirectArrayBounds, aligning two further mitigations with the front-end. The author’s subject line — “Apply hardening translator options when hardened context … done in the front-end, but not by the backends” — confirms the root cause is a backend/front-end gating mismatch. The advisory labels this “Uninitialized Use” (CWE-457); the diff substantiates that precisely — the sink is reading uninitialized shader outputs — with the security consequence being cross-origin information disclosure. (The remaining hunks are test-file reformatting of GLSL string literals and a capture-replay expectation, not security-relevant.)
Attack Path
- Renderer compromise Attacker already has code execution in a sandboxed renderer (precondition stated by the advisory; the IPC surface is not in the diff — (inferred)).
- Request hardened native context
Send a
GpuChannel::CreateCommandBuffer-style IPC requestingCONTEXT_TYPE_OPENGLES3. The service marks ithardened_context = truebut not WebGL. - Author leaky shaders
Compile a program whose vertex shader declares an
out/varying but leaves it uninitialized on some control-flow path, and whose fragment shader forwards thatinto the fragment output. - Draw to offscreen target
Bind an offscreen framebuffer and issue a draw. Because
initOutputVariableswas false, the uninitialized output carries stale GPU memory into the rendered pixels. - Read back residue
glReadPixelsreturns the stale bytes to the renderer, yielding a cross-origin / cross-process GPU-memory information leak (Site Isolation bypass).
Impact Assessment
Files Changed
src/libANGLE/renderer/gl/ShaderGL.cppsrc/libANGLE/renderer/metal/ShaderMtl.mmsrc/libANGLE/renderer/vulkan/ShaderVk.cppsrc/tests/capture_replay_tests/capture_replay_expectations.txtsrc/tests/gl_tests/GLSLTest.cpp
Audit Directions
- Other `isWebGL()`-only gatesGrep ANGLE backends and front-end for mitigations keyed on
isWebGL()withoutisHardenedContext()(array clamping, call-stack-depth limits, loop/index clamping); any such gate is bypassed for hardened native contexts. - Front-end/backend mitigation parityDiff the set of options set in
gl::Shader::compileagainst each backend’scompileto find mitigations the front-end applies that a backend silently drops. - Uninitialized-read sinks beyond outputsThe diff notes
InitializeUninitializedLocalsignores global interface variables; audit shared/uniform/interface-block initialization for similar residue paths into readable outputs. - Hardened-context propagationVerify
hardened_contextis honored everywhere a renderer-reachable native context is created, including Metal/Vulkan/GL feature paths and any new backends.
Patch
From 7b051731b916e7240d8213e04ad77d7cbaa933c7 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Mon, 27 Apr 2026 14:42:36 -0400
Subject: [PATCH] Apply hardening translator options when hardened context
This was done in the front-end, but not by the backends.
Bug: chromium:503438092
Bug: chromium:503471286
Change-Id: I656d14a99ad8cdd2b54c9e356edefb161ce66b73
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7797473
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Auto-Submit: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/gl/ShaderGL.cpp b/src/libANGLE/renderer/gl/ShaderGL.cpp
index ffa4fb0..b7b1bab 100644
--- a/src/libANGLE/renderer/gl/ShaderGL.cpp
+++ b/src/libANGLE/renderer/gl/ShaderGL.cpp
@@ -128,14 +128,14 @@
options->initGLPosition = true;
- bool isWebGL = context->isWebGL();
- if (isWebGL || (features.initFragmentOutputVariables.enabled &&
- mState.getShaderType() == gl::ShaderType::Fragment))
+ const bool isHardened = context->isWebGL() || context->isHardenedContext();
+ if (isHardened || (features.initFragmentOutputVariables.enabled &&
+ mState.getShaderType() == gl::ShaderType::Fragment))
{
options->initOutputVariables = true;
}
- if (isWebGL && !context->getState().getEnableFeature(GL_TEXTURE_RECTANGLE_ANGLE))
+ if (isHardened && !context->getState().getEnableFeature(GL_TEXTURE_RECTANGLE_ANGLE))
{
options->disableARBTextureRectangle = true;
}
@@ -201,7 +201,7 @@
options->selectViewInNvGLSLVertexShader = true;
}
- if (features.clampArrayAccess.enabled || isWebGL)
+ if (features.clampArrayAccess.enabled || isHardened)
{
options->clampIndirectArrayBounds = true;
}
diff --git a/src/libANGLE/renderer/metal/ShaderMtl.mm b/src/libANGLE/renderer/metal/ShaderMtl.mm
index d41edde..fd75a48 100644
--- a/src/libANGLE/renderer/metal/ShaderMtl.mm
+++ b/src/libANGLE/renderer/metal/ShaderMtl.mm
@@ -70,7 +70,8 @@
options->forceDeferNonConstGlobalInitializers = true;
- if (context->isWebGL() && mState.getShaderType() != gl::ShaderType::Compute)
+ const bool isHardened = context->isWebGL() || context->isHardenedContext();
+ if (isHardened && mState.getShaderType() != gl::ShaderType::Compute)
{
options->initOutputVariables = true;
}
diff --git a/src/libANGLE/renderer/vulkan/ShaderVk.cpp b/src/libANGLE/renderer/vulkan/ShaderVk.cpp
index d5e0e29..7ae4b23 100644
--- a/src/libANGLE/renderer/vulkan/ShaderVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ShaderVk.cpp
@@ -25,7 +25,7 @@
{
ContextVk *contextVk = vk::GetImpl(context);
- if (context->isWebGL())
+ if (context->isWebGL() || context->isHardenedContext())
{
// Only WebGL requires initialization of local variables, others don't.
// Extra initialization in spirv shader may affect performance.
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 75e6ddb..ab2156b 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -46,6 +46,7 @@
42264743 : VulkanExternalImageTest.WaitSemaphoresRetainsContentOpaqueFd/* = TIMEOUT
42264785 : WebGLCompatibilityTest.MultiContextNoRenderingFeedbackLoops/* = SKIP_FOR_CAPTURE
+507736512 : HardenedContextTest.InitUninitializedLocals/* = FAIL
42264706 WIN : WebGLCompatibilityTest.DrawElementsBufferOutOfBoundsInVertexBuffer/* = FLAKY
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index f978542..3f1d59f 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -6459,35 +6459,32 @@
// Test skipped on Android because of bug with Nexus 5X.
ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES());
- constexpr char kVS[] =
- "#version 300 es\n"
- "out vec2 texCoord;\n"
- "in vec2 position;\n"
- "void main()\n"
- "{\n"
- " gl_Position = vec4(position, 0, 1);\n"
- " texCoord = position * 0.5 + vec2(0.5);\n"
- "}\n";
+ constexpr char kVS[] = R"(#version 300 es
+out vec2 texCoord;
+in vec2 position;
+void main()
+{
+ gl_Position = vec4(position, 0, 1);
+ texCoord = position * 0.5 + vec2(0.5);
+})";
- constexpr char kSimpleFS[] =
- "#version 300 es\n"
- "in mediump vec2 texCoord;\n"
- "out mediump vec4 fragColor;\n"
- "void main()\n"
- "{\n"
- " fragColor = vec4(texCoord, 0, 1);\n"
- "}\n";
+ constexpr char kSimpleFS[] = R"(#version 300 es
+in mediump vec2 texCoord;
+out mediump vec4 fragColor;
+void main()
+{
+ fragColor = vec4(texCoord, 0, 1);
+})";
- constexpr char kNestedFS[] =
- "#version 300 es\n"
- "uniform mediump sampler2D samplerA;\n"
- "uniform mediump sampler2D samplerB;\n"
- "in mediump vec2 texCoord;\n"
- "out mediump vec4 fragColor;\n"
- "void main ()\n"
- "{\n"
- " fragColor = texture(samplerB, texture(samplerA, texCoord).xy);\n"
- "}\n";
+ constexpr char kNestedFS[] = R"(#version 300 es
+uniform mediump sampler2D samplerA;
+uniform mediump sampler2D samplerB;
+in mediump vec2 texCoord;
+out mediump vec4 fragColor;
+void main ()
+{
+ fragColor = texture(samplerB, texture(samplerA, texCoord).xy);
+})";
ANGLE_GL_PROGRAM(initProg, kVS, kSimpleFS);
ANGLE_GL_PROGRAM(nestedProg, kVS, kNestedFS);
@@ -6611,29 +6608,28 @@
// http://anglebug.com/40096454
ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES());
- constexpr char kFS[] =
- "#version 300 es\n"
- "precision mediump float;\n"
- "out vec4 my_FragColor;\n"
- "int result = 0;\n"
- "void main()\n"
- "{\n"
- " int u;\n"
- " result += u;\n"
- " int k = 0;\n"
- " for (int i[2], j = i[0] + 1; k < 2; ++k)\n"
- " {\n"
- " result += j;\n"
- " }\n"
- " if (result == 2)\n"
- " {\n"
- " my_FragColor = vec4(0, 1, 0, 1);\n"
- " }\n"
- " else\n"
- " {\n"
- " my_FragColor = vec4(1, 0, 0, 1);\n"
- " }\n"
- "}\n";
+ constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 my_FragColor;
+int result = 0;
+void main()
+{
+ int u;
+ result += u;
+ int k = 0;
+ for (int i[2], j = i[0] + 1; k < 2; ++k)
+ {
+ result += j;
+ }
+ if (result == 2)
+ {
+ my_FragColor = vec4(0, 1, 0, 1);
+ }
+ else
+ {
+ my_FragColor = vec4(1, 0, 0, 1);
+ }
Regression Test / PoC
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 75e6ddb..ab2156b 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -46,6 +46,7 @@
42264743 : VulkanExternalImageTest.WaitSemaphoresRetainsContentOpaqueFd/* = TIMEOUT
42264785 : WebGLCompatibilityTest.MultiContextNoRenderingFeedbackLoops/* = SKIP_FOR_CAPTURE
+507736512 : HardenedContextTest.InitUninitializedLocals/* = FAIL
42264706 WIN : WebGLCompatibilityTest.DrawElementsBufferOutOfBoundsInVertexBuffer/* = FLAKY
diff --git a/src/tests/gl_tests/GLSLTest.cpp b/src/tests/gl_tests/GLSLTest.cpp
index f978542..3f1d59f 100644
--- a/src/tests/gl_tests/GLSLTest.cpp
+++ b/src/tests/gl_tests/GLSLTest.cpp
@@ -6459,35 +6459,32 @@
// Test skipped on Android because of bug with Nexus 5X.
ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES());
- constexpr char kVS[] =
- "#version 300 es\n"
- "out vec2 texCoord;\n"
- "in vec2 position;\n"
- "void main()\n"
- "{\n"
- " gl_Position = vec4(position, 0, 1);\n"
- " texCoord = position * 0.5 + vec2(0.5);\n"
- "}\n";
+ constexpr char kVS[] = R"(#version 300 es
+out vec2 texCoord;
+in vec2 position;
+void main()
+{
+ gl_Position = vec4(position, 0, 1);
+ texCoord = position * 0.5 + vec2(0.5);
+})";
- constexpr char kSimpleFS[] =
- "#version 300 es\n"
- "in mediump vec2 texCoord;\n"
- "out mediump vec4 fragColor;\n"
- "void main()\n"
- "{\n"
- " fragColor = vec4(texCoord, 0, 1);\n"
- "}\n";
+ constexpr char kSimpleFS[] = R"(#version 300 es
+in mediump vec2 texCoord;
+out mediump vec4 fragColor;
+void main()
+{
+ fragColor = vec4(texCoord, 0, 1);
+})";
- constexpr char kNestedFS[] =
- "#version 300 es\n"
- "uniform mediump sampler2D samplerA;\n"
- "uniform mediump sampler2D samplerB;\n"
- "in mediump vec2 texCoord;\n"
- "out mediump vec4 fragColor;\n"
- "void main ()\n"
- "{\n"
- " fragColor = texture(samplerB, texture(samplerA, texCoord).xy);\n"
- "}\n";
+ constexpr char kNestedFS[] = R"(#version 300 es
+uniform mediump sampler2D samplerA;
+uniform mediump sampler2D samplerB;
+in mediump vec2 texCoord;
+out mediump vec4 fragColor;
+void main ()
+{
+ fragColor = texture(samplerB, texture(samplerA, texCoord).xy);
+})";
ANGLE_GL_PROGRAM(initProg, kVS, kSimpleFS);
ANGLE_GL_PROGRAM(nestedProg, kVS, kNestedFS);
@@ -6611,29 +6608,28 @@
// http://anglebug.com/40096454
ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES());
- constexpr char kFS[] =
- "#version 300 es\n"
- "precision mediump float;\n"
- "out vec4 my_FragColor;\n"
- "int result = 0;\n"
- "void main()\n"
- "{\n"
- " int u;\n"
- " result += u;\n"
- " int k = 0;\n"
- " for (int i[2], j = i[0] + 1; k < 2; ++k)\n"
- " {\n"
- " result += j;\n"
- " }\n"
- " if (result == 2)\n"
- " {\n"
- " my_FragColor = vec4(0, 1, 0, 1);\n"
- " }\n"
- " else\n"
- " {\n"
- " my_FragColor = vec4(1, 0, 0, 1);\n"
- " }\n"
- "}\n";
+ constexpr char kFS[] = R"(#version 300 es
+precision mediump float;
+out vec4 my_FragColor;
+int result = 0;
+void main()
+{
+ int u;
+ result += u;
+ int k = 0;
+ for (int i[2], j = i[0] + 1; k < 2; ++k)
+ {
+ result += j;
+ }
+ if (result == 2)
+ {
+ my_FragColor = vec4(0, 1, 0, 1);
+ }
+ else
+ {
+ my_FragColor = vec4(1, 0, 0, 1);
+ }
+})";
ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
@@ -6653,29 +6649,28 @@
// http://anglebug.com/40096454
ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES());
- constexpr char kFS[] =
- "precision mediump float;\n"
- "struct T\n"
- "{\n"
- " int a[2];\n"
- "};\n"
- "struct S\n"
- "{\n"
- " T t[2];\n"
- "};\n"
- "void main()\n"
- "{\n"
- " S s;\n"
- " S s2;\n"
- " if (s.t[1].a[1] == 0 && s2.t[1].a[1] == 0)\n"
- " {\n"
- " gl_FragColor = vec4(0, 1, 0, 1);\n"
- " }\n"
- " else\n"
- " {\n"
- " gl_FragColor = vec4(1, 0, 0, 1);\n"
- " }\n"
- "}\n";
+ constexpr char kFS[] = R"(precision mediump float;
+struct T
+{
+ int a[2];
+};
+struct S
+{
+ T t[2];
+};
+void main()
+{
+ S s;
+ S s2;
+ if (s.t[1].a[1] == 0 && s2.t[1].a[1] == 0)
+ {
+ gl_FragColor = vec4(0, 1, 0, 1);
+ }
+ else
+ {
+ gl_FragColor = vec4(1, 0, 0, 1);
+ }
+})";
ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), kFS);
drawQuad(program, essl1_shaders::PositionAttrib(), 0.5f, 1.0f, true);
@@ -6686,47 +6681,47 @@
// not link.
TEST_P(GLSLTest, StructureNameMatchingTest)
{
- const char *vsSource =
- "// Structures must have the same name, sequence of type names, and\n"
- "// type definitions, and field names to be considered the same type.\n"
- "// GLSL 1.017 4.2.4\n"
- "precision mediump float;\n"
- "struct info {\n"
- " vec4 pos;\n"
- " vec4 color;\n"
- "};\n"
- "\n"
- "uniform info uni;\n"
- "void main()\n"
- "{\n"
- " gl_Position = uni.pos;\n"
- "}\n";
+ constexpr char kVS[] = R"(
+// Structures must have the same name, sequence of type names, and
+// type definitions, and field names to be considered the same type.
+// GLSL 1.017 4.2.4
+precision mediump float;
+struct info {
+ vec4 pos;
+ vec4 color;
+};
- GLuint vs = CompileShader(GL_VERTEX_SHADER, vsSource);
+uniform info uni;
+void main()
+{
+ gl_Position = uni.pos;
+})";
+
+ GLuint vs = CompileShader(GL_VERTEX_SHADER, kVS);
ASSERT_NE(0u, vs);
glDeleteShader(vs);
- const char *fsSource =
- "// Structures must have the same name, sequence of type names, and\n"
- "// type definitions, and field names to be considered the same type.\n"
- "// GLSL 1.017 4.2.4\n"
- "precision mediump float;\n"
- "struct info1 {\n"
- " vec4 pos;\n"
- " vec4 color;\n"
- "};\n"
- "\n"
- "uniform info1 uni;\n"
- "void main()\n"
- "{\n"
- " gl_FragColor = uni.color;\n"
- "}\n";
+ constexpr char kFS[] = R"(
+// Structures must have the same name, sequence of type names, and
+// type definitions, and field names to be considered the same type.
+// GLSL 1.017 4.2.4
+precision mediump float;
+struct info1 {
+ vec4 pos;
+ vec4 color;
+};
- GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fsSource);
+uniform info1 uni;
+void main()
+{
+ gl_FragColor = uni.color;
+})";
+
+ GLuint fs = CompileShader(GL_FRAGMENT_SHADER, kFS);
ASSERT_NE(0u, fs);
glDeleteShader(fs);
- GLuint program = CompileProgram(vsSource, fsSource);
+ GLuint program = CompileProgram(kVS, kFS);
EXPECT_EQ(0u, program);
}
@@ -6737,17 +6732,16 @@
// http://anglebug.com/40096454
ANGLE_SKIP_TEST_IF(IsAndroid() && IsOpenGLES());
- constexpr char kFS[] =
- "#version 300 es\n"
- "precision highp float;\n"
- "out vec4 my_FragColor;\n"
- "void main()\n"
- "{\n"
- " my_FragColor = vec4(1, 0, 0, 1);\n"
- " for (struct { float q; } b; b.q < 2.0; b.q++) {\n"
- " my_FragColor = vec4(0, 1, 0, 1);\n"
- " }\n"
- "}\n";
+ constexpr char kFS[] = R"(#version 300 es
+precision highp float;
+out vec4 my_FragColor;
+void main()
+{
+ my_FragColor = vec4(1, 0, 0, 1);
+ for (struct { float q; } b; b.q < 2.0; b.q++) {
+ my_FragColor = vec4(0, 1, 0, 1);
+ }
+})";
ANGLE_GL_PROGRAM(program, essl3_shaders::vs::Simple(), kFS);
drawQuad(program, essl3_shaders::PositionAttrib(), 0.5f, 1.0f, true);
@@ -6760,22 +6754,21 @@
// http://anglebug.com/42261561
ANGLE_SKIP_TEST_IF(IsAndroid() && IsAdreno() && IsOpenGLES());
- constexpr char kFS[] =
- "precision mediump float;\n"
- "int result;\n"
- "int i[2], j = i[0] + 1;\n"
- "void main()\n"
- "{\n"
- " result += j;\n"
- " if (result == 1)\n"
... (truncated)
Original Bug Report
GPU memory leak via uninitialized shader outputs in non-WebGL hardened contexts
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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: ANGLE backends (Metal, GL, Vulkan) fail to enable shader output initialization for non-WebGL hardened contexts. A compromised renderer can exploit this by requesting a native GLES context and supplying shaders with uninitialized outputs. This potentially allows the renderer to read sensitive cross-origin GPU memory residue, bypassing Site Isolation.
Affected files:
third_party/angle/src/libANGLE/renderer/metal/ShaderMtl.mmthird_party/angle/src/libANGLE/renderer/gl/ShaderGL.cpp
Estimated timestamp from git blame: 2026-01-14
Description
In Chromium, a compromised sandboxed renderer process can request the creation of native GLES contexts (e.g., CONTEXT_TYPE_OPENGLES3) via the GpuChannel::CreateCommandBuffer IPC interface. To ensure security when untrusted processes access non-WebGL contexts, the command buffer service marks these contexts as “hardened” by setting attribs.hardened_context = true (while webgl_compatibility_context is correctly false) before passing them to EGL and ANGLE.
While ANGLE’s front-end compiler correctly applies several security mitigations (like limiting call stack depth) if a context isWebGL() OR isHardenedContext(), a critical mitigation is missed in the backend implementations. The initOutputVariables compile option, which forces the zero-initialization of shader output variables to prevent reading stale GPU memory, is gated solely on isWebGL() in the Metal, GL, and Vulkan backends. Because a hardened native GLES context is not a WebGL context, this mitigation is bypassed.
By intentionally leaving a vertex shader output (varying) uninitialized, an attacker can cause the subsequent fragment shader to read stale, uninitialized interpolator or tile memory. This memory often contains sensitive residue from previous GPU operations, such as cross-origin rendering performed by the browser compositor.
Root Cause
The vulnerability stems from the implementation of the compile method in several ANGLE backends, where the decision to enable initOutputVariables ignores isHardenedContext():
- Metal Backend (
third_party/angle/src/libANGLE/renderer/metal/ShaderMtl.mmlines 73-76):if (context->isWebGL() && mState.getShaderType() != gl::ShaderType::Compute) { options->initOutputVariables = true; } - GL Backend (
third_party/angle/src/libANGLE/renderer/gl/ShaderGL.cpplines 131-136):bool isWebGL = context->isWebGL(); if (isWebGL || ...) { options->initOutputVariables = true; } - Vulkan Backend (
third_party/angle/src/libANGLE/renderer/vulkan/ShaderVk.cpplines 28-42):if (context->isWebGL() && ...) { options->initOutputVariables = true; // ... }
Because initOutputVariables remains false, the ANGLE translator (Compiler.cpp) skips the initializeOutputVariables AST transformation step. Note that while InitializeUninitializedLocals runs, it explicitly ignores global interface variables like shader outputs.
Impact
An attacker with arbitrary code execution in a renderer process can read out-of-bounds cross-origin GPU memory. This is a Site Isolation bypass, leading to the potential disclosure of sensitive information belonging to other origins or the browser process.
Suggested Reproduction Steps
(Note: These are potential steps based on code analysis; our tooling agent does not yet have the ability to run code to verify this directly).
- From a compromised renderer, send an IPC to create a GLES command buffer with
context_typeset toCONTEXT_TYPE_OPENGLES3. - Compile a shader program with a vertex shader that declares an
outvariable (e.g.,out vec4 leaked_data;) but contains a control flow path that intentionally leaves it uninitialized. - Provide a fragment shader that takes this
in vec4 leaked_data;and assigns it to the fragment output color. - Link the program, bind an offscreen framebuffer, and perform a draw call.
- Execute
glReadPixelsto read the framebuffer contents back into the renderer process. The returned pixels will contain stale residue from the GPU’s uninitialized memory.
Suggested Fix
Update the backend shader compilation methods (ShaderMtl::compile, ShaderGL::compile, and ShaderVk::compile) to enable initOutputVariables when the context is a hardened context, aligning it with the logic used in the front-end gl::Shader::compile.
For example, change the conditions to:
if ((context->isWebGL() || context->isHardenedContext()) && ...)
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.