Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker503596863
Fix commiteb897fef02f1 (angle/angle) +82/-21
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
Task
src/tests/gl_tests/ParallelShaderCompileTest.cpp
modified
ParallelShaderCompileTestES31
src/tests/gl_tests/ParallelShaderCompileTest.cpp
modified

Files Changed

  • src/libANGLE/Program.cpp
  • src/tests/gl_tests/ParallelShaderCompileTest.cpp
From eb897fef02f149711350e6fe21c1ef96162aa570 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 28 Apr 2026 13:11:40 -0400
Subject: [PATCH] Fix race between context destroy and link job

The link job held references to Caps and Limitations of the context that
started the link.  This change makes that a copy in case the context is
destroyed while the link job is in progress.

In reality, the caps and limitations that matter for link should be
properties of the share group, because the program is shared between
them and in practice the caps and limitations that are checked during
link are equal between all contexts of the share group.  A potential
future change could refactor Caps and Limitations to be per share group
or even device, with a few caps specific to the context (e.g. where
GLES1 and GLES2+ caps differ).

Bug: chromium:503596863
Change-Id: Ib876f574cd30ee7f21e58be4dea7bd756706cfe2
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7799786
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---

diff --git a/src/libANGLE/Program.cpp b/src/libANGLE/Program.cpp
index add1e25..bbd2f4c 100644
--- a/src/libANGLE/Program.cpp
+++ b/src/libANGLE/Program.cpp
@@ -633,9 +633,10 @@
   private:
     angle::Result linkImpl();
 
-    // State needed for link
-    const Caps &mCaps;
-    const Limitations &mLimitations;
+    // State needed for link.  Note that Caps and Limitations are copied because the context that
+    // started the link task may get destroyed before the link job is finished.
+    const Caps mCaps;
+    const Limitations mLimitations;
     const Version mClientVersion;
     const bool mIsWebGL;
     Program *mProgram;
diff --git a/src/tests/gl_tests/ParallelShaderCompileTest.cpp b/src/tests/gl_tests/ParallelShaderCompileTest.cpp
index 062591b..fae24c3 100644
--- a/src/tests/gl_tests/ParallelShaderCompileTest.cpp
+++ b/src/tests/gl_tests/ParallelShaderCompileTest.cpp
@@ -38,20 +38,6 @@
         setConfigAlphaBits(8);
     }
 
-    bool ensureParallelShaderCompileExtensionAvailable()
-    {
-        if (IsGLExtensionRequestable("GL_KHR_parallel_shader_compile"))
-        {
-            glRequestExtensionANGLE("GL_KHR_parallel_shader_compile");
-        }
-
-        if (!IsGLExtensionEnabled("GL_KHR_parallel_shader_compile"))
-        {
-            return false;
-        }
-        return true;
-    }
-
     class Task
     {
       public:
@@ -388,7 +374,7 @@
 // Test basic functionality of GL_KHR_parallel_shader_compile
 TEST_P(ParallelShaderCompileTest, Basic)
 {
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     GLint count = 0;
     glMaxShaderCompilerThreadsKHR(8);
@@ -401,7 +387,7 @@
 // Test to compile and link many programs in parallel.
 TEST_P(ParallelShaderCompileTest, LinkAndDrawManyPrograms)
 {
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     TaskRunner<ClearColorWithDraw> runner;
     runner.run(this, kPollInterval);
@@ -412,12 +398,86 @@
 // crbug.com/1317673
 TEST_P(ParallelShaderCompileTest, LinkProgramAndRecompileShader)
 {
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     TaskRunner<ClearColorWithDrawRecompile> runner;
     runner.run(this, 0);
 }
 
+// Tests no crash in case the program is being linked, is not current, and the context that spawned
+// the task is destroyed.
+TEST_P(ParallelShaderCompileTest, DestroyContextWhileLinkIsInProgress)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
+
+    // Create a context shared with the current one
+    EGLWindow *window          = getEGLWindow();
+    EGLDisplay display         = window->getDisplay();
+    EGLConfig config           = window->getConfig();
+    EGLSurface surface         = window->getSurface();
+    EGLint contextAttributes[] = {
+        EGL_CONTEXT_MAJOR_VERSION_KHR,
+        GetParam().majorVersion,
+        EGL_CONTEXT_MINOR_VERSION_KHR,
+        GetParam().minorVersion,
+        EGL_NONE,
+    };
+    EGLContext context1 = eglGetCurrentContext();
+    EGLContext context2 = eglCreateContext(display, config, context1, contextAttributes);
+    ASSERT_NE(context2, EGL_NO_CONTEXT);
+    eglMakeCurrent(display, surface, surface, context2);
+
+    constexpr char kVS[] = R"(precision mediump float;
+uniform vec4 u;
+attribute vec4 a;
+varying vec4 b;
+void main()
+{
+    gl_Position = a;
+    b = u;
+})";
+    constexpr char kFS[] = R"(precision mediump float;
+uniform sampler2D s;
+varying vec4 b;
+void main()
+{
+    gl_FragColor = texture2D(s, vec2(0)) + b;
+})";
+
+    GLuint program = glCreateProgram();
+
+    GLuint vs = CompileShader(GL_VERTEX_SHADER, kVS);
+    GLuint fs = CompileShader(GL_FRAGMENT_SHADER, kFS);
+
+    EXPECT_NE(0u, vs);
+    EXPECT_NE(0u, fs);
+
+    glAttachShader(program, vs);
+    glDeleteShader(vs);
+
+    glAttachShader(program, fs);
+    glDeleteShader(fs);
+
+    // Start the link job, but don't wait for it to finish.  Don't bind the program to the context.
+    glLinkProgram(program);
+
+    // Make context1 current again.
+    eglMakeCurrent(display, surface, surface, context1);
+
+    // Destroy the context that spawned the link job
+    eglDestroyContext(display, context2);
+
+    // Destroy the shader
+    glDetachShader(program, vs);
+    glDetachShader(program, fs);
+
+    GLint linkStatus;
+    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+    EXPECT_GL_TRUE(linkStatus);
+
+    EXPECT_GL_NO_ERROR();
+}
+
 class ParallelShaderCompileTestES31 : public ParallelShaderCompileTest
 {};
 
@@ -431,7 +491,7 @@
     // TODO(http://anglebug.com/42264192): Fails on Linux+Intel+OpenGL
     ANGLE_SKIP_TEST_IF(IsLinux() && IsIntel() && IsOpenGL());
 
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     TaskRunner<ImageLoadStore> runner;
     runner.run(this, kPollInterval);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/ParallelShaderCompileTest.cpp b/src/tests/gl_tests/ParallelShaderCompileTest.cpp
index 062591b..fae24c3 100644
--- a/src/tests/gl_tests/ParallelShaderCompileTest.cpp
+++ b/src/tests/gl_tests/ParallelShaderCompileTest.cpp
@@ -38,20 +38,6 @@
         setConfigAlphaBits(8);
     }
 
-    bool ensureParallelShaderCompileExtensionAvailable()
-    {
-        if (IsGLExtensionRequestable("GL_KHR_parallel_shader_compile"))
-        {
-            glRequestExtensionANGLE("GL_KHR_parallel_shader_compile");
-        }
-
-        if (!IsGLExtensionEnabled("GL_KHR_parallel_shader_compile"))
-        {
-            return false;
-        }
-        return true;
-    }
-
     class Task
     {
       public:
@@ -388,7 +374,7 @@
 // Test basic functionality of GL_KHR_parallel_shader_compile
 TEST_P(ParallelShaderCompileTest, Basic)
 {
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     GLint count = 0;
     glMaxShaderCompilerThreadsKHR(8);
@@ -401,7 +387,7 @@
 // Test to compile and link many programs in parallel.
 TEST_P(ParallelShaderCompileTest, LinkAndDrawManyPrograms)
 {
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     TaskRunner<ClearColorWithDraw> runner;
     runner.run(this, kPollInterval);
@@ -412,12 +398,86 @@
 // crbug.com/1317673
 TEST_P(ParallelShaderCompileTest, LinkProgramAndRecompileShader)
 {
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     TaskRunner<ClearColorWithDrawRecompile> runner;
     runner.run(this, 0);
 }
 
+// Tests no crash in case the program is being linked, is not current, and the context that spawned
+// the task is destroyed.
+TEST_P(ParallelShaderCompileTest, DestroyContextWhileLinkIsInProgress)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
+
+    // Create a context shared with the current one
+    EGLWindow *window          = getEGLWindow();
+    EGLDisplay display         = window->getDisplay();
+    EGLConfig config           = window->getConfig();
+    EGLSurface surface         = window->getSurface();
+    EGLint contextAttributes[] = {
+        EGL_CONTEXT_MAJOR_VERSION_KHR,
+        GetParam().majorVersion,
+        EGL_CONTEXT_MINOR_VERSION_KHR,
+        GetParam().minorVersion,
+        EGL_NONE,
+    };
+    EGLContext context1 = eglGetCurrentContext();
+    EGLContext context2 = eglCreateContext(display, config, context1, contextAttributes);
+    ASSERT_NE(context2, EGL_NO_CONTEXT);
+    eglMakeCurrent(display, surface, surface, context2);
+
+    constexpr char kVS[] = R"(precision mediump float;
+uniform vec4 u;
+attribute vec4 a;
+varying vec4 b;
+void main()
+{
+    gl_Position = a;
+    b = u;
+})";
+    constexpr char kFS[] = R"(precision mediump float;
+uniform sampler2D s;
+varying vec4 b;
+void main()
+{
+    gl_FragColor = texture2D(s, vec2(0)) + b;
+})";
+
+    GLuint program = glCreateProgram();
+
+    GLuint vs = CompileShader(GL_VERTEX_SHADER, kVS);
+    GLuint fs = CompileShader(GL_FRAGMENT_SHADER, kFS);
+
+    EXPECT_NE(0u, vs);
+    EXPECT_NE(0u, fs);
+
+    glAttachShader(program, vs);
+    glDeleteShader(vs);
+
+    glAttachShader(program, fs);
+    glDeleteShader(fs);
+
+    // Start the link job, but don't wait for it to finish.  Don't bind the program to the context.
+    glLinkProgram(program);
+
+    // Make context1 current again.
+    eglMakeCurrent(display, surface, surface, context1);
+
+    // Destroy the context that spawned the link job
+    eglDestroyContext(display, context2);
+
+    // Destroy the shader
+    glDetachShader(program, vs);
+    glDetachShader(program, fs);
+
+    GLint linkStatus;
+    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+    EXPECT_GL_TRUE(linkStatus);
+
+    EXPECT_GL_NO_ERROR();
+}
+
 class ParallelShaderCompileTestES31 : public ParallelShaderCompileTest
 {};
 
@@ -431,7 +491,7 @@
     // TODO(http://anglebug.com/42264192): Fails on Linux+Intel+OpenGL
     ANGLE_SKIP_TEST_IF(IsLinux() && IsIntel() && IsOpenGL());
 
-    ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_KHR_parallel_shader_compile"));
 
     TaskRunner<ImageLoadStore> runner;
     runner.run(this, kPollInterval);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in ANGLE MainLinkTask via dangling Caps reference

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: A potential Use-After-Free vulnerability exists in ANGLE’s parallel shader linking when a background worker thread (MainLinkTask) holds a raw reference to a destroyed Context’s capabilities. An attacker can trigger this by utilizing a shared context group and forcing context loss while an unbound program is linking in the background. This allows a compromised renderer to achieve arbitrary code execution in the GPU process.

Affected files:

  • third_party/angle/src/libANGLE/Program.cpp
  • third_party/angle/src/libANGLE/Context.cpp
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc
  • third_party/angle/src/libANGLE/State.h

Estimated timestamp from git blame: 2023-10-24

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in ANGLE’s parallel shader compilation logic. The MainLinkTask class, which handles background program linking, captures raw C++ references to the Caps and Limitations structures (const Caps &mCaps;) owned by the Context’s inline PrivateState.

When a Context is destroyed, its PrivateState and the associated Caps memory are freed. However, Context::onDestroy only ensures that pending link tasks for the currently bound program (or pipeline) are explicitly joined. If a linking program is unbound and the context is destroyed, the background thread will continue executing and eventually dereference the freed Caps memory, resulting in a UAF.

Potential Trigger Steps

An attacker with a compromised renderer process could theoretically trigger this via the command buffer IPCs to the GPU process:

  1. Setup Shared Context: The attacker requests a GLES2 command buffer (creating Context A). They then send an IPC like CopySharedImageINTERNAL, which lazily initializes lazy_context_ in the passthrough decoder. This creates Context B, which explicitly shares the same ANGLE ShareGroup as Context A.
  2. Start Background Link: The attacker creates a shader program, attaches complex shaders (to prolong linking), and calls glLinkProgram. This creates a MainLinkTask that captures a reference to Context A’s Caps and is dispatched to a background worker thread.
  3. Unbind Program: The attacker calls glUseProgram(0) to ensure the program is no longer currently bound to Context A.
  4. Force Context Teardown: The attacker sends a LoseContextCHROMIUM command and destroys the command buffer. Context A is destroyed.
  5. Bypass Link Resolution: Because the program is unbound, Context::onDestroy’s call to mState.ensureNoPendingLink(this) ignores it, failing to join the background thread.
  6. Program Leakage: Context A is destroyed, freeing its Caps. However, Context B keeps the ShareGroup (and thus the ShaderProgramManager and the linking program) alive.
  7. UAF: The worker thread continues execution and accesses fields like caps.maxVertexAttributes from the freed memory.

Note: These are suggested steps based on source code analysis; our tooling agent cannot execute code to provide a live proof-of-concept.

Impact

By utilizing heap spraying techniques in the GPU process before Context A is fully destroyed, an attacker could potentially control the freed Caps memory. Modifying values like maxVertexAttributes bypasses bounds checks during linking (e.g., when populating usedAttribMap in Program::linkAttributes), leading to arbitrary out-of-bounds reads and writes. Because ANGLE is a third-party library without MiraclePtr protections, this UAF can be reliably exploited for a renderer-to-GPU sandbox escape.

Suggested Fix

  1. Capture by Value: Update MainLinkTask (and related structures) to capture Caps, Limitations, and Version by value rather than by reference. These structures are relatively small and copying them would guarantee the worker thread does not depend on the lifetime of the Context.
  2. Complete Thread Joining: Ensure that Context::onDestroy or ShaderProgramManager properly joins all background compilation/linking threads belonging to the context/share group upon destruction, even if the program is not currently bound.

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