High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Downloads
DescriptionUse after free in Downloads
ComponentDownloads
Sub-componentMacOS
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker498376171
Fix commitf46add2e465a (chromium/src) +56/-10
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-04-01 (per vendor advisory)
Fix landed2026-05-21 (public git)
Disclosed2026-05-12

Background

`NSProgress` cancellationHandler
a macOS Objective-C block the system invokes when a user cancels a published progress item (e.g. the Finder download (X) button); its lifetime is controlled by the OS and it may be retained via XPC beyond Chrome’s expectations.
`DownloadItem`
the C++ object (owned by the DownloadManager) representing an in-progress download; it is freed when its owning profile is destroyed.
`raw_ptr<T>` / `DanglingPtr`
Chrome’s checked raw pointer; when such a pointer is destroyed while still referencing freed memory, the MiraclePtr machinery reports a DanglingPtr (a dangling/UAF detection crash).
`CrNSProgressUserData`
a base::SupportsUserData::Data attached to the DownloadItem that owns the associated NSProgress.

Root Cause Analysis

The NSProgress cancellation handler block captured raw_ptr<download::DownloadItem> protected_download directly. Because the block’s lifetime is owned by the system (and can be held via XPC after Chrome is done with it), the captured raw_ptr could outlive the DownloadItem: when an incognito profile is destroyed mid-download, the DownloadItem is freed while the block still exists. When the system later releases the NSProgress and thus the block, the captured raw_ptr’s destructor sees freed memory and triggers a DanglingPtr crash, and if the block had instead executed it would have called Cancel on a freed DownloadItem (a true UAF).

The fix removes all C++ pointers from the block: a new Objective-C bridge object CrDownloadCanceller holds the DownloadItem (as a RAW_PTR_EXCLUSION plain pointer), the block captures that object __weak, and CrNSProgressUserData’s destructor calls [canceller_ invalidate] (nulling the pointer) and clears progress_.cancellationHandler before the DownloadItem is destroyed. Thus a block that runs late sends cancel to a nil weak reference (a no-op), and a block merely released late captures no C++ pointer, so no DanglingPtr fires.

Key insight
A system-owned block captured a raw_ptr<DownloadItem> that could outlive the item, causing a dangling-pointer/UAF; the fix moves the pointer into an ObjC CrDownloadCanceller captured __weak and invalidates it from ~CrNSProgressUserData before the item is freed.

Attack Path

  1. Start a download in incognito A download begins in an off-the-record profile, creating an NSProgress with a cancellation handler that (pre-patch) captures a raw_ptr to the DownloadItem.
  2. System retains the block macOS holds the NSProgress/block, potentially via XPC, beyond Chrome’s control.
  3. Destroy the profile mid-download The incognito profile is torn down while the download is in progress, freeing the DownloadItem while the block (and its captured raw_ptr) still exists.
  4. Block released/executed on freed item When the system later releases the NSProgress (destroying the block) the raw_ptr destructor detects freed memory and triggers a DanglingPtr crash; had the handler fired it would dereference the freed DownloadItem via Cancel (use-after-free).

Impact Assessment

The advisory rates this high severity (Use after free in Downloads). On macOS, destroying an incognito profile while a download is active can leave the system holding a block that references the freed DownloadItem, yielding a dangling-pointer/use-after-free in the browser process; the preconditions are macOS, an in-progress incognito download, and OS retention of the NSProgress block past the item’s lifetime. Exploitability beyond the observed crash is inferred and not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
if
chrome/browser/download/download_status_updater_mac.mm
modified
CrNSProgressUserData
chrome/browser/download/download_status_updater_mac.mm
modified

Files Changed

  • chrome/browser/download/download_status_updater_mac.mm

Audit Directions

  • C++ pointers captured by system-owned blocks/closures
    Audit Objective-C blocks (and callbacks handed to OS/XPC APIs) that capture raw_ptr/this; their lifetime is not Chrome’s, so capture a __weak bridge or WeakPtr instead.
  • `RAW_PTR_EXCLUSION` boundaries
    Review uses of RAW_PTR_EXCLUSION to confirm an explicit invalidation path exists (as CrNSProgressUserData provides) so the excluded pointer is nulled before the referent dies.
  • Incognito/profile teardown vs. OS-held state
    Trace OS-registered objects (progress items, notifications, share sheets) tied to a DownloadItem/profile to ensure they are unpublished and neutralized during profile destruction.
From f46add2e465aa336135befca7062424adf43d60f Mon Sep 17 00:00:00 2001
From: Xizhe Chen <[email protected]>
Date: Thu, 21 May 2026 07:57:31 -0700
Subject: [PATCH] [macOS] Fix DanglingPtr in NSProgress cancellationHandler

The NSProgress cancellationHandler block captured a
raw_ptr<DownloadItem> whose lifetime was not tied to the block's.
When an incognito profile is destroyed while a download is in
progress, the DownloadItem is freed but the block may still be
held by the system via XPC. When the system later releases the
NSProgress (and thus the block), the raw_ptr destructor detects
that it points to freed memory and triggers a DanglingPtr crash.

Fix this by introducing a lightweight ObjC bridging object
(CrDownloadCanceller) that holds the DownloadItem pointer and is
invalidated on destruction. The cancellationHandler block captures
the bridging object via __weak, so:
- If the block executes after the DownloadItem is gone, the weak
  reference is nil and [nil cancel] is a no-op.
- If the block is released after the DownloadItem is gone, no C++
  pointers are captured in the block, so no DanglingPtr is
  triggered.

Fixed: 514939123, 498376171
Change-Id: I20173c55e2e2793b8bba6cff62124a4a09a5e45e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7862663
Commit-Queue: Yisi Yu <[email protected]>
Reviewed-by: Yisi Yu <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1634279}
---

diff --git a/chrome/browser/download/download_status_updater_mac.mm b/chrome/browser/download/download_status_updater_mac.mm
index 8381fb2..6153a5f 100644
--- a/chrome/browser/download/download_status_updater_mac.mm
+++ b/chrome/browser/download/download_status_updater_mac.mm
@@ -7,30 +7,74 @@
 #import <Foundation/Foundation.h>
 
 #include "base/apple/foundation_util.h"
-#include "base/memory/scoped_policy.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/supports_user_data.h"
 #include "base/time/time.h"
 #import "chrome/browser/ui/cocoa/dock_icon.h"
 #include "components/download/public/common/download_item.h"
 #import "net/base/apple/url_conversions.h"
 
+@interface CrDownloadCanceller : NSObject
+- (instancetype)initWithDownloadItem:(download::DownloadItem*)download;
+- (void)cancel;
+- (void)invalidate;
+@end
+
+@implementation CrDownloadCanceller {
+  // `_download` is owned by the DownloadManager. It must not be captured by
+  // the NSProgress cancellationHandler, whose lifetime is controlled by the
+  // system. CrNSProgressUserData invalidates this pointer before the owning
+  // DownloadItem is destroyed.
+  RAW_PTR_EXCLUSION download::DownloadItem* _download;
+}
+
+- (instancetype)initWithDownloadItem:(download::DownloadItem*)download {
+  self = [super init];
+  if (self) {
+    _download = download;
+  }
+  return self;
+}
+
+- (void)cancel {
+  if (_download) {
+    _download->Cancel(/*user_cancel=*/true);
+  }
+}
+
+- (void)invalidate {
+  _download = nullptr;
+}
+
+@end
+
 namespace {
 
 const char kCrNSProgressUserDataKey[] = "CrNSProgressUserData";
 
 class CrNSProgressUserData : public base::SupportsUserData::Data {
  public:
-  CrNSProgressUserData(NSProgress* progress, const base::FilePath& target)
+  CrNSProgressUserData(NSProgress* progress,
+                       CrDownloadCanceller* canceller,
+                       const base::FilePath& target)
       : target_(target) {
     progress_ = progress;
+    canceller_ = canceller;
   }
-  ~CrNSProgressUserData() override { [progress_ unpublish]; }
+  ~CrNSProgressUserData() override {
+    [canceller_ invalidate];
+    // Clear the handler to eagerly release the block. The __weak reference
+    // in the block already guarantees safety if the block outlives us.
+    progress_.cancellationHandler = nil;
+    [progress_ unpublish];
+  }
 
   NSProgress* progress() const { return progress_; }
   base::FilePath target() const { return target_; }
   void setTarget(const base::FilePath& target) { target_ = target; }
 
  private:
+  CrDownloadCanceller* __strong canceller_;
   NSProgress* __strong progress_;
   base::FilePath target_;
 };
@@ -65,24 +109,26 @@
   // ship it.
   progress.pausable = NO;
 
-  // TODO(crbug.com/498376171): Mitigation for potential UAF by
-  // cancellationHandler below.
-  raw_ptr<download::DownloadItem> protected_download = download;
+  CrDownloadCanceller* __strong canceller =
+      [[CrDownloadCanceller alloc] initWithDownloadItem:download];
 
   // Do publish a cancellation handler. In icon view, the Finder provides a
   // little (X) button on the icon, and using it will cause this callback.
+  // Only capture the ObjC canceller weakly because NSProgress may be held by
+  // the system after DownloadItem is destroyed.
+  __weak CrDownloadCanceller* weak_canceller = canceller;
   progress.cancellable = YES;
   progress.cancellationHandler = ^{
     dispatch_async(dispatch_get_main_queue(), ^{
-      protected_download->Cancel(/*user_cancel=*/true);
+      [weak_canceller cancel];
     });
   };
 
   [progress publish];
 
-  download->SetUserData(
-      &kCrNSProgressUserDataKey,
-      std::make_unique<CrNSProgressUserData>(progress, destination_path));
+  download->SetUserData(&kCrNSProgressUserDataKey,
+                        std::make_unique<CrNSProgressUserData>(
+                            progress, canceller, destination_path));
 
   return static_cast<CrNSProgressUserData*>(
       download->GetUserData(&kCrNSProgressUserDataKey));
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in macOS DownloadStatusUpdater via NSProgress cancellation handler

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 security team.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the macOS implementation of download progress tracking. An asynchronous block captures a raw pointer to a DownloadItem, which can be dereferenced after the item has been destroyed, potentially leading to a browser process compromise.

Affected files:

  • chrome/browser/download/download_status_updater_mac.mm
  • chrome/browser/download/download_status_updater.h
  • components/download/internal/common/download_item_impl.cc

Estimated timestamp from git blame: 2022-03-08

Summary

A potential Use-After-Free (UAF) vulnerability exists in chrome/browser/download/download_status_updater_mac.mm due to the unsafe capture of a raw pointer in an asynchronous block.

The CreateOrGetNSProgress function sets the cancellationHandler for an NSProgress object, capturing a raw C++ download::DownloadItem* pointer by value. This handler is invoked by macOS on an arbitrary background thread when a user cancels a download via the Finder UI, and subsequently dispatches a task to the main GCD queue to call download->Cancel(). If the DownloadItem is destroyed on the main thread before the dispatched task executes, a UAF occurs.

Technical Details

In chrome/browser/download/download_status_updater_mac.mm at line 71:

  progress.cancellationHandler = ^{
    dispatch_async(dispatch_get_main_queue(), ^{
      download->Cancel(/*user_cancel=*/true);
    });
  };

The download pointer is captured as a raw pointer, circumventing Chromium’s safe pointer mechanisms. According to Apple’s documentation, the cancellationHandler runs on an arbitrary thread.

When a download is removed (e.g., via DownloadItemImpl::Remove()), the DownloadItem synchronously self-deletes. Although CrNSProgressUserData::~CrNSProgressUserData calls [progress_ unpublish], this does not revoke or cancel blocks that have already been enqueued via dispatch_async to the main GCD queue.

Since Cancel() is a virtual method (virtual void Cancel(bool user_cancel) = 0; in download_item.h), an attacker who can groom the heap to replace the freed DownloadItemImpl with controlled data can potentially achieve virtual table hijacking. This may lead to Remote Code Execution (RCE) within the highly privileged, unsandboxed browser process, effectively escaping the sandbox.

Potential Steps to Reproduce

Note: Our tooling agent doesn’t yet have the ability to run code. These are suggested steps to trigger the vulnerability based on static analysis.

  1. An attacker (e.g., via a malicious extension or website) initiates a large or slow file download to ensure the NSProgress remains active.
  2. In macOS Finder (icon view), the user clicks the (X) overlay on the file icon to cancel the download. This triggers the cancellationHandler on a background thread, which enqueues a Cancel() call to the main queue.
  3. Simultaneously, the attacker triggers the deletion of the DownloadItem on the main thread. This could be done by:
    • Using a Chrome extension with the downloads permission to call chrome.downloads.erase().
    • Closing the incognito window hosting the download.
    • Selecting “Remove from list” in chrome://downloads.
  4. When the main thread’s dispatch queue drains, it will execute download->Cancel(true) on the freed DownloadItemImpl object.

Suggested Fix

Do not capture a raw C++ pointer in an asynchronous Objective-C block.

Instead, capture a base::WeakPtr<download::DownloadItem> (if supported by the caller, which it currently is not for the abstract DownloadItem interface without casting) or capture the unique download ID (e.g., uint32_t download_id = download->GetId();) and perform a lookup via the DownloadManager inside the block executing on the main thread before calling Cancel().

Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368


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