CVE-2026-87648
Overview
Background
- `angle::Result`
- ANGLE’s status enum (
Continueon success,Stopon error) propagated by theANGLE_TRYmacro, which returns early when a result is notContinue. - `orphanSibling` / image orphaning
- when an EGLImage or its source/target is destroyed, ANGLE walks the sibling images and lets each recover its data before the backing storage is freed.
- `TextureStorage11::onDestroy` / `recoverFromAssociatedStorage`
- D3D11 texture-storage teardown that first asks each associated
Imageto copy its data out of the storage, then frees the storage. - Manual result tracking
- the pre-fix idiom of a local
angle::Result resultupdated inside a loop withif (subResult != Continue) result = subResult;before a finalANGLE_TRY(result).
Root Cause Analysis
During image orphaning and D3D11 storage destruction, ANGLE loops over siblings/associated images, calls a recovery routine on each, and tracks whether any failed in a local angle::Result before a final ANGLE_TRY(result). The manual tracking was error-prone: a failing recovery could be recorded and then effectively lost or mis-sequenced across iterations, so ANGLE_TRY(result) saw Continue even though an image had failed to recover its data. Execution then proceeded to free the TextureStorage while an associated Image still referenced that storage, producing a use-after-free when the image was later accessed.
The fix introduces angle::ResultAccumulator, whose operator= only overwrites when the incoming result is non-Continue, so the first error is retained (“sticky”) and ANGLE_TRY(result) reliably bails before the unsafe free. orphanImages and the TextureStorage11_2D/Cube/3D/2DArray::onDestroy loops were converted to accumulate through this type, guaranteeing a recovery failure aborts teardown instead of continuing to release still-referenced memory.
ResultAccumulator preserves the first non-Continue result so ANGLE_TRY stops before the UAF.Attack Path
- Set up shared images
attacker JavaScript (via WebGL/EGLImage sharing on the D3D11 backend) creates textures and images that share backing
TextureStorage. - Force a failing recovery
it drives teardown (deleting a texture/image) under conditions where
recoverFromAssociatedStorage/orphanSiblingfails for one sibling mid-loop. - Error is dropped
pre-fix, the manual tracking loses that failure, so
ANGLE_TRY(result)does not abortonDestroy. - Storage freed while referenced
teardown continues and frees the
TextureStorageeven though an associatedImagestill holds a reference. - Use-after-free a later access through the surviving image dereferences the freed storage, which heap grooming can turn into a controlled read/write in the GPU process.
Impact Assessment
Files Changed
src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cppsrc/libANGLE/Image.cppsrc/libANGLE/Error.h
Audit Directions
- Hand-rolled result tracking in loopsaudit loops that update a local
Result/error variable conditionally before a singleANGLE_TRY/check; these can drop or mis-order failures. - Free-after-recovery orderingflag teardown paths that free backing storage only after a recovery step, and confirm a failed recovery actually aborts the free.
- Sticky-error adoptionsearch remaining destruction/orphaning code for the old
if (r != Continue) result = r;idiom and migrate it toResultAccumulator.
Patch
From b085ab3a85c9fdcfda4f5900d881f554e7109ba9 Mon Sep 17 00:00:00 2001 From: wangra <[email protected]> Date: Thu, 30 Jul 2026 22:50:17 -0400 Subject: [PATCH] Introduce ResultAccumulator to aggregate results Introduce ResultAccumulator to aggregate results to simplify manual result tracking. Test: angle_end2end_tests --gtest_filter="ImageTest.SiblingOrphaningOnBackendFailure" Bug: b/513048243 Change-Id: I5a092a6c6cb0fa2b1269f1f619952b18a004ec76 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8178168 Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Commit-Queue: Ran Wang <[email protected]> --- diff --git a/src/libANGLE/Error.h b/src/libANGLE/Error.h index 5b140e3..fc8c218 100644 --- a/src/libANGLE/Error.h +++ b/src/libANGLE/Error.h @@ -123,6 +123,27 @@ Stop, }; +class [[nodiscard]] ResultAccumulator +{ + public: + ResultAccumulator() : mResult(Result::Continue) {} + ResultAccumulator(Result result) : mResult(result) {} + + ResultAccumulator &operator=(Result result) + { + if (result != Result::Continue) + { + mResult = result; + } + return *this; + } + + operator Result() const { return mResult; } + + private: + Result mResult; +}; + // TODO(jmadill): Remove this when refactor is complete. http://anglebug.com/42261727 egl::Error ResultToEGL(Result result); } // namespace angle diff --git a/src/libANGLE/Image.cpp b/src/libANGLE/Image.cpp index 33db5aa..0bad2f0 100644 --- a/src/libANGLE/Image.cpp +++ b/src/libANGLE/Image.cpp @@ -129,7 +129,7 @@ { ASSERT(outReleaseImage != nullptr); - angle::Result result = angle::Result::Continue; + angle::ResultAccumulator result = angle::Result::Continue; if (mTargetOf.get() != nullptr) { @@ -143,11 +143,7 @@ { for (Image *sourceImage : mSourcesOf) { - angle::Result orphanResult = sourceImage->orphanSibling(context, this); - if (orphanResult != angle::Result::Continue) - { - result = orphanResult; - } + result = sourceImage->orphanSibling(context, this); } mSourcesOf.clear(); } diff --git a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp index 4ad3d2c..8b86f1c 100644 --- a/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cpp @@ -1000,7 +1000,7 @@ angle::Result TextureStorage11_2D::onDestroy(const gl::Context *context) { - angle::Result result = angle::Result::Continue; + angle::ResultAccumulator result = angle::Result::Continue; for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++) { if (mAssociatedImages[i] != nullptr) @@ -1009,10 +1009,7 @@ // We must let the Images recover their data before we delete it from the // TextureStorage. - if (IsError(mAssociatedImages[i]->recoverFromAssociatedStorage(context))) - { - result = angle::Result::Stop; - } + result = mAssociatedImages[i]->recoverFromAssociatedStorage(context); } } ANGLE_TRY(result); @@ -1932,7 +1929,7 @@ angle::Result TextureStorage11_Cube::onDestroy(const gl::Context *context) { - angle::Result result = angle::Result::Continue; + angle::ResultAccumulator result = angle::Result::Continue; for (unsigned int level = 0; level < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; level++) { for (unsigned int face = 0; face < gl::kCubeFaceCount; face++) @@ -1943,10 +1940,7 @@ // We must let the Images recover their data before we delete it from the // TextureStorage. - if (IsError(mAssociatedImages[face][level]->recoverFromAssociatedStorage(context))) - { - result = angle::Result::Stop; - } + result = mAssociatedImages[face][level]->recoverFromAssociatedStorage(context); } } } @@ -2445,7 +2439,7 @@ angle::Result TextureStorage11_3D::onDestroy(const gl::Context *context) { - angle::Result result = angle::Result::Continue; + angle::ResultAccumulator result = angle::Result::Continue; for (unsigned i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++) { if (mAssociatedImages[i] != nullptr) @@ -2454,10 +2448,7 @@ // We must let the Images recover their data before we delete it from the // TextureStorage. - if (IsError(mAssociatedImages[i]->recoverFromAssociatedStorage(context))) - { - result = angle::Result::Stop; - } + result = mAssociatedImages[i]->recoverFromAssociatedStorage(context); } } @@ -2825,7 +2816,7 @@ angle::Result TextureStorage11_2DArray::onDestroy(const gl::Context *context) { - angle::Result result = angle::Result::Continue; + angle::ResultAccumulator result = angle::Result::Continue; for (auto iter : mAssociatedImages) { if (iter.second) @@ -2834,10 +2825,7 @@ // We must let the Images recover their data before we delete it from the // TextureStorage. - if (IsError(iter.second->recoverFromAssociatedStorage(context))) - { - result = angle::Result::Stop; - } + result = iter.second->recoverFromAssociatedStorage(context); } } mAssociatedImages.clear();
Original Bug Report
Potential Use-After-Free in ANGLE EGLImage Orphaning on Windows
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 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) vulnerability exists in ANGLE’s EGLImage implementation on Windows when a source texture is destroyed during Out-Of-Memory (OOM) conditions. If the backend fails to orphan the source sibling due to an allocation failure, a raw pointer to the freed object is retained in the image state. This dangling pointer can subsequently be dereferenced during rendering or upon the image’s own destruction.
Affected files:
third_party/angle/src/libANGLE/Image.cppthird_party/angle/src/libANGLE/renderer/d3d/EGLImageD3D.cppthird_party/angle/src/libANGLE/Texture.cppthird_party/angle/src/libANGLE/Renderbuffer.cpp
Estimated timestamp from git blame: 2018-07-27
The vulnerability is located in ANGLE’s EGLImage management logic, primarily affecting the D3D11 backend on Windows. When an ImageSibling (such as a gl::Texture or gl::Renderbuffer) that serves as the source for an egl::Image is destroyed, ANGLE attempts to “orphan” the sibling. Orphaning is intended to detach the image from its source while preserving its content for other objects that may still be using the image.
In third_party/angle/src/libANGLE/Image.cpp, the function Image::orphanSibling calls the backend’s implementation of orphan:
angle::Result Image::orphanSibling(const gl::Context *context, ImageSibling *sibling) {
ANGLE_TRY(mImplementation->orphan(context, sibling)); // [1] Returns early on error
if (mState.source == sibling) {
...
mState.source = nullptr; // [2] Never reached if ANGLE_TRY fails
}
}
In the D3D11 backend (third_party/angle/src/libANGLE/renderer/d3d/EGLImageD3D.cpp), orphan calls copyToLocalRendertarget, which attempts to create a copy of the texture’s render target. If this allocation fails (e.g., due to VRAM exhaustion), it returns an error. Because of the ANGLE_TRY macro at [1], the error causes the function to return immediately, skipping the crucial line at [2] that nulls the mState.source pointer.
When the source object is a gl::Texture or gl::Renderbuffer, its destruction proceeds even if orphaning fails because the error is explicitly ignored in the onDestroy method (e.g., in third_party/angle/src/libANGLE/Texture.cpp):
void Texture::onDestroy(const Context *context) {
...
egl::RefCountObjectReleaser<egl::Image> releaseImage;
(void)orphanImages(context, &releaseImage); // [3] Error is swallowed
...
}
As a result, the egl::Image maintains a dangling raw pointer to the now-freed gl::Texture object.
Potential Sinks and Impact
The dangling pointer can be dereferenced in several ways:
- Virtual Function Calls: Rendering using a target texture that refers to the orphaned
egl::Imagewill eventually callEGLImageD3D::getRenderTarget. This method performs a virtual call (getAttachmentRenderTarget) on the danglingmState.sourcepointer, potentially allowing for control flow hijacking. - Heap Corruption: When the
egl::Imageitself is destroyed,Image::onDestroycallsmState.source->removeImageSource(this). This leads to a memory write to the freed memory at the offset of themSourcesOfmember, which can be leveraged for memory corruption exploits.
Suggested Potential Steps to Reproduce
An attacker operating from a compromised renderer could potentially follow these steps:
- Induce heavy VRAM pressure in the GPU process by allocating many large textures.
- Create a
SharedImagewith initial pixel data. On Windows, this frequently triggers theEGLImageBackingpath, which creates anegl::Imagefrom a source GL texture and then attempts to release the source texture immediately if no workarounds are active. - Trigger the destruction of the source texture. If orphaning fails due to the VRAM pressure, the
egl::Imageretains a dangling pointer to the texture. - Attempt to use or destroy the
SharedImageto trigger a dereference of the dangling pointer.
Recommended Fix
The Image::orphanSibling logic should be modified to ensure that mState.source is nulled out even if the backend orphaning call returns an error. While the backend might fail to preserve the data under OOM conditions, the reference to the dying sibling must be cleared to maintain memory safety.
Note: These findings are based on manual code review; a working proof-of-concept has not yet been produced.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.