Critical watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in File Input
DescriptionUse after free in File Input
ComponentFile Input
Sub-componentFSA
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516677924
Fix commiteb0b9aba64db (chromium/src) +130/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-05-28 (public git)
Disclosed2026-06-08
Patch gap11 days — fix public in source before the advisory

Background

File System Access API
a web platform API, driven from the renderer, that lets a page request a native file/directory picker; FileSystemAccessManagerImpl is 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/WebContents destruction, can run.
`GlobalRenderFrameHostId`
a stable identifier from which a live RenderFrameHost* can be re-resolved via RenderFrameHost::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.

Key insight
The code trusted raw frame/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

  1. Invoke the picker Web content in a frame with transient user activation calls showOpenFilePicker() (or a related File System Access method), routing to ShowFilePickerOnUIThread in the browser process.
  2. Yield into a nested loop The intermediate CanShowFilePicker() permission check (or ForSecurityDropFullscreen()) spins a nested message loop, creating a window for other UI-thread tasks to run.
  3. Destroy the frame During that window the calling frame/WebContents is navigated or detached and destroyed (the regression test models this by destroying the WebContents inside CanShowFilePicker), freeing the objects the cached pointers refer to.
  4. Use after free Execution resumes and dereferences the dangling web_contents/rfh (e.g. ForSecurityDropFullscreen or FileSystemChooser::CreateAndShow), reading freed memory (inferred downstream consequence beyond the crash the test demonstrates).

Impact Assessment

An attacker controlling web content (reachable from a stock build when the frame has the user activation the picker requires) can trigger a use-after-free of 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

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

Audit Directions

  • Raw pointers held across re-entrant yields
    audit 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-resolution
    prefer re-resolving from GlobalRenderFrameHostId/GlobalRenderFrameHostToken with IsActive() checks over reusing cached pointers; flag sites that cache once and reuse later.
  • Multi-stage picker/chooser flows
    review 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.
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