CVE-2026-9878
Overview
Background
- ANGLE
- Almost Native Graphics Layer Engine, the library that translates OpenGL ES / WebGL calls onto native backends (here Vulkan) inside the Chrome GPU process.
- `ProgramExecutable`
- the object holding a linked program’s compiled state, owned by
ProgramviamState.mExecutable, and referenced by asynchronous post-link tasks. - Post-link tasks
- asynchronous jobs auto-scheduled after a successful link; some are deferred to the main thread in
getResult(), where they touch the program’s link state throughContextaccess. - UAF
- use-after-free, where memory is read or written after it has been freed, corrupting the GPU process’s state.
Root Cause Analysis
When Program linking begins, ANGLE may schedule asynchronous post-link tasks that hold references to the program’s ProgramExecutable / link state. In Program::resolveLinkImpl, the link result is finalized, but deferred getResult() work can still fail after those tasks have already been posted — practically only under out-of-memory pressure at that exact moment. On this failure path the old code immediately called mState.mExecutable->reset() and returned, freeing the executable state while post-link tasks were potentially still running or pending. Those tasks then executed against the now-freed executable, producing a use-after-free in the GPU process. The tasks’ lifetime was implicitly assumed to be bounded by the success path only, so the failure path never synchronized with them.
The fix inserts waitForPostLinkTasks(context) before the reset, guaranteeing no task outlives the state it references.
ProgramExecutable out from under still-live asynchronous tasks. Cleanup must synchronize with every asynchronous consumer of an object before freeing it, on all paths — including error paths.Attack Path
- Trigger a program link Craft WebGL/WebGL2 content that links a shader program, causing ANGLE to schedule asynchronous post-link tasks on the Vulkan backend.
- Force the OOM window
Apply memory pressure so a deferred
getResult()operation fails at the precise moment after post-link tasks have been posted. - Enter the failure path
The link is resolved as failed, driving
resolveLinkImplinto the pre-fix branch that resetsmExecutable. - Free while tasks live
mState.mExecutable->reset()frees the executable state without waiting for the still-running post-link tasks. - Race the dangling task A surviving post-link task dereferences the freed executable state, yielding a use-after-free in the GPU process.
Impact Assessment
ProgramExecutable state in the GPU process, which could in principle lead to memory corruption and compromise of that process from WebGL-reachable content. In practice it is hard to trigger and largely theoretical: it requires a link failure through a deferred getResult() path that realistically only occurs on an out-of-memory condition inside a narrow window, giving an attacker little control over timing. The narrow OOM-window precondition substantially limits real-world exploitability.Files Changed
src/libANGLE/Program.cpp
Audit Directions
- Error-path resource teardownAudit every branch that frees or resets state referenced by async tasks (
reset(), destructors,release()) to confirm the failure/error path joins those tasks, not just the success path. - Async task lifetime vs. ownerLook for asynchronous jobs holding raw references to objects (
ProgramExecutable,Context-bound state) that may be destroyed while the task is in flight, especially where a “wait” call is present on one path but missing on another. - Deferred `getResult()` failure handlingTrace fallible deferred operations (main-thread
getResult(), OOM-sensitive allocations) that run after scheduling work, and verify each failure return path synchronizes with all previously posted tasks before cleanup.
Patch
From 648b530b4470442fdc10b392aa3008dc61e9a7fd Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Wed, 08 Apr 2026 11:22:25 -0400 Subject: [PATCH] Vulkan: Wait for post-link tasks even if link fails The post-link tasks are automatically scheduled at the end of link, if it succeeds. So initially, the idea was that if the link fails, there are no post-link tasks. However, it was necessary to defer some tasks to the main thread (with access to Context), which are done in "getResult()" after waiting for the link tasks to finish. This creates a situation where post-link tasks are posted but link may still fail. This is a theoretical fix for when those functions in "getResult()" fail. This is practically never, as it requires OOM to happen at exactly that moment, and so is untestable without a fault-injection infrastructure. Bug: chromium:499054245 Change-Id: I3890026bbdf2d8987d5bc4fc1bebf2950592c5a7 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7736777 Commit-Queue: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/Program.cpp b/src/libANGLE/Program.cpp index ad1f558..add1e25 100644 --- a/src/libANGLE/Program.cpp +++ b/src/libANGLE/Program.cpp @@ -1256,6 +1256,7 @@ // The above means that it's ok for ANGLE to reset the executable here, but it *may* be // helpful to applications if it doesn't. We do reset it however, the info log should // already have enough debug information for the application. + waitForPostLinkTasks(context); mState.mExecutable->reset(); return; }
Original Bug Report
GPU Process UAF: Unsynchronized background tasks during ANGLE link failure
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 security team.
Overview: A Use-After-Free (UAF) vulnerability exists in ANGLE’s Vulkan backend due to improper cleanup of background pipeline warm-up tasks. If a shader link fails, the frontend clears its tracking list of background tasks without waiting for them to finish, causing the backend to skip synchronization during program destruction. An orphaned background task will later dereference the freed ProgramExecutableVk object, potentially allowing for arbitrary memory writes and RCE in the GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cppthird_party/angle/src/libANGLE/ProgramExecutable.cppthird_party/angle/src/libANGLE/Program.cppthird_party/angle/src/libANGLE/renderer/vulkan/ProgramVk.cppthird_party/angle/src/common/WorkerThread.cpp
Estimated timestamp from git blame: 2025-04-19
Summary
A potential Use-After-Free (UAF) vulnerability in the ANGLE Vulkan backend allows for arbitrary memory writes and code execution in the GPU process. The issue stems from a failure to synchronize background ‘warm-up’ tasks when a shader program link fails. This results in worker threads holding raw pointers to a ProgramExecutableVk object that has been subsequently destroyed.
Technical Details
When a program link is initiated in ANGLE, the Vulkan backend schedules asynchronous WarmUpGraphicsTask objects to pre-populate the pipeline cache (ProgramExecutableVk.cpp:938). These tasks are designed to run in parallel on a worker thread pool and capture a raw this pointer to the ProgramExecutableVk instance (ProgramExecutableVk.cpp:446). The frontend schedules these tasks and stores waitable events in mState.mExecutable->mPostLinkSubTasks (Program.cpp:586).
If the link process fails after these tasks have been scheduled (for example, due to an Out-Of-Memory error or unsynchronized race condition in initializeDescriptorPools during LinkTaskVk::getResult()), the frontend’s Program::resolveLinkImpl observes the failure and cleans up by calling mState.mExecutable->reset() (Program.cpp:1259).
This reset() call explicitly clears the vectors tracking the background tasks:
// third_party/angle/src/libANGLE/ProgramExecutable.cpp
void ProgramExecutable::reset()
{
// ...
mPostLinkSubTasks.clear();
mPostLinkSubTaskWaitableEvents.clear();
}
Crucially, this clearing happens without blocking or waiting for the background thread pool to finish executing the tasks. The tasks are thus orphaned.
When a subsequent link or destruction of the program occurs, ANGLE attempts to prevent UAFs in ProgramExecutableVk::resetLayout() by calling waitForPostLinkTasksImpl(). However, this safety check is ineffective because it relies on the frontend’s task vector, which was just cleared:
// third_party/angle/src/libANGLE/renderer/vulkan/ProgramExecutableVk.cpp
void ProgramExecutableVk::waitForPostLinkTasksImpl(ContextVk *contextVk)
{
const std::vector<std::shared_ptr<rx::LinkSubTask>> &postLinkSubTasks =
mExecutable->getPostLinkSubTasks();
if (postLinkSubTasks.empty())
{
return; // Returns immediately because the vector was cleared by reset()
}
// ...
}
Consequently, the ProgramExecutableVk object is freed while a WarmUpGraphicsTask is still running or queued in the background worker pool.
Potential Exploitation Steps
(Note: These are suggested steps based on static analysis; a working Proof of Concept has not yet been executed.)
- The attacker triggers a shader link via WebGL/WebGPU, causing ANGLE to schedule
WarmUpGraphicsTaskbackground tasks. - The attacker induces a link failure on the main thread (e.g., via memory pressure or a race condition on
MetaDescriptorPool), causing the frontend to clear the post-link task vectors and orphan the background tasks. - The attacker deletes the shader program, freeing the
ProgramExecutableVkobject (~16KB allocation). - The attacker reclaims the freed 16KB memory chunk using another WebGL API, such as
glProgramBinary, allocating a controlled buffer of the exact same size. - The attacker populates this buffer with a forged
ProgramExecutableVkstructure, carefully controlling internal fields, such asmCompleteGraphicsPipelines(an array ofstd::unordered_mapobjects). - The orphaned
WarmUpGraphicsTaskeventually executes in the background. It callsmExecutableVk->warmUpGraphicsPipelineCache(...), dereferencing the attacker-controlled memory. - The task attempts to insert a pipeline into the forged
std::unordered_map. By manipulating the internal pointers of the map, the attacker achieves an arbitrary memory write primitive. - The attacker uses the arbitrary write to overwrite function pointers or virtual tables, leading to Remote Code Execution (RCE) in the GPU process.
Suggested Fix
In ProgramExecutable::reset(), wait for the post-link tasks to complete before clearing the mPostLinkSubTasks and mPostLinkSubTaskWaitableEvents vectors. Alternatively, ensure that ProgramExecutableVk::destroy() explicitly cancels or waits on the worker pool tasks regardless of the state of the frontend vectors.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.