CVE-2026-9942
Overview
Background
- ANGLE shader translator hardening options
- Flags passed to ANGLE’s GLSL→backend translator that insert defensive AST transforms:
initializeUninitializedLocals(zero-initialize function-local variables),initOutputVariables(zero-initialize shader outputs),clampIndirectArrayBounds, etc. They exist to deny shaders any view of residual GPU memory. - `context->isWebGL()` vs `context->isHardenedContext()`
- WebGL contexts always get the hardening transforms.
isHardenedContext()corresponds toEGL_CONTEXT_HARDENED_ANGLE, which Chrome sets (via thehardened_contextattribute) for standard GLES2/GLES3 contexts from potentially-compromised renderers using the passthrough command decoder (notably Android/Linux). - `src/libANGLE/Shader.cpp` (front-end)
- Already enabled a set of options for both
isWebGL()andisHardenedContext()(e.g.initGLPosition,limitCallStackDepth,initSharedVariables), but the critical per-backend local/output initialization was decided in the backends. - Function-local initialization & SPIR-V
- Vulkan does not mandate zero-initialization of
OpVariable Function; without the transform, locals read whatever residual data is in GPU registers/scratch.
Root Cause Analysis
The invariant is that a “hardened” context must receive the same memory-disclosure defenses as a WebGL context. The front-end (Shader.cpp) honored isWebGL() || isHardenedContext() for its options, but the backends gated the local/output-variable initializers on isWebGL() alone. In ShaderVk::compile the code was if (context->isWebGL()) { options->initializeUninitializedLocals = true; ... options->initOutputVariables = true; }. For a hardened-but-non-WebGL context (a standard GLES context from a possibly-compromised renderer on the passthrough decoder), isWebGL() is false, so these transforms were skipped: the translated SPIR-V contained OpVariable Function without initializers and uninitialized output writes, letting a shader read residual (potentially cross-origin) GPU memory and emit it. The same gap existed in the GL backend (ShaderGL, which gated initOutputVariables, disableARBTextureRectangle, and clampIndirectArrayBounds on isWebGL) and the Metal backend (ShaderMtl, gating initOutputVariables).
The patch aligns all three backends with the front-end: each introduces const bool isHardened = context->isWebGL() || context->isHardenedContext(); and uses it in place of the bare isWebGL() check. ShaderVk::compile now enables initializeUninitializedLocals/initOutputVariables for isWebGL() || isHardenedContext(); ShaderGL uses isHardened for initOutputVariables, disableARBTextureRectangle, and clampIndirectArrayBounds; ShaderMtl uses isHardened for initOutputVariables. A new HardenedContextTest.InitUninitializedLocals expectation and the GLSL-test raw-string refactor accompany it. The advisory’s “Uninitialized Use” (CWE-457) is substantiated by the diff: hardened contexts were not getting local/output zero-initialization. This is a hardening/policy gap whose concrete sink is an uninitialized-memory read that discloses GPU contents.
isWebGL() only, so Chrome’s EGL_CONTEXT_HARDENED_ANGLE contexts for untrusted renderers skipped them and could read residual GPU memory via uninitialized variables.Attack Path
- Compromise a renderer Gain code execution in a sandboxed renderer (assumed starting point; the hardened-context mitigation exists precisely for this threat model).
- Request a hardened GLES context
Over the
GpuChannelMojo interface, request a standard GLES2/GLES3 context; the GPU passthrough decoder flags ithardened_context(isHardenedContext()true,isWebGL()false) on Android/Linux. - Compile a leaking shader
Compile a fragment shader that declares an uninitialized local (e.g.
vec4 leak;) and/or relies on an uninitialized output, writing it togl_FragColor. Pre-patch,ShaderVk/ShaderGL/ShaderMtlskip the zero-init transforms for this context. - Render to an FBO Bind an FBO and draw; the shader executes with uninitialized locals reading residual GPU registers/scratch (potentially cross-origin compositor tiles or other contexts’ data).
- Exfiltrate
glReadPixelsthe FBO to pull the leaked bytes back across the IPC boundary into the renderer — an information-disclosure primitive that crosses the Site Isolation boundary (last step / primitive).
Impact Assessment
C:L/I:L/A:L). The diff proves the missing zero-init transforms; the realized leak of residual/cross-origin data is the documented consequence and is a genuine memory-safety sink (not a mere capability toggle), but it yields disclosure, not corruption or RCE. The exact data leaked and its cross-origin reliability depend on GPU scheduling and are not demonstrated by the diff.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
- `isWebGL()`-only gates across backendsGrep ANGLE backends (D3D, GL, Metal, Vulkan, WebGPU) for any remaining
isWebGL()check that should also honorisHardenedContext(), especially for memory-initialization/clamping transforms. - Front-end vs backend option paritySystematically diff the hardening options set in
Shader.cpp(front-end) against what each backend actually enables, to find other front-end/backend mismatches. - Hardened-context coverageReview all translator options that mitigate info disclosure (output init, shared-var init, array-bounds clamping, call-stack limits) to confirm uniform application to
EGL_CONTEXT_HARDENED_ANGLEcontexts. - Passthrough decoder hardened-context plumbingVerify Chrome actually sets
hardened_contextfor the intended untrusted-renderer configurations so the now-honored flag takes effect.
Patch
From 7b051731b916e7240d8213e04ad77d7cbaa933c7 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> 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 <[email protected]> Commit-Queue: Geoff Lang <[email protected]> Auto-Submit: Shahbaz Youssefi <[email protected]> Commit-Queue: Shahbaz Youssefi <[email protected]> --- 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 locals in hardened ANGLE 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’s Vulkan backend fails to enable shader variable initialization flags for hardened, non-WebGL contexts. This allows a compromised renderer to compile shaders with uninitialized local variables that read residual cross-origin GPU memory. The leaked memory can then be read back via an FBO, bypassing Site Isolation.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/ShaderVk.cppthird_party/angle/src/libANGLE/Shader.cpp
Estimated timestamp from git blame: 2022-08-09
Summary
There is a potential security gap in ANGLE’s Vulkan backend where it fails to enable specific shader translator hardening flags—initializeUninitializedLocals and initOutputVariables—for “hardened” GLES contexts. While these flags are correctly enabled for WebGL contexts, they are missing for standard renderer contexts that have been specifically marked as hardened via EGL_CONTEXT_HARDENED_ANGLE. This discrepancy could allow a compromised renderer to bypass intended security boundaries and leak uninitialized GPU memory containing cross-origin data.
Technical Details
Chrome uses the hardened_context attribute (which maps to EGL_CONTEXT_HARDENED_ANGLE) to provide extra security for standard GLES2/GLES3 contexts requested by potentially compromised renderers, specifically when using the passthrough command decoder on Android/Linux.
In the ANGLE frontend (src/libANGLE/Shader.cpp), the logic correctly identifies these contexts and enables several security options:
// src/libANGLE/Shader.cpp
if (context->isWebGL() || context->isHardenedContext())
{
options.initGLPosition = true;
options.limitCallStackDepth = true;
options.initSharedVariables = true;
// ... (initializeUninitializedLocals is NOT set here)
}
However, the Vulkan backend (src/libANGLE/renderer/vulkan/ShaderVk.cpp) gates the critical variable initialization flags solely on isWebGL():
// src/libANGLE/renderer/vulkan/ShaderVk.cpp
if (context->isWebGL())
{
// Only WebGL requires initialization of local variables, others don't.
options->initializeUninitializedLocals = true;
// ...
options->initOutputVariables = true;
}
Because isHardenedContext() is not checked here, a shader compiled in a hardened (but non-WebGL) context will skip the AST transformations that insert zero-initializers. When the shader is translated to SPIR-V, it will result in OpVariable Function declarations without initializers. The Vulkan specification does not mandate zero-initialization for function-local variables, meaning they will contain whatever residual data was left in the physical GPU registers or scratch memory by previous execution units.
Potential Exploitation Steps
(Note: These are suggested steps based on architectural analysis; a working exploit has not been executed.)
- Renderer Compromise: An attacker achieves arbitrary code execution within the sandboxed renderer process.
- Context Request: The compromised renderer requests a standard GLES2 context via the
GpuChannelMojo interface. The GPU process passthrough decoder automatically flags this as ahardened_context. - Malicious Shader: The attacker compiles a fragment shader that declares uninitialized local variables (e.g.,
vec4 leak;) and writes them directly to the shader output (gl_FragColor = leak;). - Execution & Exfiltration: The attacker binds an FBO and issues a draw call. The shader executes, reading residual cross-origin data (e.g., compositor tile rasterization or other WebGL contexts) from uninitialized GPU registers, and writes it to the FBO. The attacker then uses
glReadPixelsto pull the leaked data back across the IPC boundary into the renderer process.
Suggested Fix
Modify ShaderVk::compile in third_party/angle/src/libANGLE/renderer/vulkan/ShaderVk.cpp to enable initializeUninitializedLocals and initOutputVariables when context->isHardenedContext() is true, matching the security posture applied to context->isWebGL().
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.