High CVSS 5 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in ANGLE
DescriptionUninitialized Use in ANGLE
ComponentANGLE
Bug ClassUninitialized Memory
Tracker503438092
Fix commit7b051731b916 (angle/angle) +181/-151
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-04-16 (per vendor advisory)
Fix landed2026-04-29 (public git)
Disclosed2026-05-27
Patch gap28 days — fix public in source before the advisory

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 to EGL_CONTEXT_HARDENED_ANGLE, which Chrome sets (via the hardened_context attribute) 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() and isHardenedContext() (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.

Key insight
The hardening translator flags that zero-initialize shader locals and outputs were gated on 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

  1. Compromise a renderer Gain code execution in a sandboxed renderer (assumed starting point; the hardened-context mitigation exists precisely for this threat model).
  2. Request a hardened GLES context Over the GpuChannel Mojo interface, request a standard GLES2/GLES3 context; the GPU passthrough decoder flags it hardened_context (isHardenedContext() true, isWebGL() false) on Android/Linux.
  3. 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 to gl_FragColor. Pre-patch, ShaderVk/ShaderGL/ShaderMtl skip the zero-init transforms for this context.
  4. 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).
  5. Exfiltrate glReadPixels the 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

This is an uninitialized-GPU-memory disclosure in the GPU process whose output is read back by the attacker’s renderer, bypassing Site Isolation for GPU contents. It requires a compromised renderer that can request a hardened GLES context on an affected backend/platform (Android/Linux passthrough), so it is defense-in-depth-after-renderer-compromise rather than a direct web primitive (CVSS 5.0, 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.cpp
  • src/libANGLE/renderer/metal/ShaderMtl.mm
  • src/libANGLE/renderer/vulkan/ShaderVk.cpp
  • src/tests/capture_replay_tests/capture_replay_expectations.txt
  • src/tests/gl_tests/GLSLTest.cpp

Audit Directions

  • `isWebGL()`-only gates across backends
    Grep ANGLE backends (D3D, GL, Metal, Vulkan, WebGPU) for any remaining isWebGL() check that should also honor isHardenedContext(), especially for memory-initialization/clamping transforms.
  • Front-end vs backend option parity
    Systematically 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 coverage
    Review 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_ANGLE contexts.
  • Passthrough decoder hardened-context plumbing
    Verify Chrome actually sets hardened_context for the intended untrusted-renderer configurations so the now-honored flag takes effect.
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);
+    }
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_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.)

  1. Renderer Compromise: An attacker achieves arbitrary code execution within the sandboxed renderer process.
  2. Context Request: The compromised renderer requests a standard GLES2 context via the GpuChannel Mojo interface. The GPU process passthrough decoder automatically flags this as a hardened_context.
  3. 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;).
  4. 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 glReadPixels to 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.

View on issue tracker