Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Bug ClassUAF
Tracker517723319
Fix commit10bdaed1daa9 (angle/angle) +20/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • src/libANGLE/renderer/gl/egl/SyncEGL.cpp
  • src/libANGLE/renderer/gl/egl/SyncEGL.h
From 10bdaed1daa9e3083f1ccfa0cafb2185cb42791f Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Tue, 02 Jun 2026 11:16:03 -0400
Subject: [PATCH] EGL: Store SyncEGL::mSync in a shared_ptr.

SyncEGL uses tail calls for the expensive native calls. It is possible
that the SyncEGL object to be destructed while executing a tail call,
store mSync in a shared_ptr so that the callback can keep it alive until
it's finished.

Fixed: chromium:517723319
Change-Id: Ia5b74e3dbaa9204f382f402d541f7ce8bac072a1
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7893330
Commit-Queue: Geoff Lang <geofflang@chromium.org>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/renderer/gl/egl/SyncEGL.cpp b/src/libANGLE/renderer/gl/egl/SyncEGL.cpp
index 2ccc9fe..6469ba7 100644
--- a/src/libANGLE/renderer/gl/egl/SyncEGL.cpp
+++ b/src/libANGLE/renderer/gl/egl/SyncEGL.cpp
@@ -15,19 +15,22 @@
 namespace rx
 {
 
-SyncEGL::SyncEGL(const FunctionsEGL *egl) : mEGL(egl), mSync(EGL_NO_SYNC_KHR) {}
+SyncEGL::SyncEGL(const FunctionsEGL *egl) : mEGL(egl)
+{
+    mSync = std::make_shared<EGLSync>(EGL_NO_SYNC_KHR);
+}
 
 SyncEGL::~SyncEGL()
 {
-    ASSERT(mSync == EGL_NO_SYNC_KHR);
+    ASSERT(mSync && *mSync == EGL_NO_SYNC_KHR);
 }
 
 void SyncEGL::onDestroy(const egl::Display *display)
 {
-    if (mSync != EGL_NO_SYNC_KHR)
+    if (*mSync != EGL_NO_SYNC_KHR)
     {
         egl::Display::GetCurrentThreadUnlockedTailCall()->add(
-            [egl = mEGL, sync = mSync](void *resultOut) {
+            [egl = mEGL, sync = *mSync](void *resultOut) {
                 EGLBoolean result = egl->destroySyncKHR(sync);
                 if (resultOut)
                 {
@@ -36,7 +39,7 @@
                     *static_cast<EGLBoolean *>(resultOut) = result;
                 }
             });
-        mSync = EGL_NO_SYNC_KHR;
+        *mSync = EGL_NO_SYNC_KHR;
     }
 }
 
@@ -59,12 +62,12 @@
     nativeAttribs.push_back(EGL_NONE);
 
     egl::Display::GetCurrentThreadUnlockedTailCall()->add(
-        [egl = mEGL, &sync = mSync, type, attribs = nativeAttribs](void *resultOut) {
-            sync = egl->createSyncKHR(type, attribs.data());
+        [egl = mEGL, syncRef = mSync, type, attribs = nativeAttribs](void *resultOut) {
+            *syncRef = egl->createSyncKHR(type, attribs.data());
 
             // If sync creation failed, force the return value of eglCreateSync to EGL_NO_SYNC. This
             // won't delete this sync object but a driver error is unexpected at this point.
-            if (sync == EGL_NO_SYNC_KHR)
+            if (*syncRef == EGL_NO_SYNC_KHR)
             {
                 ERR() << "eglCreateSync failed with " << gl::FmtHex(egl->getError());
                 *static_cast<EGLSync *>(resultOut) = EGL_NO_SYNC_KHR;
@@ -80,12 +83,12 @@
                                EGLTime timeout,
                                EGLint *outResult)
 {
-    ASSERT(mSync != EGL_NO_SYNC_KHR);
+    ASSERT(*mSync != EGL_NO_SYNC_KHR);
 
     // If we need to perform a CPU wait don't set the resultOut parameter passed into the
     // method, instead set the parameter passed into the unlocked tail call.
     egl::Display::GetCurrentThreadUnlockedTailCall()->add(
-        [egl = mEGL, sync = mSync, flags, timeout](void *resultOut) {
+        [egl = mEGL, sync = *mSync, flags, timeout](void *resultOut) {
             *static_cast<EGLint *>(resultOut) = egl->clientWaitSyncKHR(sync, flags, timeout);
         });
 
@@ -96,10 +99,10 @@
                                const gl::Context *context,
                                EGLint flags)
 {
-    ASSERT(mSync != EGL_NO_SYNC_KHR);
+    ASSERT(*mSync != EGL_NO_SYNC_KHR);
 
     egl::Display::GetCurrentThreadUnlockedTailCall()->add(
-        [egl = mEGL, sync = mSync, flags](void *resultOut) {
+        [egl = mEGL, sync = *mSync, flags](void *resultOut) {
             *static_cast<EGLBoolean *>(resultOut) = egl->waitSyncKHR(sync, flags);
         });
 
@@ -108,8 +111,8 @@
 
 egl::Error SyncEGL::getStatus(const egl::Display *display, EGLint *outStatus)
 {
-    ASSERT(mSync != EGL_NO_SYNC_KHR);
-    EGLBoolean result = mEGL->getSyncAttribKHR(mSync, EGL_SYNC_STATUS_KHR, outStatus);
+    ASSERT(*mSync != EGL_NO_SYNC_KHR);
+    EGLBoolean result = mEGL->getSyncAttribKHR(*mSync, EGL_SYNC_STATUS_KHR, outStatus);
 
     if (result == EGL_FALSE)
     {
@@ -121,8 +124,8 @@
 
 egl::Error SyncEGL::dupNativeFenceFD(const egl::Display *display, EGLint *result) const
 {
-    ASSERT(mSync != EGL_NO_SYNC_KHR);
-    *result = mEGL->dupNativeFenceFDANDROID(mSync);
+    ASSERT(*mSync != EGL_NO_SYNC_KHR);
+    *result = mEGL->dupNativeFenceFDANDROID(*mSync);
     if (*result == EGL_NO_NATIVE_FENCE_FD_ANDROID)
     {
         return egl::Error(mEGL->getError(), "eglDupNativeFenceFDANDROID failed");
diff --git a/src/libANGLE/renderer/gl/egl/SyncEGL.h b/src/libANGLE/renderer/gl/egl/SyncEGL.h
index b63dd3e..44e8952 100644
--- a/src/libANGLE/renderer/gl/egl/SyncEGL.h
+++ b/src/libANGLE/renderer/gl/egl/SyncEGL.h
@@ -48,7 +48,7 @@
   private:
     const FunctionsEGL *mEGL;
 
-    EGLSync mSync;
+    std::shared_ptr<EGLSync> mSync;
 };
 
 }  // namespace rx
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free write in SyncEGL::initialize

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) write vulnerability exists in ANGLE’s SyncEGL::initialize due to a by-reference capture of mSync inside a deferred UnlockedTailCall lambda. Because the lambda executes after the global EGL lock is released, a concurrent thread can destroy the sync object and free the underlying SyncEGL instance prior to the lambda running. When the deferred lambda eventually executes, it attempts to write the driver-returned EGLSync handle into the freed memory of the dangling mSync reference.

Affected files:

  • third_party/angle/src/libANGLE/renderer/gl/egl/SyncEGL.cpp

Estimated timestamp from git blame: 2024-04-23

Description

In ANGLE’s OpenGL-on-EGL backend, a potential Use-After-Free (UAF) write vulnerability exists within SyncEGL::initialize (third_party/angle/src/libANGLE/renderer/gl/egl/SyncEGL.cpp).

The issue lies in how the UnlockedTailCall lambda is constructed:

    egl::Display::GetCurrentThreadUnlockedTailCall()->add(
        [egl = mEGL, &sync = mSync, type, attribs = nativeAttribs](void *resultOut) {
            sync = egl->createSyncKHR(type, attribs.data());
            ...

Here, the lambda captures mSync (a member variable of the SyncEGL instance) by reference (&sync = mSync). This lambda is queued to be executed as an unlocked tail call after the global EGL lock is released.

Race Condition & Lifetime Analysis

During the window of time after the global EGL lock is released and before the thread’s unlocked tail calls are executed, a concurrent thread can call eglDestroySync on the newly allocated sync object’s ID:

  1. Thread A calls eglCreateSync. Under the global lock, Display::createSync allocates a predictable, sequential SyncID and invokes SyncEGL::initialize, which enqueues the UnlockedTailCall capturing &sync (referencing SyncEGL::mSync). The pointer to the Sync object is inserted into mSyncMap before releasing the global lock.
  2. The global lock is released.
  3. Thread B calls eglDestroySync using the sequential SyncID on another thread:
    • In Display::destroySyncImpl, if the per-type synchronization pool (mSyncPools) is full, syncs->erase() is called.
    • This destroys the std::unique_ptr<Sync>, which invokes the destructor of Sync.
    • The destructor of Sync destroys Sync::mFence (which is a std::unique_ptr<rx::EGLSyncImpl>, holding the underlying SyncEGL instance).
    • This immediately frees the heap-allocated SyncEGL object.
  4. Thread A executes its pending tail calls. The lambda runs and executes sync = egl->createSyncKHR(...), writing the returned EGLSync handle into the dangling sync reference, which now points to the freed SyncEGL::mSync field inside the deleted memory block.

Note that even if the idle pool is not full, this can result in double-creation, driver handle leaks, and state confusion if the wrapper is recycled and re-initialized by a third thread prior to the delayed tail call running.

Suggested Remediation

To resolve this lifetime vulnerability, avoid capturing raw member references by reference in deferred tail calls whose parent objects can be asynchronously deleted.

One potential fix is to manage the lifetime of the deferred sync creation via a shared state block (e.g., utilizing a ref-counted state struct or std::shared_ptr) that outlives both the SyncEGL object and the tail call, or to safely perform the driver-level sync creation synchronously prior to releasing the global EGL lock when deferred execution cannot guarantee lifetime safety.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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