Low watch ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Sub-componentInternals>GPU>ANGLE
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Root-cause line numbers (:52, :70-81) match the d2e49812^ HandleAllocator.cpp, not e156412^ (ASSERT is near line 43 there); unlabeled but consistent with Background. One false claim about create* wrappers raising errors.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker452209503
Fix commite15641282324 (angle/angle) +424/-218
Related commitsd2e49812d968 earlier hardening · angle/angle
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGlitchers BoB 14th.
Reported2025-10-15 (per vendor advisory)
Fix landed2025-11-12 (public git)
Disclosed2026-01-13
Patch gap62 days — fix public in source before the advisory

Background

`gl::HandleAllocator`
ANGLE’s GL object-name allocator, which hands out names from a list of free ranges (mUnallocatedList) and a heap of released names (mReleasedList) (src/libANGLE/HandleAllocator.cpp:50-90 at d2e49812^).
`HandleRange`
A [begin, end] range of not-yet-issued handle values; the default allocator starts with one range HandleRange(1, mMaxValue), where mMaxValue is std::numeric_limits<GLuint>::max() (src/libANGLE/HandleAllocator.cpp:26-33 at d2e49812^).
`ResourceManagerBase`
The base class of ANGLE’s per-type resource managers (buffers, textures, shaders/programs, …); before the fix it default-constructed its mHandleAllocator (removed line ResourceManagerBase::ResourceManagerBase() : mRefCount(1) {}).
`IMPLEMENTATION_MAX_OBJECT_HANDLES`
A new constant, 1 << 24, added to src/libANGLE/Constants.h by the fix as the per-type maximum number of simultaneous handles.

Root Cause Analysis

Re-analyzed on 2026-10-03. The fix commit e15641282324 was always correctly mapped, but the earlier analysis was written from a truncated copy of its diff that left out the changed product source (Context.cpp, Display.cpp and the other libANGLE call sites) and showed only build/test files. This analysis is written from the complete diff.

HandleAllocator::allocate() did not handle the case where both free lists were empty. The only guard was a debug ASSERT(!mUnallocatedList.empty() || !mReleasedList.empty()) (src/libANGLE/HandleAllocator.cpp:52 at d2e49812^; the line numbers in this paragraph refer to that revision, which d2e49812 only touched with CheckedNumeric wrapping). If mReleasedList is empty, the code takes auto listIt = mUnallocatedList.begin() (:70) and, without checking it against end(), reads listIt->begin (:72) and listIt->end (:75). It then either calls mUnallocatedList.erase(listIt) (:77) or increments listIt->begin (:81). After the last range is used up and erased at :77, the next allocation dereferences the begin iterator of an empty container. That reads, and may then erase or write, the storage of an element that was already destroyed. This matches the reporter’s description of re-reading a destructed HandleRange (the container type is not shown, so the vector-storage detail is (inferred)). Every resource creator returned allocate()’s result directly with no failure path, for example ShaderProgramID handle = ShaderProgramID{mHandleAllocator.allocate()}; in ShaderProgramManager::createShader and Context::createShader (src/libANGLE/Context.cpp:1137-1142 at e156412^).

The fix commit e156412823246a4d68a7e39f65995f9a5ce6c2e4 adds if (mUnallocatedList.empty()) { return false; } and changes the signature to bool allocate(GLuint *outId). It threads that result through every manager, the Context, Display and GLES1Renderer creators, which now raise GL_OUT_OF_MEMORY/EGL_BAD_ALLOC with kHandleExhaustion. It also caps each allocator at IMPLEMENTATION_MAX_OBJECT_HANDLES so exhaustion becomes a reachable, handled condition. The earlier commit d2e49812 (same bug) only wraps the begin++ and the release() arithmetic in angle::CheckedNumeric. It adds no emptiness check, so it is hardening and not the fix.

Key insight
allocate() assumed the free-range list was never empty and relied on a debug-only ASSERT, so exhaustion led to dereferencing an empty container’s iterator. The fix makes exhaustion an explicit false return that callers turn into a GL/EGL out-of-memory error, and lowers the limit so that path is tested rather than theoretical.

Attack Path

  1. Create many objects of one type Content repeatedly calls a creation entry point such as createShader (the reporter uses WebGL gl.createShader). Each call reaches ShaderProgramManager::createShader and then HandleAllocator::allocate() (src/libANGLE/Context.cpp:1137-1142). The WebGL-to-ANGLE plumbing in Chrome is (inferred).
  2. Exhaust the free range Once the single HandleRange(1, UINT_MAX) is used up, allocate() erases it at src/libANGLE/HandleAllocator.cpp:75-77, leaving mUnallocatedList empty. With the pre-fix full GLuint range this needs about 2^32 live objects; the reporter shrank the range to reproduce it.
  3. Allocate once more With mReleasedList also empty, allocate() dereferences mUnallocatedList.begin() on an empty container (:70-72), reads stale begin/end, and then calls erase or begin++ on it (:75-81).
  4. Memory-safety violation The returned handle comes from freed or out-of-bounds memory and the list bookkeeping is corrupted. The reporter observed an ASan container-overflow read in Firefox’s copy of ANGLE; anything beyond that is (inferred).

Impact Assessment

An attacker who can create a very large number of GL objects of one type, for example through WebGL (inferred for Chrome), triggers a use-after-free or out-of-bounds read and possibly a write in ANGLE’s handle bookkeeping, in the process that hosts ANGLE (the GPU process, inferred). The precondition is exhausting about 4 billion handles of one type, which the reporter could not reach without modifying the source; that is consistent with the advisory’s low severity. The advisory does not report in-the-wild exploitation.

Files Changed

  • scripts/code_generation_hashes/GL_EGL_entry_points.json
  • scripts/generate_entry_points.py
  • src/libANGLE/Constants.h
  • src/libANGLE/Context.cpp
  • src/libANGLE/Context.h
  • src/libANGLE/Context_gles_3_0_autogen.h
  • src/libANGLE/Display.cpp
  • src/libANGLE/ErrorStrings.h
  • src/libANGLE/GLES1Renderer.cpp
  • src/libANGLE/HandleAllocator.cpp
  • src/libANGLE/HandleAllocator.h
  • src/libANGLE/HandleAllocator_unittest.cpp
  • src/libANGLE/PixelLocalStorage.cpp
  • src/libANGLE/ResourceManager.cpp
  • src/libANGLE/ResourceManager.h
  • src/libANGLE/ResourceManager_unittest.cpp
  • src/libANGLE/State.cpp
  • src/libANGLE/State.h
  • src/libANGLE/capture/FrameCapture.cpp
  • src/libANGLE/context_private_call.inl.h
  • src/libANGLE/context_private_call_autogen.h
  • src/libANGLE/validationES2.cpp
  • src/libANGLE/validationES3.cpp
  • src/libANGLE/validationES3_autogen.h
  • src/libANGLE/validationESEXT_autogen.h
  • src/libGLESv2/entry_points_gles_3_0_autogen.cpp
  • src/libGLESv2/entry_points_gles_ext_autogen.cpp

Audit Directions

  • Debug-only invariants on allocators
    Look for ASSERT-only emptiness or bounds checks in front of begin(), front() or back() on containers in release builds, especially in ID/name allocators that untrusted content can drive.
  • Failure paths after a fallible allocate
    CoherentBufferTracker::canProtectDirectly only logs ERR() on failure and then continues to bindBuffer with a bufferId that was never assigned. Check other new callers for the same pattern of continuing after failure.
  • Off-by-one at the cap
    Ranges are inclusive (HandleRange(1, mMaxValue)), but release() now discards handle >= mMaxValue, so the value mMaxValue itself is never reclaimed. Confirm this is only a minor leak and cannot cause a duplicate name when combined with reserve().
diff --git a/scripts/code_generation_hashes/GL_EGL_entry_points.json b/scripts/code_generation_hashes/GL_EGL_entry_points.json
index 35ff7f4..56e77d6 100644
--- a/scripts/code_generation_hashes/GL_EGL_entry_points.json
+++ b/scripts/code_generation_hashes/GL_EGL_entry_points.json
@@ -6,7 +6,7 @@
   "scripts/entry_point_packed_gl_enums.json":
     "cc42c7d7f13b0ba7966c84629f735299",
   "scripts/generate_entry_points.py":
-    "606e148f063cbc5db2e41c49bd72b2de",
+    "39414cc2beac01698144c9c04576e055",
   "scripts/gl_angle_ext.xml":
     "da4ecccdd77635f1b0e9d4664f856706",
   "scripts/registry_xml.py":
@@ -24,7 +24,7 @@
   "src/libANGLE/Context_gles_2_0_autogen.h":
     "63f297a21ff3d1ef014595d70e7f94ae",
   "src/libANGLE/Context_gles_3_0_autogen.h":
-    "32a1ba3b4db9db343fe9b43a0d859076",
+    "6f7e3f65e013135786455ea33b197e07",
   "src/libANGLE/Context_gles_3_1_autogen.h":
     "2b81a1dd0d3109090afc073417e0667c",
   "src/libANGLE/Context_gles_3_2_autogen.h":
@@ -64,7 +64,7 @@
   "src/libANGLE/capture/capture_gles_ext_autogen.h":
     "f13556b6b8aa39a0e0ce1e34ca2aa8a6",
   "src/libANGLE/context_private_call_autogen.h":
-    "3bed70b648c7cb8b0fd9e6923629897c",
+    "82434f431a5851ec2168f8517329de82",
   "src/libANGLE/validationCL_autogen.h":
     "034b45381592c163c4f7aaa9325223b3",
   "src/libANGLE/validationEGL_autogen.h":
@@ -78,9 +78,9 @@
   "src/libANGLE/validationES32_autogen.h":
     "67429f628946feab206d1ce9bdcf6fe9",
   "src/libANGLE/validationES3_autogen.h":
-    "c6150e9073885a2a8344c40ed0be0fb0",
+    "127ed661a38f20de9ba13fa989ce05d8",
   "src/libANGLE/validationESEXT_autogen.h":
-    "32c253b727cecf45b3100bd89c85a778",
+    "ed490c09b8abef586b011972d534edf2",
   "src/libEGL/libEGL_autogen.cpp":
     "77d82db4a45c2f08108929ef7fe8b698",
   "src/libEGL/libEGL_autogen.def":
@@ -118,7 +118,7 @@
   "src/libGLESv2/entry_points_gles_2_0_autogen.h":
     "691c60c2dfed9beca68aa1f32aa2c71b",
   "src/libGLESv2/entry_points_gles_3_0_autogen.cpp":
-    "53198b435a1983717987cc292fb89f93",
+    "02476248c56dcf3e6487883c1ccefd93",
   "src/libGLESv2/entry_points_gles_3_0_autogen.h":
     "4ac2582759cdc6a30f78f83ab684d555",
   "src/libGLESv2/entry_points_gles_3_1_autogen.cpp":
@@ -130,7 +130,7 @@
   "src/libGLESv2/entry_points_gles_3_2_autogen.h":
     "647f932a299cdb4726b60bbba059f0d2",
   "src/libGLESv2/entry_points_gles_ext_autogen.cpp":
-    "0c46de9633a4aa0e13f63198d77cb211",
+    "44c2341f03e8d6315c28b3de19b9d579",
   "src/libGLESv2/entry_points_gles_ext_autogen.h":
     "9edbc6d2857cbe0349a5b7c70bc22ed8",
   "src/libGLESv2/libGLESv2_autogen.cpp":
diff --git a/scripts/generate_entry_points.py b/scripts/generate_entry_points.py
index ca35374..f40273d 100755
--- a/scripts/generate_entry_points.py
+++ b/scripts/generate_entry_points.py
@@ -106,8 +106,6 @@
     'glEnablei',
     'glEnableVertexAttribArray',
     'glFrontFace',
-    'glGenVertexArrays',
-    'glGenVertexArraysOES',
     'glHint',
     'glIsEnabled',
     'glIsEnabledi',
diff --git a/src/libANGLE/Constants.h b/src/libANGLE/Constants.h
index 36a37dd..7b49653 100644
--- a/src/libANGLE/Constants.h
+++ b/src/libANGLE/Constants.h
@@ -105,6 +105,9 @@
     // TODO (anglebug.com/42266906): Implement support for multiple layers
     IMPLEMENTATION_MAX_NUM_LAYERS   = 1,
     IMPLEMENTATION_MAX_FOCAL_POINTS = 2,
+
+    // Maximum simultaneous handles of an object type.
+    IMPLEMENTATION_MAX_OBJECT_HANDLES = 1 << 24,
 };
 
 namespace limits
diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index e6a1d4f..b1b5310 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -7,6 +7,7 @@
 // Context.cpp: Implements the gl::Context class, managing all GL state and performing
 // rendering operations. It is the GLES2 specific implementation of EGLContext.
 
+#include "common/entry_points_enum_autogen.h"
 #ifdef UNSAFE_BUFFERS_BUILD
 #    pragma allow_unsafe_buffers
 #endif
@@ -700,6 +701,9 @@
       mLabel(nullptr),
       mCompiler(),
       mConfig(config),
+      mFenceNVHandleAllocator(IMPLEMENTATION_MAX_OBJECT_HANDLES),
+      mQueryHandleAllocator(IMPLEMENTATION_MAX_OBJECT_HANDLES),
+      mTransformFeedbackHandleAllocator(IMPLEMENTATION_MAX_OBJECT_HANDLES),
       mHasBeenCurrent(false),
       mSurfacelessSupported(displayExtensions.surfacelessContext),
       mCurrentDrawSurface(static_cast<egl::Surface *>(EGL_NO_SURFACE)),
@@ -1124,52 +1128,71 @@
     mErrors.validationError(entryPoint, GL_CONTEXT_LOST, err::kContextLost);
 }
 
-BufferID Context::createBuffer()
+void Context::handleExhaustionError(angle::EntryPoint entryPoint)
 {
-    return mState.mBufferManager->createBuffer();
+    mErrors.validationError(entryPoint, GL_OUT_OF_MEMORY, err::kHandleExhaustion);
+}
+
+bool Context::createBuffer(BufferID *outBuffer)
+{
+    return mState.mBufferManager->createBuffer(outBuffer);
 }
 
 GLuint Context::createProgram()
 {
-    return mState.mShaderProgramManager->createProgram(mImplementation.get()).value;
+    ShaderProgramID id;
+    if (!mState.mShaderProgramManager->createProgram(mImplementation.get(), &id))
+    {
+        handleExhaustionError(angle::EntryPoint::GLCreateProgram);
+        return 0;
+    }
+    return id.value;
 }
 
 GLuint Context::createShader(ShaderType type)
 {
-    return mState.mShaderProgramManager
-        ->createShader(mImplementation.get(), mState.getLimitations(), type)
-        .value;
+    ShaderProgramID id;
+    if (!mState.mShaderProgramManager->createShader(mImplementation.get(), mState.getLimitations(),
+                                                    type, &id))
+    {
+        handleExhaustionError(angle::EntryPoint::GLCreateShader);
+        return 0;
+    }
+    return id.value;
 }
 
-TextureID Context::createTexture()
+bool Context::createTexture(TextureID *outTexture)
 {
-    return mState.mTextureManager->createTexture();
+    return mState.mTextureManager->createTexture(outTexture);
 }
 
-RenderbufferID Context::createRenderbuffer()
+bool Context::createRenderbuffer(RenderbufferID *outRenderbuffer)
 {
-    return mState.mRenderbufferManager->createRenderbuffer();
+    return mState.mRenderbufferManager->createRenderbuffer(outRenderbuffer);
 }
 
 // Returns an unused framebuffer name
-FramebufferID Context::createFramebuffer()
+bool Context::createFramebuffer(FramebufferID *outFramebuffer)
 {
-    return mState.mFramebufferManager->createFramebuffer();
+    return mState.mFramebufferManager->createFramebuffer(outFramebuffer);
 }
 
 void Context::genFencesNV(GLsizei n, FenceNVID *fences)
 {
     for (int i = 0; i < n; i++)
     {
-        GLuint handle = mFenceNVHandleAllocator.allocate();
-        mFenceNVMap.assign({handle}, new FenceNV(mImplementation.get()));
-        fences[i] = {handle};
+        if (!mFenceNVHandleAllocator.allocate(&fences[i].value))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenFencesNV);
+            return;
+        }
+        mFenceNVMap.assign(fences[i], new FenceNV(mImplementation.get()));
     }
 }
 
-ProgramPipelineID Context::createProgramPipeline()
+bool Context::createProgramPipeline(ProgramPipelineID *outProgramPipeline)
 {
-    return mState.mProgramPipelineManager->createProgramPipeline();
+    return mState.mProgramPipelineManager->createProgramPipeline(outProgramPipeline);
 }
 
 GLuint Context::createShaderProgramv(ShaderType type, GLsizei count, const GLchar *const *strings)
@@ -1224,14 +1247,14 @@
     return 0u;
 }
 
-MemoryObjectID Context::createMemoryObject()
+bool Context::createMemoryObject(MemoryObjectID *outMemoryObject)
 {
-    return mState.mMemoryObjectManager->createMemoryObject(mImplementation.get());
+    return mState.mMemoryObjectManager->createMemoryObject(mImplementation.get(), outMemoryObject);
 }
 
-SemaphoreID Context::createSemaphore()
+bool Context::createSemaphore(SemaphoreID *outSemaphore)
 {
-    return mState.mSemaphoreManager->createSemaphore(mImplementation.get());
+    return mState.mSemaphoreManager->createSemaphore(mImplementation.get(), outSemaphore);
 }
 
 void Context::deleteBuffer(BufferID bufferName)
@@ -7090,7 +7113,11 @@
 {
     for (int i = 0; i < n; i++)
     {
-        buffers[i] = createBuffer();
+        if (!createBuffer(&buffers[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenBuffers);
+            return;
+        }
     }
 }
 
@@ -7098,7 +7125,11 @@
 {
     for (int i = 0; i < n; i++)
     {
-        framebuffers[i] = createFramebuffer();
+        if (!createFramebuffer(&framebuffers[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenFramebuffers);
+            return;
+        }
     }
 }
 
@@ -7106,7 +7137,11 @@
 {
     for (int i = 0; i < n; i++)
     {
-        renderbuffers[i] = createRenderbuffer();
+        if (!createRenderbuffer(&renderbuffers[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenRenderbuffers);
+            return;
+        }
     }
 }
 
@@ -7114,7 +7149,11 @@
 {
     for (int i = 0; i < n; i++)
     {
-        textures[i] = createTexture();
+        if (!createTexture(&textures[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenTextures);
+            return;
+        }
     }
 }
 
@@ -7683,9 +7722,12 @@
 {
     for (GLsizei i = 0; i < n; i++)
     {
-        QueryID handle = QueryID{mQueryHandleAllocator.allocate()};
-        mQueryMap.assign(handle, nullptr);
-        ids[i] = handle;
+        if (!mQueryHandleAllocator.allocate(&ids[i].value))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenQueries);
+            return;
+        }
+        mQueryMap.assign(ids[i], nullptr);
     }
 }
 
@@ -7795,9 +7837,25 @@
 {
     for (int i = 0; i < n; i++)
     {
-        TransformFeedbackID transformFeedback = {mTransformFeedbackHandleAllocator.allocate()};
-        mTransformFeedbackMap.assign(transformFeedback, nullptr);
-        ids[i] = transformFeedback;
+        if (!mTransformFeedbackHandleAllocator.allocate(&ids[i].value))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenTransformFeedbacks);
+            return;
+        }
+        mTransformFeedbackMap.assign(ids[i], nullptr);
+    }
+}
+
+void Context::genVertexArrays(GLsizei n, VertexArrayID *ids)
+{
+    PrivateState *privState = getMutablePrivateState();
+    for (int i = 0; i < n; i++)
+    {
+        if (!privState->allocateVertexID(&ids[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenVertexArrays);
+            return;
+        }
     }
 }
 
@@ -7932,7 +7990,13 @@
 
 GLsync Context::fenceSync(GLenum condition, GLbitfield flags)
 {
-    SyncID syncHandle = mState.mSyncManager->createSync(mImplementation.get());
+    SyncID syncHandle;
+    if (!mState.mSyncManager->createSync(mImplementation.get(), &syncHandle))
+    {
+        handleExhaustionError(angle::EntryPoint::GLFenceSync);
+        return nullptr;
+    }
+
     Sync *syncObject  = getSync(syncHandle);
     if (syncObject->set(this, condition, flags) == angle::Result::Stop)
     {
@@ -8006,7 +8070,11 @@
 {
     for (int i = 0; i < count; i++)
     {
-        samplers[i] = mState.mSamplerManager->createSampler();
+        if (!mState.mSamplerManager->createSampler(&samplers[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenSamplers);
+            return;
+        }
     }
 }
 
@@ -8384,7 +8452,11 @@
 {
     for (int i = 0; i < count; i++)
     {
-        pipelines[i] = createProgramPipeline();
+        if (!createProgramPipeline(&pipelines[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenProgramPipelines);
+            return;
+        }
     }
 }
 
@@ -8600,7 +8672,11 @@
 {
     for (int i = 0; i < n; i++)
     {
-        memoryObjects[i] = createMemoryObject();
+        if (!createMemoryObject(&memoryObjects[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLCreateMemoryObjectsEXT);
+            return;
+        }
     }
 }
 
@@ -8763,7 +8839,11 @@
 {
     for (int i = 0; i < n; i++)
     {
-        semaphores[i] = createSemaphore();
+        if (!createSemaphore(&semaphores[i]))
+        {
+            handleExhaustionError(angle::EntryPoint::GLGenSemaphoresEXT);
+            return;
+        }
     }
 }
 
diff --git a/src/libANGLE/Context.h b/src/libANGLE/Context.h
index 668f8a7..2ee84ee 100644
--- a/src/libANGLE/Context.h
+++ b/src/libANGLE/Context.h
@@ -628,12 +628,12 @@
     egl::Error unMakeCurrent(const egl::Display *display);
 
     // These create and destroy methods pass through to ResourceManager, which owns these objects.
-    BufferID createBuffer();
-    TextureID createTexture();
-    RenderbufferID createRenderbuffer();
-    ProgramPipelineID createProgramPipeline();
-    MemoryObjectID createMemoryObject();
-    SemaphoreID createSemaphore();
+    bool createBuffer(BufferID *outBuffer);
+    bool createTexture(TextureID *outTexture);
+    bool createRenderbuffer(RenderbufferID *outRenderbuffer);
+    bool createProgramPipeline(ProgramPipelineID *outProgramPipeline);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/libANGLE/HandleAllocator_unittest.cpp b/src/libANGLE/HandleAllocator_unittest.cpp
index 43e7fcc..5376603 100644
--- a/src/libANGLE/HandleAllocator_unittest.cpp
+++ b/src/libANGLE/HandleAllocator_unittest.cpp
@@ -20,9 +20,11 @@
 namespace
 {
 
+constexpr GLuint kMaxHandleForTesting = std::numeric_limits<GLuint>::max();
+
 TEST(HandleAllocatorTest, ReservationsWithGaps)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     std::set<GLuint> allocationList;
     for (GLuint id = 2; id < 50; id += 2)
@@ -38,7 +40,8 @@
     std::set<GLuint> allocatedList;
     for (size_t allocationNum = 0; allocationNum < allocationList.size() * 2; ++allocationNum)
     {
-        GLuint handle = allocator.allocate();
+        GLuint handle = 0;
+        EXPECT_TRUE(allocator.allocate(&handle));
         EXPECT_EQ(0u, allocationList.count(handle));
         EXPECT_EQ(0u, allocatedList.count(handle));
         allocatedList.insert(handle);
@@ -47,7 +50,7 @@
 
 TEST(HandleAllocatorTest, Random)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     std::set<GLuint> allocationList;
     for (size_t iterationCount = 0; iterationCount < 40; ++iterationCount)
@@ -64,7 +67,8 @@
 
         for (size_t normalCount = 0; normalCount < 40; ++normalCount)
         {
-            GLuint normalHandle = allocator.allocate();
+            GLuint normalHandle = 0;
+            EXPECT_TRUE(allocator.allocate(&normalHandle));
             EXPECT_EQ(0u, allocationList.count(normalHandle));
             allocationList.insert(normalHandle);
         }
@@ -78,7 +82,8 @@
 
     for (GLuint count = 1; count < 10; count++)
     {
-        GLuint result = limitedAllocator.allocate();
+        GLuint result = 0;
+        EXPECT_TRUE(limitedAllocator.allocate(&result));
         EXPECT_EQ(count, result);
     }
 
@@ -92,47 +97,50 @@
         limitedAllocator.reserve(count);
     }
 
-    GLint finalResult = limitedAllocator.allocate();
-    EXPECT_EQ(finalResult, 1);
+    GLuint finalResult = 0;
+    EXPECT_TRUE(limitedAllocator.allocate(&finalResult));
+    EXPECT_EQ(finalResult, 1u);
 }
 
 // The following test covers reserving a handle with max uint value. See
 // http://anglebug.com/42260058
 TEST(HandleAllocatorTest, ReserveMaxUintHandle)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     GLuint maxUintHandle = std::numeric_limits<GLuint>::max();
     allocator.reserve(maxUintHandle);
 
-    GLuint normalHandle = allocator.allocate();
+    GLuint normalHandle = 0;
+    EXPECT_TRUE(allocator.allocate(&normalHandle));
     EXPECT_EQ(1u, normalHandle);
 }
 
 // The following test covers reserving a handle with max uint value minus one then max uint value.
 TEST(HandleAllocatorTest, ReserveMaxUintHandle2)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     GLuint maxUintHandle = std::numeric_limits<GLuint>::max();
     allocator.reserve(maxUintHandle - 1);
     allocator.reserve(maxUintHandle);
 
-    GLuint normalHandle = allocator.allocate();
+    GLuint normalHandle = 0;
+    EXPECT_TRUE(allocator.allocate(&normalHandle));
     EXPECT_EQ(1u, normalHandle);
 }
 
 // To test if the allocator keep the handle in a sorted order.
 TEST(HandleAllocatorTest, SortedOrderHandle)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     allocator.reserve(3);
 
     GLuint allocatedList[5];
     for (GLuint count = 0; count < 5; count++)
     {
-        allocatedList[count] = allocator.allocate();
+        EXPECT_TRUE(allocator.allocate(&allocatedList[count]));
     }
 
     EXPECT_EQ(1u, allocatedList[0]);
@@ -145,14 +153,18 @@
 // Tests the reset method.
 TEST(HandleAllocatorTest, Reset)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     for (int iteration = 0; iteration < 1; ++iteration)
     {
         allocator.reserve(3);
-        EXPECT_EQ(1u, allocator.allocate());
-        EXPECT_EQ(2u, allocator.allocate());
-        EXPECT_EQ(4u, allocator.allocate());
+        GLuint handle = 0;
+        EXPECT_TRUE(allocator.allocate(&handle));
+        EXPECT_EQ(1u, handle);
+        EXPECT_TRUE(allocator.allocate(&handle));
+        EXPECT_EQ(2u, handle);
+        EXPECT_TRUE(allocator.allocate(&handle));
+        EXPECT_EQ(4u, handle);
         allocator.reset();
     }
 }
@@ -165,10 +177,15 @@
     const std::unordered_set<GLuint> expectedHandles = {1, 2, 3};
     std::unordered_set<GLuint> handles;
 
+    auto allocateHandle = [&]() {
+        GLuint handle = 0;
+        EXPECT_TRUE(allocator.allocate(&handle));
+        handles.insert(handle);
+    };
     EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), true);
-    handles.insert(allocator.allocate());
-    handles.insert(allocator.allocate());
-    handles.insert(allocator.allocate());
+    allocateHandle();
+    allocateHandle();
+    allocateHandle();
     EXPECT_EQ(expectedHandles, handles);
     EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), false);
 
@@ -176,9 +193,9 @@
     allocator.reset();
 
     EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), true);
-    handles.insert(allocator.allocate());
-    handles.insert(allocator.allocate());
-    handles.insert(allocator.allocate());
+    allocateHandle();
+    allocateHandle();
+    allocateHandle();
     EXPECT_EQ(expectedHandles, handles);
     EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), false);
 }
@@ -186,15 +203,16 @@
 // Covers a particular bug with reserving and allocating sub ranges.
 TEST(HandleAllocatorTest, ReserveAndAllocateIterated)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     for (int iteration = 0; iteration < 3; ++iteration)
     {
         allocator.reserve(5);
         allocator.reserve(6);
-        GLuint a = allocator.allocate();
-        GLuint b = allocator.allocate();
-        GLuint c = allocator.allocate();
+        GLuint a = 0, b = 0, c = 0;
+        EXPECT_TRUE(allocator.allocate(&a));
+        EXPECT_TRUE(allocator.allocate(&b));
+        EXPECT_TRUE(allocator.allocate(&c));
         allocator.release(c);
         allocator.release(a);
         allocator.release(b);
@@ -206,11 +224,11 @@
 // This test reproduces invalid heap bug when reserve resources after release.
 TEST(HandleAllocatorTest, ReserveAfterReleaseBug)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     for (int iteration = 1; iteration <= 16; ++iteration)
     {
-        allocator.allocate();
+        EXPECT_TRUE(allocator.allocate(nullptr));
     }
 
     allocator.release(15);
@@ -223,19 +241,19 @@
 
     allocator.reserve(1);
 
-    allocator.allocate();
+    EXPECT_TRUE(allocator.allocate(nullptr));
 }
 
 // This test is to verify that we consolidate handle ranges when releasing a handle.
 TEST(HandleAllocatorTest, ConsolidateRangeDuringRelease)
 {
-    gl::HandleAllocator allocator;
+    gl::HandleAllocator allocator(kMaxHandleForTesting);
 
     // Reserve GLuint(-1)
     allocator.reserve(static_cast<GLuint>(-1));
     // Allocate a few others
-    allocator.allocate();
-    allocator.allocate();
+    EXPECT_TRUE(allocator.allocate(nullptr));
+    EXPECT_TRUE(allocator.allocate(nullptr));
 
     // Release GLuint(-1)
     allocator.release(static_cast<GLuint>(-1));
@@ -243,8 +261,38 @@
     // Allocate one more handle.
     // Since we consolidate handle ranges during a release we do not expect to get back a
     // handle value of GLuint(-1).
-    GLuint handle = allocator.allocate();
+    GLuint handle = 0;
+    EXPECT_TRUE(allocator.allocate(&handle));
     EXPECT_NE(handle, static_cast<GLuint>(-1));
 }
 
+// Test that HandleAllocator::allocate returns false when there are no more available handles.
+TEST(HandleAllocatorTest, HandleExhaustion)
+{
+    constexpr size_t kCount = 16;
+    gl::HandleAllocator allocator(kCount);
+
+    // Use all available handles
+    std::vector<GLuint> handles;
+    for (size_t iteration = 0; iteration < kCount; ++iteration)
+    {
+        GLuint handle;
+        EXPECT_TRUE(allocator.allocate(&handle));
+        handles.push_back(handle);
+    }
+
+    // allocations should fail
+    EXPECT_FALSE(allocator.allocate(nullptr));
+    EXPECT_FALSE(allocator.anyHandleAvailableForAllocation());
+
+    // Release one handle, the next allocation should succeed
+    allocator.release(handles[0]);
+    EXPECT_TRUE(allocator.anyHandleAvailableForAllocation());
+    EXPECT_TRUE(allocator.allocate(nullptr));
+
+    // The allocator is full again, allocations should fail
+    EXPECT_FALSE(allocator.allocate(nullptr));
+    EXPECT_FALSE(allocator.anyHandleAvailableForAllocation());
+}
+
 }  // anonymous namespace
diff --git a/src/libANGLE/ResourceManager_unittest.cpp b/src/libANGLE/ResourceManager_unittest.cpp
index c15a8d8..b84b393 100644
--- a/src/libANGLE/ResourceManager_unittest.cpp
+++ b/src/libANGLE/ResourceManager_unittest.cpp
@@ -48,7 +48,8 @@
     EXPECT_CALL(mMockFactory, createTexture(_)).Times(1).RetiresOnSaturation();
 
     mTextureManager->checkTextureAllocation(&mMockFactory, {1}, TextureType::_2D);
-    TextureID newTexture = mTextureManager->createTexture();
+    TextureID newTexture;
+    EXPECT_TRUE(mTextureManager->createTexture(&newTexture));
     EXPECT_NE(1u, newTexture.value);
 }
 
@@ -57,7 +58,8 @@
     EXPECT_CALL(mMockFactory, createBuffer(_)).Times(1).RetiresOnSaturation();
 
     mBufferManager->checkBufferAllocation(&mMockFactory, {1});
-    BufferID newBuffer = mBufferManager->createBuffer();
+    BufferID newBuffer;
+    EXPECT_TRUE(mBufferManager->createBuffer(&newBuffer));
     EXPECT_NE(1u, newBuffer.value);
 }
 
@@ -66,7 +68,8 @@
     EXPECT_CALL(mMockFactory, createRenderbuffer(_)).Times(1).RetiresOnSaturation();
 
     mRenderbuffermanager->checkRenderbufferAllocation(&mMockFactory, {1});
-    RenderbufferID newRenderbuffer = mRenderbuffermanager->createRenderbuffer();
+    RenderbufferID newRenderbuffer;
+    EXPECT_TRUE(mRenderbuffermanager->createRenderbuffer(&newRenderbuffer));
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

ANGLE HandleAllocator: Invalid iterator access on empty vector → UAF + OOB (double-free plausible)

Reporter statement

I reported the same issue to Firefox(status: confirmed) because ANGLE’s HandleAllocator does not handle ID-exhaustion correctly. Since Chromium ships the same ANGLE code, I believe Chromium is affected as well. ()

Why I believe Chromium/ANGLE is affected

Firefox and Chromium use the same ANGLE upstream sources for this component (the HandleAllocator implementation is identical or functionally equivalent at the affected site).

I couldn’t find handling for ID-exhaustion on the Chromium side either (same behavior at the allocator level)

VULNERABILITY DETAILS

third_party/angle/src/libANGLE/HandleAllocator.cpp (gl::HandleAllocator::allocate()) mishandles handle-exhaustion. After erasing the last HandleRange from mUnallocatedList, the very next allocation does:

First, when the ID is exhausted,

if (listIt->begin == listIt->end)
{
    mUnallocatedList.erase(listIt);
}

due to this code, mUnallocatedList becomes an invalid iterator, and the HandleRange object that was inside calls its destructor.

The problem occurs when, after all IDs are exhausted, you create an object again to receive an ID. When you attempt another allocation after all IDs are exhausted,

auto listIt = mUnallocatedList.begin();

this again accesses an invalid iterator, and due to begin(), you once again obtain the HandleRange object whose destructor should already have been called. Since this HandleRange object did not initialize its begin and end values again,

if (listIt->begin == listIt->end)
{
    mUnallocatedList.erase(listIt);
}

this code causes one more erase. At this time, since the size of mUnallocatedList is 0, a problem also occurs in that erase. (In Firefox, when performing that erase, a 4-byte container overflow read occurred in the ASan log, and while attempting to read an inaccessible region, an unhandled exception occurred during debugging. In Firefox, I was able to check the ASan log and follow the debugging to the end, but I could not do so in Chromium. Please refer to the debugging video that will be attached later, and if you can let me know why I could not debug to the end and why the ASan log did not occur, it would be very helpful for submitting the ASan log in the future. I will continue to update the report and attach the ASan log later.)

Environment

Debugger: Visual Studio 2022 Browser: Google Chrome 143.0.7473.0 (Developer Build) (64-bit) OS Name: Microsoft Windows 11 Pro OS Version: 10.0.26100 N/A build 26100 Precondition: Sufficient computer memory resources are required to create a UINTMAX number of shader objects

Reproduction:

Since I do not have sufficient computer memory resources, I have manipulated the begin value as follows: The original HandleAllocator constructor:

HandleAllocator::HandleAllocator() : mBaseValue(1), mNextValue(1), mLoggingEnabled(false) { mUnallocatedList.push_back(HandleRange(1, std::numeric_limits<GLuint>::max())); } was modified to:

HandleAllocator::HandleAllocator() : mBaseValue(1), mNextValue(1), mLoggingEnabled(false) { // EXPERIMENT: narrow free range to (2^32-1000 .. 2^32-1) for fast exhaustion

const GLuint kStart = std::numeric_limits<GLuint>::max() - 1000u; const GLuint kEnd = std::numeric_limits<GLuint>::max(); mUnallocatedList.push_back(HandleRange(kStart, kEnd)); } Then, I entered the following code into the web browser’s console:

PoC

(() => {
let c = document.querySelector('#dbg-webgl-canvas');
if (!c) {
c = document.createElement('canvas');
c.id = 'dbg-webgl-canvas';
c.width = 4; c.height = 4;
document.body.appendChild(c);
}
window.gl = c.getContext('webgl2') || c.getContext('webgl');
console.log('gl ready:', gl ? (gl instanceof WebGL2RenderingContext ? 'WebGL2' : 'WebGL1') : 'FAILED');
})();

(() => {
if (!gl) { console.warn('run setup first'); return; }
const N = 2000; // Increase if necessary
window.__shaders = window.__shaders || [];
for (let i = 0; i < N; ++i) {
const type = (i & 1) ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER;
__shaders.push(gl.createShader(type));
}
console.log('created shaders:', __shaders.length);
})();
View on issue tracker
Uploaded PoC files