CVE-2026-11630
Overview
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
Patch
From eb0b9aba64dbe619399fb2e4e1be6a784524c809 Mon Sep 17 00:00:00 2001 From: Ming-Ying Chung <[email protected]> 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 <[email protected]> Commit-Queue: Ming-Ying Chung <[email protected]> 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.