High chrome UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in FileSystem
DescriptionUse after free in FileSystem
ComponentFileSystem
Sub-componentFSA
Bug ClassUAF
Tracker574911589
Fix commitcd4327123733 (chromium/src) +246/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-27

Background

File System Access (FSA)
the API letting web content read/write local files via FileSystemFileHandle, FileSystemDirectoryHandle, writers, and sync access handles. In the browser these are backed by FileSystemAccessManagerImpl (content/browser/file_system_access/file_system_access_manager_impl.cc).
Receiver containers
the manager owns file_receivers_/directory_receivers_ (mojo::UniqueReceiverSet) and writer_receivers_/access_handle_host_receivers_ (base::flat_set<std::unique_ptr<...>>). Each owned impl’s destructor can run side effects.
`FileSystemAccessLockManager` / `LockHandle`
manages exclusive/shared locks on files; writers and access handles hold scoped_refptr<LockHandle>s. Destroying a holder releases its lock.
`Lock::LockHandleDestroyed()` / lock promotion
when a held lock is released, the lock manager synchronously promotes pending lock requests and runs their TakeLockCallbacks, which land back on live FileSystemAccessFileHandleImpl instances.
BFCache + Incognito specifics
a lock held by an evicting back/forward-cache handle gets released during teardown; in Incognito, DidTakeAccessHandleLock() → DoOpenIncognitoFile() completes synchronously and calls CreateAccessHandleHost(), inserting into access_handle_host_receivers_.

Root Cause Analysis

The bug is a re-entrant use-after-free during ~FileSystemAccessManagerImpl(). Member destruction order meant access_handle_host_receivers_ and writer_receivers_ were destroyed before file_receivers_ and directory_receivers_. Destroying a writer or access-handle host releases its LockHandle; when that lock belonged to an evicting BFCache handle, Lock::LockHandleDestroyed() synchronously promoted a pending lock request and ran its callback on a still-alive FileSystemAccessFileHandleImpl. In Incognito that callback path (DidTakeAccessHandleLock → DoOpenIncognitoFile → CreateAccessHandleHost) completes synchronously and inserts a new element into access_handle_host_receivers_ while that very container is mid-destruction — writing into (or reallocating) a flat_set whose backing buffer is being, or has already been, freed. Separately, RemoveFileWriter() erased a writer from writer_receivers_ by key and let the erase run the destructor in place, so a lock-promotion callback fired during that erase could re-entrantly mutate the set being modified.

The patch removes the re-entrancy on three fronts. (1) In the header, writer_receivers_ and access_handle_host_receivers_ are moved to be declared before file_receivers_/directory_receivers_ so they are destroyed after them, and ~FileSystemAccessManagerImpl() now explicitly weak_factory_.InvalidateWeakPtrs() then file_receivers_.Clear(); directory_receivers_.Clear(); first, cancelling any pending callbacks before any lock is released. (2) TakeLock() wraps its TakeLockCallback in a lambda bound with weak_factory_.GetWeakPtr(); if the manager is gone, the callback runs with nullptr instead of touching freed state. (3) Teardown and RemoveFileWriter() now std::move each element out of its set and erase() the (now-empty) entry before the moved-out object’s destructor runs, so releasing locks cannot re-entrantly mutate a flat_set while it is being iterated/destroyed. The advisory’s Use after free in FileSystem is exactly what the diff addresses.

Key insight
Tearing down the FSA manager released locks held by writers/access-handle hosts before invalidating the file/directory handle callbacks, so synchronous lock promotion re-entered the manager (inserting into access_handle_host_receivers_ in Incognito) and used containers that were already being destroyed.

Attack Path

  1. Open handles A page (Incognito) opens FSA file handles and sync access handles, so the manager holds locks and populates access_handle_host_receivers_/writer_receivers_. (web-reachable via FSA API)
  2. Put frame in BFCache Navigate so the frame enters the back/forward cache while still holding access-handle locks.
  3. Contend From a new active frame, issue OpenAccessHandle requests on the same files, queuing pending lock requests behind the BFCache holder.
  4. Trigger teardown The manager is destroyed (e.g., eviction/shutdown); destroying writers/access-handle hosts releases locks, synchronously promoting the pending requests.
  5. Re-entrancy The promotion callback runs DidTakeAccessHandleLock → DoOpenIncognitoFile → CreateAccessHandleHost, inserting into access_handle_host_receivers_ while it is being destroyed (or into a freed buffer).
  6. Primitive Use-after-free / re-entrant mutation of a freed flat_set in the browser process. (escalation to RCE inferred)

Impact Assessment

Runs in the browser process, the highest-value target. The trigger is web-reachable: the FSA API is available to origins with permission, and the specific recipe (Incognito + BFCache-held access-handle locks + contending OpenAccessHandle requests, all exercised by the new regression tests) drives synchronous lock promotion during manager teardown. The primitive is a browser-process use-after-free (writing into / reallocating a destructed flat_set), which is a strong corruption primitive; reliable escalation to code execution is (inferred) and not shown by the diff. Preconditions (Incognito, BFCache eviction timing, lock contention) make it somewhat delicate but the tests demonstrate deterministic reproduction.

Changed Functions

FunctionChangeNotes
if
content/browser/file_system_access/file_system_access_manager_impl.cc
modified
for
content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
modified
TEST_F
content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
modified

Files Changed

  • content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
  • content/browser/file_system_access/file_system_access_manager_impl.h
  • content/browser/file_system_access/file_system_access_manager_impl.cc

Audit Directions

  • Destructor-order dependencies
    Audit other browser objects that own both lock-holding impls and callback receivers for member-order assumptions; destroying a lock holder before cancelling callbacks is the core hazard here.
  • Synchronous lock promotion
    Review every LockHandle release site (writers, access handles, directory handles) for callbacks that re-enter the owning manager synchronously, especially in Incognito where DoOpenIncognitoFile completes inline.
  • `flat_set`/`UniqueReceiverSet` mutation during destruction
    Look for erase/Clear paths where the element destructor can mutate the same container; the move-out-then-erase idiom should be used.
  • WeakPtr-guarded callbacks
    Check other FSA manager callbacks that outlive potential teardown but are not weak_factory_-guarded.
From cd43271237330d34795ca374997b23684001182a Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@chromium.org>
Date: Tue, 20 Oct 2026 04:30:58 -0700
Subject: [PATCH] [FSA] Prevent synchronous lock promotion UAF during manager teardown

During ~FileSystemAccessManagerImpl(), destroying
`access_handle_host_receivers_` and `writer_receivers_` before
`file_receivers_` and `directory_receivers_` releases held `LockHandle`s
while file and directory handles are still alive. When a lock held by an
evicting BFCache handle is destroyed, `Lock::LockHandleDestroyed()`
synchronously promotes pending lock requests and runs callbacks on live
`FileSystemAccessFileHandleImpl` instances. In Incognito mode,
`DidTakeAccessHandleLock()` -> `DoOpenIncognitoFile()` completes
synchronously and calls `CreateAccessHandleHost()`, inserting into
`access_handle_host_receivers_` while it is mid-destruction or after its
backing buffer has already been freed.

Fix this by:
1. Declaring `file_receivers_` and `directory_receivers_` after
   `writer_receivers_` and `access_handle_host_receivers_` and explicitly
   clearing them first in `~FileSystemAccessManagerImpl()`, after
   invalidating `weak_factory_`.
2. Guarding `TakeLock()` callbacks with `weak_factory_.GetWeakPtr()` so
   any lock promotion during manager teardown passes `nullptr`.
3. Moving elements out of `writer_receivers_` and
   `access_handle_host_receivers_` before erasing them during teardown
   and in `RemoveFileWriter()`.

TAG=agy
CONV=1c519f9c-078d-4200-8484-614956cd3f7d

Bug: 574911589, 562191732
Change-Id: I3c65bbd8e8f56ebec3995f81b9b6c0fa9b82e7e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8602672
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Auto-Submit: Eriko Kurimoto <elkurin@chromium.org>
Commit-Queue: Eriko Kurimoto <elkurin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1722787}
---
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 99467983..81de9393 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
@@ -573,6 +573,28 @@
 
 FileSystemAccessManagerImpl::~FileSystemAccessManagerImpl() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  // Invalidate weak pointers and destroy file and directory handle receivers
+  // first so that pending lock callbacks are cancelled before releasing locks
+  // held by writers or access handle hosts.
+  weak_factory_.InvalidateWeakPtrs();
+  file_receivers_.Clear();
+  directory_receivers_.Clear();
+
+  // Move each writer and access handle host out of its `flat_set` and erase the
+  // entry before running its destructor, so releasing locks cannot re-entrantly
+  // mutate a set while it is being destroyed.
+  while (!writer_receivers_.empty()) {
+    auto iter = writer_receivers_.begin();
+    auto writer_receiver = std::move(*iter);
+    writer_receivers_.erase(iter);
+  }
+
+  while (!access_handle_host_receivers_.empty()) {
+    auto iter = access_handle_host_receivers_.begin();
+    auto access_handle_host_receiver = std::move(*iter);
+    access_handle_host_receivers_.erase(iter);
+  }
 }
 
 void FileSystemAccessManagerImpl::BindReceiver(
@@ -1546,8 +1568,19 @@
     FileSystemAccessLockManager::LockType lock_type,
     FileSystemAccessLockManager::TakeLockCallback callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  lock_manager_->TakeLock(binding_context.frame_id, url, lock_type,
-                          std::move(callback));
+  lock_manager_->TakeLock(
+      binding_context.frame_id, url, lock_type,
+      base::BindOnce(
+          [](base::WeakPtr<FileSystemAccessManagerImpl> manager,
+             FileSystemAccessLockManager::TakeLockCallback callback,
+             scoped_refptr<FileSystemAccessLockManager::LockHandle> lock) {
+            if (!manager) {
+              std::move(callback).Run(nullptr);
+              return;
+            }
+            std::move(callback).Run(std::move(lock));
+          },
+          weak_factory_.GetWeakPtr(), std::move(callback)));
 }
 bool FileSystemAccessManagerImpl::IsContentious(
     const storage::FileSystemURL& url,
@@ -2147,9 +2180,19 @@
 void FileSystemAccessManagerImpl::RemoveFileWriter(
     FileSystemAccessFileWriterImpl* writer) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  CHECK(writer, base::NotFatalUntil::M159);
 
-  size_t count_removed = writer_receivers_.erase(writer);
-  CHECK_EQ(1u, count_removed, base::NotFatalUntil::M159);
+  // Move `writer` out of `writer_receivers_` and erase the entry before
+  // destroying it, since destroying a `FileSystemAccessFileWriterImpl`
+  // releases its locks and can synchronously trigger lock promotion callbacks.
+  auto iter = writer_receivers_.find(writer);
+  if (iter == writer_receivers_.end()) {
+    NOTREACHED(base::NotFatalUntil::M159);
+    return;
+  }
+
+  auto writer_receiver = std::move(*iter);
+  writer_receivers_.erase(iter);
 }
 
 void FileSystemAccessManagerImpl::RemoveAccessHandleHost(
diff --git a/content/browser/file_system_access/file_system_access_manager_impl.h b/content/browser/file_system_access/file_system_access_manager_impl.h
index 690cc437..f36b3611 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl.h
+++ b/content/browser/file_system_access/file_system_access_manager_impl.h
@@ -704,24 +704,33 @@
   // and `access_handle_host_receivers_`. The locks held by file writers and
   // access handles dereference the lock manager on destruction, so it should
   // outlive them.
+  //
+  // `writer_receivers_` and `access_handle_host_receivers_` should be
+  // destroyed after `file_receivers_` and `directory_receivers_`. Destroying
+  // file writers and access handle hosts releases their locks, which can
+  // synchronously promote pending lock requests and invoke callbacks on live
+  // file or directory handles. Destroying `file_receivers_` and
+  // `directory_receivers_` first invalidates those callbacks before any locks
+  // are released.
   scoped_refptr<FileSystemAccessLockManager> lock_manager_
       GUARDED_BY_CONTEXT(sequence_checker_);
 
   FileSystemAccessWatcherManager watcher_manager_
       GUARDED_BY_CONTEXT(sequence_checker_);
 
+  base::flat_set<std::unique_ptr<FileSystemAccessFileWriterImpl>,
+                 base::UniquePtrComparator>
+      writer_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
+  base::flat_set<std::unique_ptr<FileSystemAccessAccessHandleHostImpl>,
+                 base::UniquePtrComparator>
+      access_handle_host_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
+
   // All the receivers for file and directory handles that have references to
   // them.
   mojo::UniqueReceiverSet<blink::mojom::FileSystemAccessFileHandle>
       file_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
   mojo::UniqueReceiverSet<blink::mojom::FileSystemAccessDirectoryHandle>
       directory_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
-  base::flat_set<std::unique_ptr<FileSystemAccessFileWriterImpl>,
-                 base::UniquePtrComparator>
-      writer_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
-  base::flat_set<std::unique_ptr<FileSystemAccessAccessHandleHostImpl>,
-                 base::UniquePtrComparator>
-      access_handle_host_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
 
   const bool off_the_record_;
 
diff --git a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
index 4ebd55f..3311d6a 100644
--- a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
@@ -38,6 +38,7 @@
 #include "content/browser/file_system_access/features.h"
 #include "content/browser/file_system_access/fixed_file_system_access_permission_grant.h"
 #include "content/browser/file_system_access/mock_file_system_access_permission_grant.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/public/browser/file_system_access_permission_context.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_task_environment.h"
@@ -46,6 +47,7 @@
 #include "content/test/test_web_contents.h"
 #include "file_system_access_directory_handle_impl.h"
 #include "mock_file_system_access_permission_context.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/io_buffer.h"
 #include "net/base/mime_util.h"
 #include "net/base/net_errors.h"
@@ -964,6 +966,188 @@
   EXPECT_TRUE(access_handle_remote.is_valid());
 }
 
+TEST_F(FileSystemAccessAccessHandleIncognitoTest,
+       ManagerDestructionWithPendingOpenAccessHandle) {
+  auto* bfcache_rfh =
+      static_cast<RenderFrameHostImpl*>(web_contents_->GetPrimaryMainFrame());
+  const FileSystemAccessManagerImpl::BindingContext bfcache_context(
+      test_src_storage_key_, test_src_url_, bfcache_rfh->GetGlobalId());
+  const FileSystemAccessManagerImpl::SharedHandleState shared_handle_state(
+      allow_grant_, allow_grant_);
+
+  constexpr size_t kNumFiles = 4;
+  std::vector<FileSystemURL> file_urls;
+  std::vector<mojo::Remote<blink::mojom::FileSystemAccessFileHandle>>
+      bfcache_file_handles;
+  std::vector<
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+      held_access_handles;
+
+  auto [initial_result, initial_file, initial_access_handle_remote] =
+      OpenAccessHandle(
+          blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite);
+  ASSERT_EQ(initial_result->status, FileSystemAccessStatus::kOk);
+  ASSERT_TRUE(initial_access_handle_remote.is_valid());
+  held_access_handles.push_back(std::move(initial_access_handle_remote));
+
+  for (size_t i = 0; i < kNumFiles; ++i) {
+    auto safe_name = base::SafeBaseName::Create(
+        base::FilePath::FromUTF8Unsafe(base::StringPrintf("file_%zu", i)));
+    ASSERT_TRUE(safe_name.has_value());
+    FileSystemURL url = test_file_url_.CreateSibling(*safe_name);
+    ASSERT_EQ(base::File::FILE_OK, storage::AsyncFileTestHelper::CreateFile(
+                                       file_system_context_.get(), url));
+    file_urls.push_back(url);
+
+    mojo::Remote<blink::mojom::FileSystemAccessFileHandle> file_handle(
+        manager_->CreateFileHandle(bfcache_context, url,
+                                   base::StringPrintf("file_%zu", i),
+                                   shared_handle_state));
+    base::test::TestFuture<
+        blink::mojom::FileSystemAccessErrorPtr,
+        blink::mojom::FileSystemAccessAccessHandleFilePtr,
+        mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+        open_future;
+    file_handle->OpenAccessHandle(
+        blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+        open_future.GetCallback());
+    auto [result, file, access_handle_remote] = open_future.Take();
+    ASSERT_EQ(result->status, FileSystemAccessStatus::kOk);
+    ASSERT_TRUE(access_handle_remote.is_valid());
+    bfcache_file_handles.push_back(std::move(file_handle));
+    held_access_handles.push_back(std::move(access_handle_remote));
+  }
+
+  bfcache_rfh->SetLifecycleState(
+      RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+  EXPECT_FALSE(bfcache_rfh->is_evicted_from_back_forward_cache());
+
+  WebContents* active_web_contents =
+      web_contents_factory_.CreateWebContents(&browser_context_);
+  static_cast<TestWebContents*>(active_web_contents)
+      ->NavigateAndCommit(test_src_url_);
+  const FileSystemAccessManagerImpl::BindingContext active_context(
+      test_src_storage_key_, test_src_url_,
+      active_web_contents->GetPrimaryMainFrame()->GetGlobalId());
+
+  std::vector<mojo::Remote<blink::mojom::FileSystemAccessFileHandle>>
+      active_file_handles;
+  std::vector<std::unique_ptr<base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      blink::mojom::FileSystemAccessAccessHandleFilePtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>>>
+      pending_futures;
+
+  for (size_t i = 0; i < kNumFiles; ++i) {
+    mojo::Remote<blink::mojom::FileSystemAccessFileHandle> file_handle(
+        manager_->CreateFileHandle(active_context, file_urls[i],
+                                   base::StringPrintf("file_%zu", i),
+                                   shared_handle_state));
+    auto pending_future = std::make_unique<base::test::TestFuture<
+        blink::mojom::FileSystemAccessErrorPtr,
+        blink::mojom::FileSystemAccessAccessHandleFilePtr,
+        mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>>();
+    file_handle->OpenAccessHandle(
+        blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+        pending_future->GetCallback());
+    file_handle.FlushForTesting();
+    EXPECT_FALSE(pending_future->IsReady());
+    active_file_handles.push_back(std::move(file_handle));
+    pending_futures.push_back(std::move(pending_future));
+  }
+
+  // Also queue a pending OpenAccessHandle request on a standalone handle
+  // owned outside `manager_->file_receivers_`.
+  auto standalone_handle = std::make_unique<FileSystemAccessFileHandleImpl>(
+      manager_.get(), active_context, test_file_url_, "test",
+      shared_handle_state);
+  base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      blink::mojom::FileSystemAccessAccessHandleFilePtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+      standalone_future;
+  standalone_handle->OpenAccessHandle(
+      blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+      standalone_future.GetCallback());
+  EXPECT_TRUE(bfcache_rfh->is_evicted_from_back_forward_cache());
+  ASSERT_FALSE(standalone_future.IsReady());
+
+  // Destroying the manager while access handles are held by a BFCache frame
+  // and contending OpenAccessHandle requests are pending must not re-entrantly
+  // mutate `access_handle_host_receivers_` during its destruction.
+  handle_.reset();
+  manager_.reset();
+
+  ASSERT_TRUE(standalone_future.IsReady());
+  EXPECT_EQ(std::get<0>(standalone_future.Take())->status,
+            FileSystemAccessStatus::kNoModificationAllowedError);
+  standalone_handle.reset();
+}
+
+TEST_F(FileSystemAccessAccessHandleIncognitoTest,
+       ManagerDestructionWithPendingOpenAccessHandleAndFileWriter) {
+  auto* bfcache_rfh =
+      static_cast<RenderFrameHostImpl*>(web_contents_->GetPrimaryMainFrame());
+  const FileSystemAccessManagerImpl::SharedHandleState shared_handle_state(
+      allow_grant_, allow_grant_);
+
+  // Open and close an access handle first so `access_handle_host_receivers_`
+  // retains non-zero capacity while empty when the manager is later destroyed.
+  {
+    auto [warmup_result, warmup_file, warmup_access_handle_remote] =
+        OpenAccessHandle(
+            blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite);
+    ASSERT_EQ(warmup_result->status, FileSystemAccessStatus::kOk);
+    ASSERT_TRUE(warmup_access_handle_remote.is_valid());
+    mojo::Remote<blink::mojom::FileSystemAccessAccessHandleHost> warmup_remote(
+        std::move(warmup_access_handle_remote));
+    base::test::TestFuture<void> close_future;
+    warmup_remote->Close(close_future.GetCallback());
+    EXPECT_TRUE(close_future.Wait());
+  }
+
+  base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
+      writer_future;
+  handle_->CreateFileWriter(
+      /*keep_existing_data=*/false, /*auto_close=*/false,
+      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kExclusive,
+      writer_future.GetCallback());
+  auto [writer_result, writer_remote] = writer_future.Take();
+  ASSERT_EQ(writer_result->status, FileSystemAccessStatus::kOk);
+  ASSERT_TRUE(writer_remote.is_valid());
+
+  bfcache_rfh->SetLifecycleState(
+      RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+
+  WebContents* active_web_contents =
+      web_contents_factory_.CreateWebContents(&browser_context_);
+  static_cast<TestWebContents*>(active_web_contents)
+      ->NavigateAndCommit(test_src_url_);
+  const FileSystemAccessManagerImpl::BindingContext active_context(
+      test_src_storage_key_, test_src_url_,
+      active_web_contents->GetPrimaryMainFrame()->GetGlobalId());
+
+  mojo::Remote<blink::mojom::FileSystemAccessFileHandle> active_file_handle(
+      manager_->CreateFileHandle(active_context, test_file_url_, "test",
+                                 shared_handle_state));
+  base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      blink::mojom::FileSystemAccessAccessHandleFilePtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+      pending_future;
+  active_file_handle->OpenAccessHandle(
+      blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+      pending_future.GetCallback());
+  active_file_handle.FlushForTesting();
+  EXPECT_FALSE(pending_future.IsReady());
+  EXPECT_TRUE(bfcache_rfh->is_evicted_from_back_forward_cache());
+
+  handle_.reset();
+  manager_.reset();
+}
+
 TEST_F(FileSystemAccessAccessHandleTest, OpenAccessHandleLockModes_Readwrite) {
   // For a file with an open access handle in READWRITE mode, no other access
   // handles can be open.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
index 4ebd55f..3311d6a 100644
--- a/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_file_handle_impl_unittest.cc
@@ -38,6 +38,7 @@
 #include "content/browser/file_system_access/features.h"
 #include "content/browser/file_system_access/fixed_file_system_access_permission_grant.h"
 #include "content/browser/file_system_access/mock_file_system_access_permission_grant.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/public/browser/file_system_access_permission_context.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_task_environment.h"
@@ -46,6 +47,7 @@
 #include "content/test/test_web_contents.h"
 #include "file_system_access_directory_handle_impl.h"
 #include "mock_file_system_access_permission_context.h"
+#include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/io_buffer.h"
 #include "net/base/mime_util.h"
 #include "net/base/net_errors.h"
@@ -964,6 +966,188 @@
   EXPECT_TRUE(access_handle_remote.is_valid());
 }
 
+TEST_F(FileSystemAccessAccessHandleIncognitoTest,
+       ManagerDestructionWithPendingOpenAccessHandle) {
+  auto* bfcache_rfh =
+      static_cast<RenderFrameHostImpl*>(web_contents_->GetPrimaryMainFrame());
+  const FileSystemAccessManagerImpl::BindingContext bfcache_context(
+      test_src_storage_key_, test_src_url_, bfcache_rfh->GetGlobalId());
+  const FileSystemAccessManagerImpl::SharedHandleState shared_handle_state(
+      allow_grant_, allow_grant_);
+
+  constexpr size_t kNumFiles = 4;
+  std::vector<FileSystemURL> file_urls;
+  std::vector<mojo::Remote<blink::mojom::FileSystemAccessFileHandle>>
+      bfcache_file_handles;
+  std::vector<
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+      held_access_handles;
+
+  auto [initial_result, initial_file, initial_access_handle_remote] =
+      OpenAccessHandle(
+          blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite);
+  ASSERT_EQ(initial_result->status, FileSystemAccessStatus::kOk);
+  ASSERT_TRUE(initial_access_handle_remote.is_valid());
+  held_access_handles.push_back(std::move(initial_access_handle_remote));
+
+  for (size_t i = 0; i < kNumFiles; ++i) {
+    auto safe_name = base::SafeBaseName::Create(
+        base::FilePath::FromUTF8Unsafe(base::StringPrintf("file_%zu", i)));
+    ASSERT_TRUE(safe_name.has_value());
+    FileSystemURL url = test_file_url_.CreateSibling(*safe_name);
+    ASSERT_EQ(base::File::FILE_OK, storage::AsyncFileTestHelper::CreateFile(
+                                       file_system_context_.get(), url));
+    file_urls.push_back(url);
+
+    mojo::Remote<blink::mojom::FileSystemAccessFileHandle> file_handle(
+        manager_->CreateFileHandle(bfcache_context, url,
+                                   base::StringPrintf("file_%zu", i),
+                                   shared_handle_state));
+    base::test::TestFuture<
+        blink::mojom::FileSystemAccessErrorPtr,
+        blink::mojom::FileSystemAccessAccessHandleFilePtr,
+        mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+        open_future;
+    file_handle->OpenAccessHandle(
+        blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+        open_future.GetCallback());
+    auto [result, file, access_handle_remote] = open_future.Take();
+    ASSERT_EQ(result->status, FileSystemAccessStatus::kOk);
+    ASSERT_TRUE(access_handle_remote.is_valid());
+    bfcache_file_handles.push_back(std::move(file_handle));
+    held_access_handles.push_back(std::move(access_handle_remote));
+  }
+
+  bfcache_rfh->SetLifecycleState(
+      RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+  EXPECT_FALSE(bfcache_rfh->is_evicted_from_back_forward_cache());
+
+  WebContents* active_web_contents =
+      web_contents_factory_.CreateWebContents(&browser_context_);
+  static_cast<TestWebContents*>(active_web_contents)
+      ->NavigateAndCommit(test_src_url_);
+  const FileSystemAccessManagerImpl::BindingContext active_context(
+      test_src_storage_key_, test_src_url_,
+      active_web_contents->GetPrimaryMainFrame()->GetGlobalId());
+
+  std::vector<mojo::Remote<blink::mojom::FileSystemAccessFileHandle>>
+      active_file_handles;
+  std::vector<std::unique_ptr<base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      blink::mojom::FileSystemAccessAccessHandleFilePtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>>>
+      pending_futures;
+
+  for (size_t i = 0; i < kNumFiles; ++i) {
+    mojo::Remote<blink::mojom::FileSystemAccessFileHandle> file_handle(
+        manager_->CreateFileHandle(active_context, file_urls[i],
+                                   base::StringPrintf("file_%zu", i),
+                                   shared_handle_state));
+    auto pending_future = std::make_unique<base::test::TestFuture<
+        blink::mojom::FileSystemAccessErrorPtr,
+        blink::mojom::FileSystemAccessAccessHandleFilePtr,
+        mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>>();
+    file_handle->OpenAccessHandle(
+        blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+        pending_future->GetCallback());
+    file_handle.FlushForTesting();
+    EXPECT_FALSE(pending_future->IsReady());
+    active_file_handles.push_back(std::move(file_handle));
+    pending_futures.push_back(std::move(pending_future));
+  }
+
+  // Also queue a pending OpenAccessHandle request on a standalone handle
+  // owned outside `manager_->file_receivers_`.
+  auto standalone_handle = std::make_unique<FileSystemAccessFileHandleImpl>(
+      manager_.get(), active_context, test_file_url_, "test",
+      shared_handle_state);
+  base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      blink::mojom::FileSystemAccessAccessHandleFilePtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+      standalone_future;
+  standalone_handle->OpenAccessHandle(
+      blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+      standalone_future.GetCallback());
+  EXPECT_TRUE(bfcache_rfh->is_evicted_from_back_forward_cache());
+  ASSERT_FALSE(standalone_future.IsReady());
+
+  // Destroying the manager while access handles are held by a BFCache frame
+  // and contending OpenAccessHandle requests are pending must not re-entrantly
+  // mutate `access_handle_host_receivers_` during its destruction.
+  handle_.reset();
+  manager_.reset();
+
+  ASSERT_TRUE(standalone_future.IsReady());
+  EXPECT_EQ(std::get<0>(standalone_future.Take())->status,
+            FileSystemAccessStatus::kNoModificationAllowedError);
+  standalone_handle.reset();
+}
+
+TEST_F(FileSystemAccessAccessHandleIncognitoTest,
+       ManagerDestructionWithPendingOpenAccessHandleAndFileWriter) {
+  auto* bfcache_rfh =
+      static_cast<RenderFrameHostImpl*>(web_contents_->GetPrimaryMainFrame());
+  const FileSystemAccessManagerImpl::SharedHandleState shared_handle_state(
+      allow_grant_, allow_grant_);
+
+  // Open and close an access handle first so `access_handle_host_receivers_`
+  // retains non-zero capacity while empty when the manager is later destroyed.
+  {
+    auto [warmup_result, warmup_file, warmup_access_handle_remote] =
+        OpenAccessHandle(
+            blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite);
+    ASSERT_EQ(warmup_result->status, FileSystemAccessStatus::kOk);
+    ASSERT_TRUE(warmup_access_handle_remote.is_valid());
+    mojo::Remote<blink::mojom::FileSystemAccessAccessHandleHost> warmup_remote(
+        std::move(warmup_access_handle_remote));
+    base::test::TestFuture<void> close_future;
+    warmup_remote->Close(close_future.GetCallback());
+    EXPECT_TRUE(close_future.Wait());
+  }
+
+  base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessFileWriter>>
+      writer_future;
+  handle_->CreateFileWriter(
+      /*keep_existing_data=*/false, /*auto_close=*/false,
+      blink::mojom::FileSystemAccessWritableFileStreamLockMode::kExclusive,
+      writer_future.GetCallback());
+  auto [writer_result, writer_remote] = writer_future.Take();
+  ASSERT_EQ(writer_result->status, FileSystemAccessStatus::kOk);
+  ASSERT_TRUE(writer_remote.is_valid());
+
+  bfcache_rfh->SetLifecycleState(
+      RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+
+  WebContents* active_web_contents =
+      web_contents_factory_.CreateWebContents(&browser_context_);
+  static_cast<TestWebContents*>(active_web_contents)
+      ->NavigateAndCommit(test_src_url_);
+  const FileSystemAccessManagerImpl::BindingContext active_context(
+      test_src_storage_key_, test_src_url_,
+      active_web_contents->GetPrimaryMainFrame()->GetGlobalId());
+
+  mojo::Remote<blink::mojom::FileSystemAccessFileHandle> active_file_handle(
+      manager_->CreateFileHandle(active_context, test_file_url_, "test",
+                                 shared_handle_state));
+  base::test::TestFuture<
+      blink::mojom::FileSystemAccessErrorPtr,
+      blink::mojom::FileSystemAccessAccessHandleFilePtr,
+      mojo::PendingRemote<blink::mojom::FileSystemAccessAccessHandleHost>>
+      pending_future;
+  active_file_handle->OpenAccessHandle(
+      blink::mojom::FileSystemAccessAccessHandleLockMode::kReadwrite,
+      pending_future.GetCallback());
+  active_file_handle.FlushForTesting();
+  EXPECT_FALSE(pending_future.IsReady());
+  EXPECT_TRUE(bfcache_rfh->is_evicted_from_back_forward_cache());
+
+  handle_.reset();
+  manager_.reset();
+}
+
 TEST_F(FileSystemAccessAccessHandleTest, OpenAccessHandleLockModes_Readwrite) {
   // For a file with an open access handle in READWRITE mode, no other access
   // handles can be open.
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.