CVE-2026-7974
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc |
modified |
Files Changed
third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc
Patch
From e11da14a35755a60e35c2669ae046da1818c3edf Mon Sep 17 00:00:00 2001 From: Colin Blundell <[email protected]> Date: Mon, 30 Mar 2026 08:19:37 -0700 Subject: [PATCH] [Blink] Fix UaF in ImageDownloaderImpl::ContextDestroyed() ImageDownloaderImpl::ContextDestroyed() iterates over image_fetchers_ and calls Dispose() on each fetcher. MultiResolutionImageResourceFetcher::Dispose() synchronously runs its callback [1], which is bound to ImageDownloaderImpl::DidFetchImage() [2]. DidFetchImage() erases the fetcher from image_fetchers_ [3]. This synchronous mutation of the vector during range-based for loop iteration leads to Use-After-Free because the loop caches the end iterator and encounters stale pointers in the uncleared trailing slots of the WTF::Vector after erasure. This CL fixes the issue by moving image_fetchers_ to a local variable before iterating, ensuring the loop operates on a stable copy and image_fetchers_ is empty during the synchronous callbacks. [1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/image_downloader/multi_resolution_image_resource_fetcher.cc;l=168;drc=4376ecb9520cb832e52d06b36db3806440ab19b2;bpv=1;bpt=1 [2] https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc;l=289;bpv=1;bpt=1 [3] https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc;l=311-314;bpv=1;bpt=1 Bug: 497649372 Change-Id: Id7c951c17e54b47b3386425b42b1706c03a95db9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7705801 Reviewed-by: Vasiliy Telezhnikov <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Cr-Commit-Position: refs/heads/main@{#1607114} --- diff --git a/third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc b/third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc index 70fd7123..de2aa57 100644 --- a/third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc +++ b/third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc @@ -325,11 +325,17 @@ } void ImageDownloaderImpl::ContextDestroyed() { - for (const auto& fetcher : image_fetchers_) { + // Calling `Dispose()` will end up calling back synchronously into + // DidFetchImage(). To avoid `image_fetchers_` being mutated while it's being + // iterated over, move its contents to a temporary var before doing the + // iteration. + auto fetchers = std::move(image_fetchers_); + image_fetchers_.clear(); + + for (const auto& fetcher : fetchers) { // Will run callbacks with an empty image vector. fetcher->Dispose(); } - image_fetchers_.clear(); } } // namespace blink
Original Bug Report
Potential Use-After-Free in ImageDownloaderImpl::ContextDestroyed
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A range-based for loop in ImageDownloaderImpl::ContextDestroyed iterates over a vector of unique_ptrs. Calling Dispose() on the elements synchronously removes them from the vector, leaving stale pointers in the vector’s trailing slots that the loop subsequently dereferences, leading to a Use-After-Free.
Affected files:
third_party/blink/renderer/modules/image_downloader/image_downloader_impl.ccthird_party/blink/renderer/modules/image_downloader/multi_resolution_image_resource_fetcher.cc
Estimated timestamp from git blame: 2020-02-16
Overview
There is a potential Use-After-Free (UAF) vulnerability in ImageDownloaderImpl::ContextDestroyed() in the renderer process. The issue stems from a range-based for loop iterating over WTF::Vector<std::unique_ptr<MultiResolutionImageResourceFetcher>> image_fetchers_. The loop body calls a method on the unique_ptr that synchronously mutates the vector by erasing the element. Because WTF::Vector::erase() uses memmove to shift elements but does not clear the trailing slots, the loop continues to iterate (using its cached end iterator) and eventually dereferences a stale unique_ptr in the uncleared memory, causing a UAF when it calls Dispose() on an already-deleted object.
Technical Details
- The Loop:
ImageDownloaderImpl::ContextDestroyed()iterates over theimage_fetchers_vector using a range-based for loop:void ImageDownloaderImpl::ContextDestroyed() { for (const auto& fetcher : image_fetchers_) { fetcher->Dispose(); } image_fetchers_.clear(); } - Synchronous Mutation:
MultiResolutionImageResourceFetcher::Dispose()synchronously runs its callback:std::move(callback_).Run(...). - Vector Erasure: This callback is bound to
ImageDownloaderImpl::DidFetchImage(), which finds the corresponding fetcher inimage_fetchers_and erases it:it = image_fetchers_.erase(it);. - Stale Pointers:
WTF::Vector::erase()callsTypeOperations::MoveOverlapping()(usingmemmove) to shift the remaining elements left. Crucially, because the default allocator isPartitionAlloc(wherekIsGarbageCollectedis false),ClearUnusedSlots()is a no-op. The trailing slots of the vector’s backing buffer retain the raw bit patterns of the movedunique_ptrs. - The UAF: The range-based for loop caches the
enditerator initially. Even though the vector shrinks with each erasure, the loop continues iterating until it reaches the originalend. It eventually encounters the uncleared, staleunique_ptrs in the trailing slots. These point to memory that was freed in previous iterations. The loop dereferences the stale pointer and callsDispose()on the attacker-controlled, freed memory chunk.
Potential Exploitability
This vulnerability could potentially lead to Remote Code Execution (RCE) in the renderer process.
Inside Dispose(), the code accesses the callback_ member, which is a base::OnceCallback. Executing this callback requires reading the bind_state_ pointer (a scoped_refptr<BindStateBase>) to find the polymorphic_invoke_ function pointer.
An attacker could attempt to groom the heap to reclaim the memory chunk of the freed MultiResolutionImageResourceFetcher during the synchronous window provided immediately after the erasure (e.g., during the Mojo IPC allocations in DidDownloadImage, which is called right after DidFetchImage). By controlling the contents of the reclaimed memory, the attacker could forge the BindStateHolder and hijack control flow.
This issue is not protected by MiraclePtr (BackupRefPtr) because the stale pointer is a raw T* stored inside a standard std::unique_ptr within the WTF::Vector’s raw backing store, completely bypassing BRP checks.
Suggested Fix
To fix this, the image_fetchers_ vector should be cleared safely before iterating, for example by swapping it with a local vector or taking ownership using std::exchange:
void ImageDownloaderImpl::ContextDestroyed() {
auto fetchers = std::move(image_fetchers_);
for (const auto& fetcher : fetchers) {
fetcher->Dispose();
}
}
(Note: These are suggested steps based on code analysis; a working proof of concept has not yet been executed to confirm exact exploitation parameters.)
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
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.