CVE-2026-17832
Overview
Files Changed
src/libANGLE/renderer/gl/egl/SyncEGL.cppsrc/libANGLE/renderer/gl/egl/SyncEGL.h
Patch
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
Original Bug Report
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:
- Thread A calls
eglCreateSync. Under the global lock,Display::createSyncallocates a predictable, sequentialSyncIDand invokesSyncEGL::initialize, which enqueues theUnlockedTailCallcapturing&sync(referencingSyncEGL::mSync). The pointer to theSyncobject is inserted intomSyncMapbefore releasing the global lock. - The global lock is released.
- Thread B calls
eglDestroySyncusing the sequentialSyncIDon 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 ofSync. - The destructor of
SyncdestroysSync::mFence(which is astd::unique_ptr<rx::EGLSyncImpl>, holding the underlyingSyncEGLinstance). - This immediately frees the heap-allocated
SyncEGLobject.
- In
- Thread A executes its pending tail calls. The lambda runs and executes
sync = egl->createSyncKHR(...), writing the returnedEGLSynchandle into the danglingsyncreference, which now points to the freedSyncEGL::mSyncfield 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.