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
Tracker562191732
Fix commitcd4327123733 (chromium/src) +246/-10
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-27

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
From cd43271237330d34795ca374997b23684001182a Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <[email protected]>
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 <[email protected]>
Auto-Submit: Eriko Kurimoto <[email protected]>
Commit-Queue: Eriko Kurimoto <[email protected]>
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.