CVE-2026-8551
Overview
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::Dataattached to theDownloadItemthat owns the associatedNSProgress.
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.
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
- Start a download in incognito
A download begins in an off-the-record profile, creating an
NSProgresswith a cancellation handler that (pre-patch) captures araw_ptrto theDownloadItem. - System retains the block
macOS holds the
NSProgress/block, potentially via XPC, beyond Chrome’s control. - Destroy the profile mid-download
The incognito profile is torn down while the download is in progress, freeing the
DownloadItemwhile the block (and its capturedraw_ptr) still exists. - Block released/executed on freed item
When the system later releases the
NSProgress(destroying the block) theraw_ptrdestructor detects freed memory and triggers aDanglingPtrcrash; had the handler fired it would dereference the freedDownloadItemviaCancel(use-after-free).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/download/download_status_updater_mac.mm |
modified | |
CrNSProgressUserDatachrome/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/closuresAudit Objective-C blocks (and callbacks handed to OS/XPC APIs) that capture
raw_ptr/this; their lifetime is not Chrome’s, so capture a__weakbridge orWeakPtrinstead. - `RAW_PTR_EXCLUSION` boundariesReview uses of
RAW_PTR_EXCLUSIONto confirm an explicit invalidation path exists (asCrNSProgressUserDataprovides) so the excluded pointer is nulled before the referent dies. - Incognito/profile teardown vs. OS-held stateTrace OS-registered objects (progress items, notifications, share sheets) tied to a
DownloadItem/profile to ensure they are unpublished and neutralized during profile destruction.
Patch
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));
Original Bug Report
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.mmchrome/browser/download/download_status_updater.hcomponents/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.
- An attacker (e.g., via a malicious extension or website) initiates a large or slow file download to ensure the
NSProgressremains active. - In macOS Finder (icon view), the user clicks the (X) overlay on the file icon to cancel the download. This triggers the
cancellationHandleron a background thread, which enqueues aCancel()call to the main queue. - Simultaneously, the attacker triggers the deletion of the
DownloadItemon the main thread. This could be done by:- Using a Chrome extension with the
downloadspermission to callchrome.downloads.erase(). - Closing the incognito window hosting the download.
- Selecting “Remove from list” in
chrome://downloads.
- Using a Chrome extension with the
- When the main thread’s dispatch queue drains, it will execute
download->Cancel(true)on the freedDownloadItemImplobject.
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.