Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in File Input
DescriptionUse after free in File Input
ComponentFile Input
Bug ClassUAF
Tracker516677924
Fix commiteb0b9aba64db (chromium/src) +130/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
content/browser/file_system_access/file_system_access_manager_impl.cc
modified

Files Changed

  • content/browser/file_system_access/file_system_access_manager_impl.cc
  • content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
From eb0b9aba64dbe619399fb2e4e1be6a784524c809 Mon Sep 17 00:00:00 2001
From: Ming-Ying Chung <mych@chromium.org>
Date: Thu, 28 May 2026 00:41:24 -0700
Subject: [PATCH] [FSA] Fix UAF in ShowFilePickerOnUIThread.

The `ShowFilePickerOnUIThread()` function in the browser process was
caching raw pointers to `RenderFrameHost` and `WebContents` which could
become dangling if the frame was destroyed during nested message loops
spun by intermediate operations like `CanShowFilePicker()` or
`ForSecurityDropFullscreen()`.

Th CL re-resolves the RenderFrameHost and WebContents pointers from
the stored `GlobalRenderFrameHostId` after these operations and aborts
safely if they are no longer active or valid.

Bug: 516677924
Change-Id: I50046d8f72f139f06f885309847b5179a32b7b37
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7871715
Reviewed-by: Fergal Daly <fergal@chromium.org>
Commit-Queue: Ming-Ying Chung <mych@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637522}
---

diff --git a/content/browser/file_system_access/file_system_access_manager_impl.cc b/content/browser/file_system_access/file_system_access_manager_impl.cc
index e40ce0c..51049e7 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl.cc
+++ b/content/browser/file_system_access/file_system_access_manager_impl.cc
@@ -19,6 +19,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/i18n/file_util_icu.h"
+#include "base/memory/raw_ptr.h"
 #include "base/notreached.h"
 #include "base/strings/string_util.h"
 #include "base/strings/string_view_util.h"
@@ -96,6 +97,40 @@
 constexpr char kThirdPartyIframesNotAllowedToShowFilePicker[] =
     "Third party iframes are not allowed to show a file picker.";
 
+// Holds resolved and validated frame objects. All pointers are guaranteed to be
+// non-null and active if this struct is returned.
+struct ResolvedFrame {
+  raw_ptr<RenderFrameHost> rfh;
+  raw_ptr<WebContents> web_contents;
+  raw_ptr<RenderFrameHost> outermost_rfh;
+};
+
+// Resolves `frame_id` to its corresponding `RenderFrameHost`, `WebContents`,
+// and outermost `RenderFrameHost`, and validates that they are all non-null
+// and active.
+// Returns a `ResolvedFrame` struct if ALL resolved objects are valid and
+// active, i.e. non-null; otherwise, returns `std::nullopt`.
+//
+// This check is critical because intermediate operations, like permission
+// checks or security prompts, can run nested message loops during which the
+// calling frame or WebContents can be destroyed or navigated.
+std::optional<ResolvedFrame> ResolveAndValidateFrame(
+    GlobalRenderFrameHostId frame_id) {
+  RenderFrameHost* rfh = RenderFrameHost::FromID(frame_id);
+  if (!rfh || !rfh->IsActive()) {
+    return std::nullopt;
+  }
+  WebContents* web_contents = WebContents::FromRenderFrameHost(rfh);
+  if (!web_contents) {
+    return std::nullopt;
+  }
+  RenderFrameHost* outermost_rfh = rfh->GetOutermostMainFrame();
+  if (!outermost_rfh || !outermost_rfh->IsActive()) {
+    return std::nullopt;
+  }
+  return ResolvedFrame{rfh, web_contents, outermost_rfh};
+}
+
 #if BUILDFLAG(IS_ANDROID)
 // Adaptor between FileSystemChooser::ResultCallback and FileSelectListener
 // used when delegating file choosing to WebContentsDelegate.
@@ -207,6 +242,18 @@
                           {});
                       return;
                     });
+    // `CanShowFilePicker()` runs nested message loops during which the frame
+    // could be destroyed or navigated. Re-resolve and re-validate the frame.
+    auto re_resolved = ResolveAndValidateFrame(frame_id);
+    if (!re_resolved) {
+      std::move(callback).Run(file_system_access_error::FromStatus(
+                                  FileSystemAccessStatus::kOperationAborted),
+                              {});
+      return;
+    }
+    rfh = re_resolved->rfh;
+    web_contents = re_resolved->web_contents;
+    outermost_rfh = re_resolved->outermost_rfh;
   }
 
 #if BUILDFLAG(IS_ANDROID)
@@ -247,12 +294,20 @@
 
   auto blocker =
       web_contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);
-  if (!blocker) {
+
+  // `ForSecurityDropFullscreen()` can also run nested message loops. Re-resolve
+  // and re-validate the frame, and ensure the fullscreen blocker was
+  // successfully acquired.
+  auto post_fullscreen_resolved = ResolveAndValidateFrame(frame_id);
+  if (!post_fullscreen_resolved || !blocker) {
     std::move(callback).Run(file_system_access_error::FromStatus(
                                 FileSystemAccessStatus::kOperationAborted),
                             {});
     return;
   }
+  rfh = post_fullscreen_resolved->rfh;
+  web_contents = post_fullscreen_resolved->web_contents;
+  outermost_rfh = post_fullscreen_resolved->outermost_rfh;
 
   FileSystemChooser::ScopedObjects scoped_objects(
       /*fullscreen_block=*/std::move(*blocker),
diff --git a/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc b/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
index 904349e..b6037546 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
@@ -1608,6 +1608,80 @@
   ASSERT_TRUE(future.Wait());
 }
 
+// Covers the re-entrancy and lifetime safety of `RenderFrameHost` and
+// `WebContents` in `ShowFilePickerOnUIThread`. Specifically, it covers the
+// scenario where the frame is detached/destroyed during the
+// `CanShowFilePicker` permission check.
+//
+// Without the fix, this scenario triggers a crash when the code attempts to
+// access the destroyed `WebContents` to drop fullscreen
+// `web_contents->ForSecurityDropFullscreen(...)`.
+//
+// Note: If destruction occurred during the fullscreen exit loop instead, the
+// crash would occur in `FileSystemChooser::CreateAndShow()` when dereferencing
+// the dangling RenderFrameHost.
+TEST_F(FileSystemAccessManagerImplTest,
+       ChooseEntries_CanShowFilePickerDestroysFrameUAF) {
+  static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame())
+      ->SimulateUserActivation();
+
+  mojo::Remote<blink::mojom::FileSystemAccessManager> manager_remote;
+  FileSystemAccessManagerImpl::BindingContext binding_context = {
+      kTestStorageKey, kTestURL,
+      web_contents_->GetPrimaryMainFrame()->GetGlobalId()};
+  manager_->BindReceiver(binding_context,
+                         manager_remote.BindNewPipeAndPassReceiver());
+
+  EXPECT_CALL(permission_context_,
+              CanObtainReadPermission(kTestStorageKey.origin()))
+      .WillOnce(testing::Return(true));
+
+  EXPECT_CALL(
+      permission_context_,
+      GetWellKnownDirectoryPath(blink::mojom::WellKnownDirectory::kDirDocuments,
+                                kTestStorageKey.origin()))
+      .WillOnce(testing::Return(base::FilePath()));
+  EXPECT_CALL(permission_context_,
+              GetLastPickedDirectory(kTestStorageKey.origin(), std::string()))
+      .WillOnce(testing::Return(PathInfo()));
+  EXPECT_CALL(permission_context_, GetPickerTitle(testing::_))
+      .WillOnce(testing::Return(std::u16string()));
+
+  // Mock CanShowFilePicker to synchronously destroy the WebContents, which
+  // destroys the RFH. This simulates the frame being detached/destroyed
+  // during the yielding permission check.
+  EXPECT_CALL(permission_context_, CanShowFilePicker(testing::_))
+      .WillOnce([&](content::RenderFrameHost* rfh) {
+        auto* temp_wc = web_contents_.get();
+        web_contents_ = nullptr;
+        web_contents_factory_.DestroyWebContents(temp_wc);
+        return base::ok();
+      });
+
+  auto open_file_picker_options = blink::mojom::OpenFilePickerOptions::New(
+      blink::mojom::AcceptsTypesInfo::New(
+          std::vector<blink::mojom::ChooseFileSystemEntryAcceptsOptionPtr>(),
+          /*include_accepts_all=*/true),
+      /*can_select_multiple_files=*/false);
+  auto picker_options = blink::mojom::FilePickerOptions::New(
+      blink::mojom::TypeSpecificFilePickerOptionsUnion::
+          NewOpenFilePickerOptions(std::move(open_file_picker_options)),
+      /*starting_directory_id=*/std::string(),
+      blink::mojom::FilePickerStartInOptionsUnionPtr());
+
+  base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr,
+                         std::vector<blink::mojom::FileSystemAccessEntryPtr>>
+      future;
+  manager_remote->ChooseEntries(std::move(picker_options),
+                                future.GetCallback());
+  ASSERT_TRUE(future.Wait());
+
+  // The call should be aborted gracefully since the frame was destroyed.
+  // Without the fix, this would have crashed during the picker call.
+  EXPECT_EQ(blink::mojom::FileSystemAccessStatus::kOperationAborted,
+            future.Get<0>()->status);
+}
+
 TEST_F(FileSystemAccessManagerImplTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc b/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
index 904349e..b6037546 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_manager_impl_unittest.cc
@@ -1608,6 +1608,80 @@
   ASSERT_TRUE(future.Wait());
 }
 
+// Covers the re-entrancy and lifetime safety of `RenderFrameHost` and
+// `WebContents` in `ShowFilePickerOnUIThread`. Specifically, it covers the
+// scenario where the frame is detached/destroyed during the
+// `CanShowFilePicker` permission check.
+//
+// Without the fix, this scenario triggers a crash when the code attempts to
+// access the destroyed `WebContents` to drop fullscreen
+// `web_contents->ForSecurityDropFullscreen(...)`.
+//
+// Note: If destruction occurred during the fullscreen exit loop instead, the
+// crash would occur in `FileSystemChooser::CreateAndShow()` when dereferencing
+// the dangling RenderFrameHost.
+TEST_F(FileSystemAccessManagerImplTest,
+       ChooseEntries_CanShowFilePickerDestroysFrameUAF) {
+  static_cast<TestRenderFrameHost*>(web_contents_->GetPrimaryMainFrame())
+      ->SimulateUserActivation();
+
+  mojo::Remote<blink::mojom::FileSystemAccessManager> manager_remote;
+  FileSystemAccessManagerImpl::BindingContext binding_context = {
+      kTestStorageKey, kTestURL,
+      web_contents_->GetPrimaryMainFrame()->GetGlobalId()};
+  manager_->BindReceiver(binding_context,
+                         manager_remote.BindNewPipeAndPassReceiver());
+
+  EXPECT_CALL(permission_context_,
+              CanObtainReadPermission(kTestStorageKey.origin()))
+      .WillOnce(testing::Return(true));
+
+  EXPECT_CALL(
+      permission_context_,
+      GetWellKnownDirectoryPath(blink::mojom::WellKnownDirectory::kDirDocuments,
+                                kTestStorageKey.origin()))
+      .WillOnce(testing::Return(base::FilePath()));
+  EXPECT_CALL(permission_context_,
+              GetLastPickedDirectory(kTestStorageKey.origin(), std::string()))
+      .WillOnce(testing::Return(PathInfo()));
+  EXPECT_CALL(permission_context_, GetPickerTitle(testing::_))
+      .WillOnce(testing::Return(std::u16string()));
+
+  // Mock CanShowFilePicker to synchronously destroy the WebContents, which
+  // destroys the RFH. This simulates the frame being detached/destroyed
+  // during the yielding permission check.
+  EXPECT_CALL(permission_context_, CanShowFilePicker(testing::_))
+      .WillOnce([&](content::RenderFrameHost* rfh) {
+        auto* temp_wc = web_contents_.get();
+        web_contents_ = nullptr;
+        web_contents_factory_.DestroyWebContents(temp_wc);
+        return base::ok();
+      });
+
+  auto open_file_picker_options = blink::mojom::OpenFilePickerOptions::New(
+      blink::mojom::AcceptsTypesInfo::New(
+          std::vector<blink::mojom::ChooseFileSystemEntryAcceptsOptionPtr>(),
+          /*include_accepts_all=*/true),
+      /*can_select_multiple_files=*/false);
+  auto picker_options = blink::mojom::FilePickerOptions::New(
+      blink::mojom::TypeSpecificFilePickerOptionsUnion::
+          NewOpenFilePickerOptions(std::move(open_file_picker_options)),
+      /*starting_directory_id=*/std::string(),
+      blink::mojom::FilePickerStartInOptionsUnionPtr());
+
+  base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr,
+                         std::vector<blink::mojom::FileSystemAccessEntryPtr>>
+      future;
+  manager_remote->ChooseEntries(std::move(picker_options),
+                                future.GetCallback());
+  ASSERT_TRUE(future.Wait());
+
+  // The call should be aborted gracefully since the frame was destroyed.
+  // Without the fix, this would have crashed during the picker call.
+  EXPECT_EQ(blink::mojom::FileSystemAccessStatus::kOperationAborted,
+            future.Get<0>()->status);
+}
+
 TEST_F(FileSystemAccessManagerImplTest,
        ChooseEntries_CrossOriginDenialDoesNotConsumeActivation) {
   auto* rfh =
Loading diff…

Original Bug Report

reported by vm...@google.com

Browser-Process Use-After-Free in ShowFilePickerOnUIThread

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 potential Use-After-Free (UAF) vulnerability exists in the browser process within ShowFilePickerOnUIThread. The function resolves and retains a raw RenderFrameHost* pointer across a call to ForSecurityDropFullscreen, which can synchronously spin a nested message loop (e.g., during Cocoa fullscreen transition animations on macOS). If the requesting subframe is detached and destroyed during this nested loop, subsequent dereferences of the dangling raw pointer can lead to browser-process memory corruption.

Affected files:

  • content/browser/file_system_access/file_system_access_manager_impl.cc
  • content/browser/file_system_access/file_system_chooser.cc
  • content/browser/web_contents_based_canceller.cc

Estimated timestamp from git blame: 2025-11-07

Summary and Root Cause

A potential Use-After-Free (UAF) vulnerability has been identified in content/browser/file_system_access/file_system_access_manager_impl.cc within the ShowFilePickerOnUIThread function.

The function obtains a raw, stack-allocated RenderFrameHost* pointer (rfh) from a GlobalRenderFrameHostId at the beginning of the call:

RenderFrameHost* rfh = RenderFrameHost::FromID(frame_id);

It then retains this raw pointer across a call to ForSecurityDropFullscreen:

auto blocker = web_contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);

ForSecurityDropFullscreen initiates a fullscreen exit for the target window or tab. On macOS, this calls AppKit’s native -[NSWindow toggleFullScreen:] inside the Cocoa window bridge, which synchronously spins a nested message loop while running the transition animation. Because the browser process continues processing incoming IPC messages during this nested loop, if the renderer sends a detachment IPC for a subframe (e.g., if the parent frame removes the iframe), the subframe’s RenderFrameHost is immediately destroyed and freed.

Once the fullscreen transition completes and the nested message loop exits, ShowFilePickerOnUIThread resumes execution. However, the raw stack pointer rfh is now dangling. It is subsequently passed to FileSystemChooser::CreateAndShow:

FileSystemChooser::CreateAndShow(rfh, options, std::move(callback), std::move(scoped_objects));

Dereference Mechanism

Inside FileSystemChooser::CreateAndShow, multiple dereferences of the dangling pointer occur:

  1. Resolving the associated WebContents via WebContents::FromRenderFrameHost(render_frame_host), which calls rfh->delegate().
  2. In WebContentsBasedCanceller::WebContentsBasedCanceller, the code performs virtual function dispatch on the freed memory to get a weak document pointer:
document_(render_frame_host->GetWeakDocumentPtr())

Since rfh is a raw stack-allocated pointer and not a class member variable, it is not protected by MiraclePtr/BackupRefPtr (BRP). Because the browser process runs unsandboxed with full privileges, performing a virtual method call on freed memory could potentially be leveraged to hijack control flow.

Potential Trigger Scenario

An attacker might attempt to trigger this issue using same-origin iframe interactions and transition races:

  1. An attacker page (Parent) contains a same-origin subframe (Iframe).
  2. The user interacts with the page, entering HTML fullscreen.
  3. The user interacts with the iframe, invoking the file picker API (window.showOpenFilePicker()).
  4. The browser UI thread enters ShowFilePickerOnUIThread, resolves rfh to the iframe’s RenderFrameHost, and calls ForSecurityDropFullscreen which spins a nested message loop.
  5. During the nested loop, the parent page removes the iframe from the DOM (iframe.remove()). The frame detachment IPC is processed immediately, destroying the iframe’s RenderFrameHost.
  6. ShowFilePickerOnUIThread resumes and invokes FileSystemChooser::CreateAndShow with the freed rfh pointer.

Note: These are suggested potential steps; our current security analysis tooling does not have the capability to run code or verify the exploitability of this flow dynamically.

To remediate this issue, the code must re-resolve the RenderFrameHost from its GlobalRenderFrameHostId immediately after exiting any operation that could spin a nested message loop, such as ForSecurityDropFullscreen. If the re-resolved pointer is null, the operation must abort safely.

auto blocker = web_contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);
if (!blocker) {
  std::move(callback).Run(file_system_access_error::FromStatus(
                              FileSystemAccessStatus::kOperationAborted),
                          {});
  return;
}

// Re-resolve RFH to ensure it wasn't detached during the nested loop
rfh = RenderFrameHost::FromID(frame_id);
if (!rfh) {
  std::move(callback).Run(file_system_access_error::FromStatus(
                              FileSystemAccessStatus::kOperationAborted),
                          {});
  return;
}

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


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