Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Downloads
DescriptionUse after free in Downloads
ComponentDownloads
Bug ClassUAF
Tracker498376171
Fix commitf46add2e465a (chromium/src) +56/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

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
From f46add2e465aa336135befca7062424adf43d60f Mon Sep 17 00:00:00 2001
From: Xizhe Chen <chenxizhe.ultron@gmail.com>
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 <yuyisi@chromium.org>
Reviewed-by: Yisi Yu <yuyisi@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
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 rj...@google.com

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