High CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
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
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-17 (per vendor advisory)
Fix landed2026-04-29 (public git)
Disclosed2026-05-27
Patch gap28 days — fix public in source before the advisory

Background

ANGLE parallel shader linking
Under GL_KHR_parallel_shader_compile, glLinkProgram spawns a background worker (MainLinkTask in src/libANGLE/Program.cpp) that performs the link off the GL thread; the program becomes usable once the job completes.
`Caps` / `Limitations`
Device/context capability structures (max vertex attributes, etc.) consulted during link to size and bounds-check per-attribute data (e.g. usedAttribMap in Program::linkAttributes). They are owned by the Context’s inline PrivateState.
`Context` lifetime and `ShareGroup`
Multiple GL contexts can share one ANGLE ShareGroup, which owns the ShaderProgramManager and thus keeps programs alive even after one context is destroyed. A context’s Caps/Limitations die with the context, independent of the share group.
`Context::onDestroy` / `ensureNoPendingLink`
Teardown only joins link jobs for the currently bound program/pipeline; an unbound-but-still-linking program’s background job is not joined.

Root Cause Analysis

The violated invariant is that a background MainLinkTask must not outlive the storage it dereferences. MainLinkTask held const Caps &mCaps; and const Limitations &mLimitations; — raw references into the originating Context’s PrivateState. When that context is destroyed, its PrivateState (and the referenced Caps/Limitations) is freed. Because Context::onDestroy only forces mState.ensureNoPendingLink(this) for the bound program, a program that is linking but has been unbound (glUseProgram(0)) is skipped, so its worker thread is never joined. The program itself stays alive through a sharing context’s ShareGroup/ShaderProgramManager, so the worker keeps running and later reads mCaps/mLimitations (e.g. caps.maxVertexAttributes) from freed memory — a use-after-free on Context-owned state.

The patch changes the two members from references to by-value copies: const Caps mCaps; and const Limitations mLimitations; (src/libANGLE/Program.cpp), with a comment stating the copy is made “because the context that started the link task may get destroyed before the link job is finished.” Copying these small structs severs the worker’s dependence on the context’s lifetime: even if the originating Context is torn down mid-link, the job operates on its own snapshot. The author notes the caps/limitations relevant to link are effectively equal across a share group, so copying is semantically safe. A regression test ParallelShaderCompileTest.DestroyContextWhileLinkIsInProgress reproduces the scenario: create a shared context, start a link without binding the program, make the other context current, destroy the spawning context, and assert the link still succeeds with no error. The diff substantiates the advisory’s UAF class; it does not itself add thread-joining in onDestroy (the advisory’s alternative fix), relying instead on the by-value copy to make the dangling reference harmless.

Key insight
The background link job borrowed the capability structs by reference from the context that started it, but context teardown only joined jobs for bound programs — so an unbound program kept alive by a sibling share-group context left its worker reading freed Caps/Limitations.

Attack Path

  1. Compromised renderer Attacker drives the command buffer to the GPU process (precondition per advisory; IPC surface not in the diff — (inferred)).
  2. Create a shared second context Trigger a path (the advisory cites CopySharedImageINTERNAL lazily creating lazy_context_) so Context B shares Context A’s ShareGroup.
  3. Start a long background link On Context A, attach complex shaders and glLinkProgram, dispatching a MainLinkTask that captures references to Context A’s Caps/Limitations.
  4. Unbind the program glUseProgram(0) so the program is not current on Context A, ensuring onDestroy’s ensureNoPendingLink will skip it.
  5. Destroy Context A LoseContextCHROMIUM / destroy the command buffer. Context A (and its Caps) is freed, but the share group keeps the program and its running link job alive.
  6. UAF read The worker thread dereferences the freed mCaps/mLimitations (e.g. caps.maxVertexAttributes).
  7. Corruption / escape (inferred) With GPU-process heap spraying to control the freed Caps, forged values such as an inflated maxVertexAttributes bypass link-time bounds checks, yielding OOB reads/writes and, per the advisory, a renderer-to-GPU sandbox escape. Heap control and the OOB-to-escape chain are plausible but not shown by the diff.

Impact Assessment

The defect is in ANGLE running in the GPU process, reachable from a compromised renderer via shared-context command-buffer sequencing, and requires a lifetime race between link completion and context destruction (AC:H, consistent with the advisory). Confirmed by the diff: a use-after-free reading Context-owned Caps/Limitations from a background thread. Escalation to OOB read/write and sandbox escape is (inferred): it depends on controlling the freed allocation and on specific caps feeding bounds checks, which the diff does not demonstrate; ANGLE is a third-party library without MiraclePtr, so the freed memory is not quarantined, raising exploitability. Absent successful heap control the realized effect is a UAF read (likely crash).

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

Audit Directions

  • Other reference captures in async GL tasks
    Audit MainLinkTask, compile tasks, and any angle::Closure/worker that stores const T& to Context/PrivateState-owned data; such borrows are unsafe once the context can die mid-job.
  • Incomplete join on destroy
    Review Context::onDestroy and State::ensureNoPendingLink for other objects (pipelines, separable programs, transform-feedback) whose background jobs are only joined when bound.
  • Share-group lifetime skew
    Look for state the share group keeps alive (programs, shaders) that transitively references per-context storage; the share group outliving a context is the general hazard here.
  • Caps as a trust boundary
    Since forged caps bypass link-time bounds checks, audit all link/validation code that trusts caps.* for sizing or indexing, independent of this lifetime bug.
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