Medium patch-now CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Blink
DescriptionUse after free in Blink
ComponentBlink
Sub-componentBlink
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (unverified)
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.
Tracker497649372
Fix commite11da14a3575 (chromium/src) +8/-2
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
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-03-30 (public git)
Disclosed2026-05-05
Patch gap36 days — fix public in source before the advisory

Background

`ImageDownloaderImpl`
the Blink-renderer implementation of the image-downloader Mojo interface that owns in-flight fetchers in the image_fetchers_ container.
`MultiResolutionImageResourceFetcher::Dispose`
tears down a fetcher and, per the commit message, synchronously invokes its bound completion callback.
`ContextDestroyed`
a lifecycle hook run when the execution context goes away, which must tear down all outstanding fetchers.
`WTF::Vector`
Blink’s vector type; erasing an element shifts/clears trailing slots, and a cached end iterator over it is invalidated by mutation during iteration.

Root Cause Analysis

ImageDownloaderImpl::ContextDestroyed iterated over the member image_fetchers_ with a range-based for loop and called fetcher->Dispose() on each entry. Dispose() synchronously runs the fetcher’s callback, which is bound to ImageDownloaderImpl::DidFetchImage, and that callback erases the just-disposed fetcher from image_fetchers_. Mutating the container during range iteration invalidated the loop’s cached end iterator and left stale pointers in the uncleared trailing slots of the WTF::Vector, so the loop dereferenced freed/dangling fetcher pointers — a use-after-free.

The fix moves the container into a local fetchers via std::move(image_fetchers_) and immediately clear()s the member, so the loop iterates a stable copy while image_fetchers_ is already empty when the synchronous DidFetchImage callbacks run. The erase inside the callback now operates on the emptied member and cannot perturb the iteration.

Key insight
The core mistake was iterating a member container while a synchronous callback (Dispose → DidFetchImage) mutated that same container; moving the contents to a local copy and clearing the member first makes the iteration stable and the re-entrant erase harmless.

Attack Path

  1. Start image downloads A page triggers image downloads so that ImageDownloaderImpl holds one or more live entries in image_fetchers_.
  2. Tear down the context The execution context is destroyed, invoking ContextDestroyed, which begins the range-based loop over image_fetchers_.
  3. Synchronous re-entrant erase fetcher->Dispose() synchronously runs DidFetchImage, which erases the fetcher from image_fetchers_, invalidating the loop’s end iterator and leaving stale pointers in trailing slots.
  4. Dangling dereference The loop continues over the mutated vector and dereferences a freed fetcher pointer, producing the use-after-free (inferred: further exploitation primitives are not shown by the patch).

Impact Assessment

An attacker able to drive image downloads and context teardown in a renderer can trigger a use-after-free on ImageDownloaderImpl’s fetcher pointers within the renderer process. The advisory rates this medium severity (use after free in Blink) and does not report in-the-wild exploitation; any escalation to a controlled read/write or code execution is (inferred) and not demonstrated by the diff. The precondition is outstanding image fetchers at the moment the execution context is destroyed.

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc
modified

Files Changed

  • third_party/blink/renderer/modules/image_downloader/image_downloader_impl.cc

Audit Directions

  • Synchronous callback re-entrancy
    Audit loops that call a method which may synchronously invoke a bound callback; verify the callback cannot mutate the container being iterated.
  • Dispose/teardown ordering
    Review lifecycle hooks (ContextDestroyed, destructors) that walk owned-object containers for the pattern of mutate-during-iterate, preferring move-then-clear-then-iterate.
  • WTF::Vector iteration with erase
    Search for range-based iteration over WTF::Vector/sets where an in-loop call path can erase elements, invalidating cached iterators.
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • third_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

  1. The Loop: ImageDownloaderImpl::ContextDestroyed() iterates over the image_fetchers_ vector using a range-based for loop:
    void ImageDownloaderImpl::ContextDestroyed() {
      for (const auto& fetcher : image_fetchers_) {
        fetcher->Dispose();
      }
      image_fetchers_.clear();
    }
    
  2. Synchronous Mutation: MultiResolutionImageResourceFetcher::Dispose() synchronously runs its callback: std::move(callback_).Run(...).
  3. Vector Erasure: This callback is bound to ImageDownloaderImpl::DidFetchImage(), which finds the corresponding fetcher in image_fetchers_ and erases it: it = image_fetchers_.erase(it);.
  4. Stale Pointers: WTF::Vector::erase() calls TypeOperations::MoveOverlapping() (using memmove) to shift the remaining elements left. Crucially, because the default allocator is PartitionAlloc (where kIsGarbageCollected is false), ClearUnusedSlots() is a no-op. The trailing slots of the vector’s backing buffer retain the raw bit patterns of the moved unique_ptrs.
  5. The UAF: The range-based for loop caches the end iterator initially. Even though the vector shrinks with each erasure, the loop continues iterating until it reaches the original end. It eventually encounters the uncleared, stale unique_ptrs in the trailing slots. These point to memory that was freed in previous iterations. The loop dereferences the stale pointer and calls Dispose() 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.

View on issue tracker