CVE-2026-11630
Overview
Background
- File System Access API
- a web platform API, driven from the renderer, that lets a page request a native file/directory picker;
FileSystemAccessManagerImplis its browser-process backend. - `ShowFilePickerOnUIThread`
- the browser-process routine (the hunk’s enclosing function, not named in the
@@header) that validates the calling frame and then shows the OS file chooser. - Nested message loop
- a re-entrant run loop spun by a synchronous operation (here a permission check or a fullscreen-drop) during which other tasks, including frame/
WebContentsdestruction, can run. - `GlobalRenderFrameHostId`
- a stable identifier from which a live
RenderFrameHost*can be re-resolved viaRenderFrameHost::FromID, as opposed to a cached raw pointer that can dangle.
Root Cause Analysis
ShowFilePickerOnUIThread cached raw RenderFrameHost* (rfh), WebContents* (web_contents), and outermost-frame pointers, then called intermediate operations — CanShowFilePicker() and WebContents::ForSecurityDropFullscreen() — that spin nested message loops. During those loops the calling frame could be navigated away or destroyed (and with it the WebContents), leaving the cached pointers dangling, yet the code continued to dereference them (e.g. web_contents->ForSecurityDropFullscreen(...) and later FileSystemChooser::CreateAndShow() on the stale rfh). The invariant violated is that a raw pointer held across a re-entrant yield point must be revalidated before reuse.
The fix adds ResolveAndValidateFrame(frame_id), which re-resolves the frame from the stable GlobalRenderFrameHostId and checks rfh, web_contents, and outermost_rfh for non-null and IsActive(); it is called after CanShowFilePicker() and again after ForSecurityDropFullscreen(), aborting with kOperationAborted if resolution fails and otherwise refreshing the cached pointers. This works because every use after a yield point now operates on freshly validated pointers or not at all.
WebContents pointers across nested run loops that can destroy those very objects; the fix re-resolves and revalidates them from the stable GlobalRenderFrameHostId after each re-entrant call and bails out safely when they are gone.Attack Path
- Invoke the picker
Web content in a frame with transient user activation calls
showOpenFilePicker()(or a related File System Access method), routing toShowFilePickerOnUIThreadin the browser process. - Yield into a nested loop
The intermediate
CanShowFilePicker()permission check (orForSecurityDropFullscreen()) spins a nested message loop, creating a window for other UI-thread tasks to run. - Destroy the frame
During that window the calling frame/
WebContentsis navigated or detached and destroyed (the regression test models this by destroying theWebContentsinsideCanShowFilePicker), freeing the objects the cached pointers refer to. - Use after free
Execution resumes and dereferences the dangling
web_contents/rfh(e.g.ForSecurityDropFullscreenorFileSystemChooser::CreateAndShow), reading freed memory (inferred downstream consequence beyond the crash the test demonstrates).
Impact Assessment
WebContents/RenderFrameHost in the browser process by racing frame destruction against the nested loops. The vendor advisory rates this critical; it does not report in-the-wild exploitation. Any escalation beyond the browser-process UAF/crash (controlled reuse of the freed object, code execution) is (inferred) and not shown by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/file_system_access/file_system_access_manager_impl.cc |
modified |
Files Changed
content/browser/file_system_access/file_system_access_manager_impl.cccontent/browser/file_system_access/file_system_access_manager_impl_unittest.cc
Audit Directions
- Raw pointers held across re-entrant yieldsaudit browser-process code that caches
RenderFrameHost*/WebContents*and then calls permission prompts, fullscreen/modal operations, or anything spinning a nested run loop, and ensure revalidation after each. - Stable-ID re-resolutionprefer re-resolving from
GlobalRenderFrameHostId/GlobalRenderFrameHostTokenwithIsActive()checks over reusing cached pointers; flag sites that cache once and reuse later. - Multi-stage picker/chooser flowsreview other File System Access and chooser paths (serial, HID, Bluetooth, print) for the same cache-then-yield-then-deref pattern across their intermediate async steps.
Patch
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,
Regression Test / PoC
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 =
Original Bug Report
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.cccontent/browser/file_system_access/file_system_chooser.cccontent/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:
- Resolving the associated
WebContentsviaWebContents::FromRenderFrameHost(render_frame_host), which callsrfh->delegate(). - 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:
- An attacker page (Parent) contains a same-origin subframe (Iframe).
- The user interacts with the page, entering HTML fullscreen.
- The user interacts with the iframe, invoking the file picker API (
window.showOpenFilePicker()). - The browser UI thread enters
ShowFilePickerOnUIThread, resolvesrfhto the iframe’sRenderFrameHost, and callsForSecurityDropFullscreenwhich spins a nested message loop. - 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’sRenderFrameHost. ShowFilePickerOnUIThreadresumes and invokesFileSystemChooser::CreateAndShowwith the freedrfhpointer.
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.
Recommended Remediation
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.