Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Downloads
DescriptionUse after free in Downloads
ComponentDownloads
Bug ClassUAF
Tracker515462244
Fix commit3268f8dd3e24 (chromium/src) +21/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
if
chrome/browser/download/android/dangerous_download_dialog_bridge.cc
modified
if
chrome/browser/download/android/policy_warning_download_dialog_bridge.cc
modified

Files Changed

  • chrome/browser/download/android/dangerous_download_dialog_bridge.cc
  • chrome/browser/download/android/download_controller.cc
  • chrome/browser/download/android/download_controller.h
  • chrome/browser/download/android/policy_warning_download_dialog_bridge.cc
From 3268f8dd3e2431833508afeb1626b925bb0ecaa5 Mon Sep 17 00:00:00 2001
From: Brian Lefler <[email protected]>
Date: Fri, 05 Jun 2026 15:56:19 -0700
Subject: [PATCH] Schedule download removal on the UI thread for Android bridges

DangerousDownloadDialogBridge and PolicyWarningDownloadDialogBridge
were synchronously removing DownloadItems when no WindowAndroid was
present.

This CL exposes DownloadController::ScheduleRemoveDownloadItem and
uses it to defer the destruction of the DownloadItem.

TAG=agy

Bug: 515462244
Change-Id: I0ca91ba7c62e1768310eee1c657a70ddd9b49dfc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7905754
Reviewed-by: Yaw Frempong <[email protected]>
Reviewed-by: Xinghui Lu <[email protected]>
Commit-Queue: Brian Lefler <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1642658}
---

diff --git a/chrome/browser/download/android/dangerous_download_dialog_bridge.cc b/chrome/browser/download/android/dangerous_download_dialog_bridge.cc
index 7887ee4..89c5362 100644
--- a/chrome/browser/download/android/dangerous_download_dialog_bridge.cc
+++ b/chrome/browser/download/android/dangerous_download_dialog_bridge.cc
@@ -13,6 +13,7 @@
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/android/android_theme_resources.h"
 #include "chrome/browser/android/resource_mapper.h"
+#include "chrome/browser/download/android/download_controller.h"
 #include "chrome/browser/download/android/download_dialog_utils.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/url_formatter/elide_url.h"
@@ -62,7 +63,7 @@
     return;
   }
   if (!window_android) {
-    download_item->Remove();
+    DownloadController::ScheduleRemoveDownloadItem(download_item);
     return;
   }
   download_item->AddObserver(this);
@@ -111,7 +112,7 @@
       &download_items_, download_guid);
   if (download) {
     download->RemoveObserver(this);
-    download->Remove();
+    DownloadController::ScheduleRemoveDownloadItem(download);
   }
 }
 
diff --git a/chrome/browser/download/android/download_controller.cc b/chrome/browser/download/android/download_controller.cc
index 97b0f08..1206b47 100644
--- a/chrome/browser/download/android/download_controller.cc
+++ b/chrome/browser/download/android/download_controller.cc
@@ -135,15 +135,6 @@
   }
 }
 
-void ScheduleRemoveDownloadItem(download::DownloadItem* download) {
-  auto download_manager_getter = std::make_unique<DownloadManagerGetter>(
-      content::DownloadItemUtils::GetBrowserContext(download)
-          ->GetDownloadManager());
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE,
-      base::BindOnce(&RemoveDownloadItem, std::move(download_manager_getter),
-                     download->GetGuid()));
-}
 
 bool ShouldOpenPdfInline(DownloadItem* item) {
   BrowserContext* context = content::DownloadItemUtils::GetBrowserContext(item);
@@ -225,6 +216,18 @@
 
 }  // namespace
 
+// static
+void DownloadController::ScheduleRemoveDownloadItem(
+    download::DownloadItem* item) {
+  auto download_manager_getter = std::make_unique<DownloadManagerGetter>(
+      content::DownloadItemUtils::GetBrowserContext(item)
+          ->GetDownloadManager());
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE,
+      base::BindOnce(&RemoveDownloadItem, std::move(download_manager_getter),
+                     item->GetGuid()));
+}
+
 static void JNI_DownloadController_CancelDownload(
     JNIEnv* env,
     Profile* profile,
diff --git a/chrome/browser/download/android/download_controller.h b/chrome/browser/download/android/download_controller.h
index 954b0ba..943c4eb2 100644
--- a/chrome/browser/download/android/download_controller.h
+++ b/chrome/browser/download/android/download_controller.h
@@ -64,6 +64,9 @@
   static void CloseTabIfEmpty(content::WebContents* web_contents,
                               download::DownloadItem* download);
 
+  // Schedules the removal of a download item on the UI thread.
+  static void ScheduleRemoveDownloadItem(download::DownloadItem* download);
+
   // Callback when user permission prompt finishes. Args: whether file access
   // permission is acquired, which permission to update.
   using AcquirePermissionCallback =
diff --git a/chrome/browser/download/android/policy_warning_download_dialog_bridge.cc b/chrome/browser/download/android/policy_warning_download_dialog_bridge.cc
index 33aadf3..04c38d8 100644
--- a/chrome/browser/download/android/policy_warning_download_dialog_bridge.cc
+++ b/chrome/browser/download/android/policy_warning_download_dialog_bridge.cc
@@ -13,6 +13,7 @@
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/android/android_theme_resources.h"
 #include "chrome/browser/android/resource_mapper.h"
+#include "chrome/browser/download/android/download_controller.h"
 #include "chrome/browser/download/android/download_dialog_utils.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/url_formatter/elide_url.h"
@@ -44,7 +45,7 @@
     return;
   }
   if (!window_android) {
-    download_item->Remove();
+    DownloadController::ScheduleRemoveDownloadItem(download_item);
     return;
   }
   download_item->AddObserver(this);
@@ -84,7 +85,7 @@
       &download_items_, download_guid);
   if (download) {
     download->RemoveObserver(this);
-    download->Remove();
+    DownloadController::ScheduleRemoveDownloadItem(download);
   }
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

Potential Browser-process UAF in DownloadItemImpl due to nested UpdateObservers() re-entrancy

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A Use-After-Free (UAF) vulnerability in the browser process occurs when a DownloadItemImpl is synchronously removed during an observer notification loop. A logic error in the handling of a safety flag allows the object’s destruction to bypass a critical check, leading to memory corruption when the notification loop continues execution on freed memory.

Affected files:

  • components/download/internal/common/download_item_impl.cc
  • content/browser/download/download_manager_impl.cc
  • chrome/browser/download/android/download_controller.cc
  • chrome/browser/download/android/dangerous_download_dialog_bridge.cc

Estimated timestamp from git blame: 2017-08-17

Description

A potential Use-After-Free (UAF) vulnerability exists in DownloadItemImpl due to improper management of the is_updating_observers_ state flag during re-entrant calls to UpdateObservers(). This can allow a DownloadItemImpl object to be destroyed synchronously while an active iteration over its observers is still on the stack, leading to subsequent memory access on the freed object.

Technical Details

In components/download/internal/common/download_item_impl.cc, the UpdateObservers() method uses the is_updating_observers_ boolean flag to guard against re-entrancy and to ensure the object is not destroyed during observer notification. On entry, the flag is set to true, and on exit, it is unconditionally reset to false (line 556).

A vulnerability arises when a nested call to UpdateObservers() occurs. This happens if an observer synchronously triggers the Remove() method of the DownloadItemImpl. The Remove() method itself calls UpdateObservers() (the nested call). When this nested call completes, it resets the is_updating_observers_ flag to false.

Remove() then proceeds to notify its delegate (DownloadManagerImpl), which erases the unique_ptr owning the DownloadItemImpl (content/browser/download/download_manager_impl.cc:1049), triggering the destructor. The destructor ~DownloadItemImpl() contains a CHECK(!is_updating_observers_) designed to prevent destruction during iteration. However, because the nested UpdateObservers() call already reset the flag to false, this check passes, and the object is freed.

When the stack unwinds back to the original (outer) UpdateObservers() call, the code continues execution using the freed this pointer:

  1. The range-for loop continues, accessing the destroyed ObserverList (line 554).
  2. The code performs a UAF write: this->is_updating_observers_ = false (line 556).
  3. Control returns to the original caller (e.g., OnTargetResolved), which continues accessing the freed object (e.g., calling MaybeCompleteDownload()).

Potential Trigger (Android)

On Chrome for Android, a synchronous path to this UAF exists. During target determination, if a download is identified as dangerous, DownloadController::OnDownloadUpdated is called. If the associated WebContents is detached from its WindowAndroid (e.g., during tab manipulation), DangerousDownloadDialogBridge::Show is invoked with a null window, which synchronously calls item->Remove() (chrome/browser/download/android/dangerous_download_dialog_bridge.cc:65).

Suggested Potential Steps to Reproduce

  1. Initiate a download of a file type that triggers a dangerous file warning (e.g., an .apk).
  2. Manipulate the browser state (e.g., by detaching or closing the tab) at the exact moment target determination completes (OnTargetResolved).
  3. This triggers the synchronous removal path on Android, causing the UAF in the browser process.

Note: These are potential steps; our tooling agent does not yet have the ability to run code or provide a functional PoC.

Suggested Fix

To prevent this issue, the is_updating_observers_ flag should be managed in a way that respects nesting (e.g., using a re-entrancy counter or base::AutoReset<bool>). Additionally, the destruction of DownloadItemImpl should be deferred if a notification loop is currently active on the stack. The Remove() implementation should be audited to ensure that deletion via the delegate is never performed synchronously during an observer update.

Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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