Chrome · FileSystem › FSA
CVE-2026-113248
Memory Corruption in FileSystem (FSA)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/file_system_access/chrome_file_system_access_permission_context.cc |
modified | |
ifcontent/browser/file_system_access/file_system_access_handle_base.cc |
modified | |
DropFullscreenDelegatechrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc |
modified | |
ifchrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc |
modified | |
BindLambdaForTestingchrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc |
modified |
Files Changed
content/browser/file_system_access/file_system_access_handle_base_unittest.ccchrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.ccchrome/browser/file_system_access/chrome_file_system_access_permission_context.cccontent/browser/file_system_access/file_system_access_handle_base.hcontent/browser/file_system_access/file_system_access_handle_base.cc
Patch
From 51db79da065d84597d7e2410fdada352ec1832e2 Mon Sep 17 00:00:00 2001
From: Ming-Ying Chung <mych@chromium.org>
Date: Thu, 17 Sep 2026 22:25:24 -0700
Subject: [PATCH] [FSA] Preserve lifetime and revalidate state in FSA permission requests
Exiting fullscreen mode during a File System Access permission request
can invoke nested message loops, during which the associated frame,
WebContents, or permission context may navigate or be destroyed. In
addition, references held by the handle could be released during
nested message processing, causing the grant instance to be destructed
while still executing on the call stack.
This CL maintains a scoped_refptr reference on the stack in
`PermissionGrantImpl::RequestPermission()` and
`PermissionGrantImpl::SetPath()` to guarantee object lifetime across
nested processing. It also re-resolves and validates the
RenderFrameHost, WebContents, origin, and permission request manager
after dropping fullscreen before proceeding.
Bug: 554560940
Change-Id: I0179fb6364a17d4aae9851a7814994a441af7e22
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8377117
Reviewed-by: Mingyu Lei <leimy@chromium.org>
Commit-Queue: Ming-Ying Chung <mych@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700489}
---
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
index 7d834cc..a6bd272 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
@@ -914,7 +914,7 @@
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/40101962): Determine if this should return denied for
// guard block, and how ancestor permission should be handled.
- if (status_ == PermissionStatus::ASK &&
+ if (status_ == PermissionStatus::ASK && context_ &&
context_->CanAutoGrantViaPersistentPermission(origin_, path_info_.path,
handle_type_, type_)) {
return PermissionStatus::GRANTED;
@@ -942,6 +942,7 @@
UserActivationState user_activation_state,
base::OnceCallback<void(PermissionRequestOutcome)> callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ scoped_refptr<PermissionGrantImpl> self(this);
// Check if a permission request has already been processed previously. This
// check is done first because we don't want to reset the status of a
@@ -1116,12 +1117,33 @@
// Drop fullscreen mode so that the user sees the URL bar.
auto blocker = web_contents->ForSecurityDropFullscreen(
/*display_id=*/display::kInvalidDisplayId);
- if (!blocker) {
+
+ // Exiting fullscreen can run nested message loops during which the
+ // permission context may be destroyed, or concurrent operations may have
+ // already resolved or changed the grant status away from `ASK` (e.g. user
+ // accepted/denied another prompt or permission was revoked/granted). Abort
+ // if the prompt is no longer needed or if the context is gone.
+ if (!blocker || !context_ ||
+ GetActivePermissionStatus() != PermissionStatus::ASK) {
RunCallbackAndRecordPermissionRequestOutcome(
std::move(callback), PermissionRequestOutcome::kRequestAborted);
return;
}
+ // Exiting fullscreen can run nested message loops during which the frame,
+ // WebContents, or permission manager could be destroyed or navigated.
+ // Re-resolve and validate before proceeding.
+ base::expected<FileSystemAccessPermissionRequestManager*,
+ PermissionRequestOutcome>
+ request_manager_or_error =
+ RevalidateRequestManagerAfterFullscreenDrop(frame_id);
+ if (!request_manager_or_error.has_value()) {
+ RunCallbackAndRecordPermissionRequestOutcome(
+ std::move(callback), request_manager_or_error.error());
+ return;
+ }
+ request_manager = *request_manager_or_error;
+
if (context_->IsEligibleToUpgradePermissionRequestToRestorePrompt(
origin_, path_info_.path, handle_type_, user_action_, type_)) {
std::vector<FileRequestData> request_data_list =
@@ -1527,6 +1549,7 @@
void SetPath(const content::PathInfo& new_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ scoped_refptr<PermissionGrantImpl> self(this);
if (path_info_ == new_path) {
return;
@@ -1534,8 +1557,8 @@
path_info_ = new_path;
- if (base::FeatureList::IsEnabled(
- features::kFileSystemAccessPersistentPermissions)) {
+ if (context_ && base::FeatureList::IsEnabled(
+ features::kFileSystemAccessPersistentPermissions)) {
const std::unique_ptr<Object> object = context_->GetGrantedObject(
origin_, PathAsPermissionKey(path_info_.path));
if (object) {
@@ -1552,6 +1575,56 @@
NotifyPermissionStatusChanged();
}
+ private:
+ // Re-validates the RenderFrameHost, WebContents, embedding origin, and
+ // permission request manager after exiting fullscreen mode.
+ //
+ // Exiting fullscreen mode can invoke nested modal message loops on some
+ // platforms. During these loops, events such as tab closure or navigation
+ // can asynchronously destroy the RenderFrameHost, WebContents, or the
+ // permission manager.
+ //
+ // Returns the active `FileSystemAccessPermissionRequestManager` on success,
+ // or a `PermissionRequestOutcome` error if the frame or context became
+ // invalid.
+ base::expected<FileSystemAccessPermissionRequestManager*,
+ PermissionRequestOutcome>
+ RevalidateRequestManagerAfterFullscreenDrop(
+ content::GlobalRenderFrameHostId frame_id) const {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+ content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
+ if (!rfh ||
+ rfh->IsInactiveAndDisallowActivation(
+ content::DisallowActivationReasonId::
+ kFileSystemAccessPermissionRequest) ||
+ rfh->IsNestedWithinFencedFrame()) {
+ return base::unexpected(PermissionRequestOutcome::kInvalidFrame);
+ }
+
+ content::WebContents* web_contents =
+ content::WebContents::FromRenderFrameHost(rfh);
+ if (!web_contents) {
+ return base::unexpected(PermissionRequestOutcome::kInvalidFrame);
+ }
+
+ url::Origin embedding_origin = url::Origin::Create(
+ permissions::PermissionUtil::GetLastCommittedOriginAsURL(
+ rfh->GetMainFrame()));
+ if (embedding_origin != origin_ ||
+ rfh->GetStorageKey().IsThirdPartyContext()) {
+ return base::unexpected(PermissionRequestOutcome::kThirdPartyContext);
+ }
+
+ auto* request_manager =
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents);
+ if (!request_manager) {
+ return base::unexpected(PermissionRequestOutcome::kRequestAborted);
+ }
+
+ return request_manager;
+ }
+
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtr<ChromeFileSystemAccessPermissionContext> const context_;
diff --git a/content/browser/file_system_access/file_system_access_handle_base.cc b/content/browser/file_system_access/file_system_access_handle_base.cc
index 49e529a..fb155c66 100644
--- a/content/browser/file_system_access/file_system_access_handle_base.cc
+++ b/content/browser/file_system_access/file_system_access_handle_base.cc
@@ -171,23 +171,30 @@
return;
}
+ scoped_refptr<FileSystemAccessPermissionGrant> read_grant =
+ handle_state_.read_grant;
+ scoped_refptr<FileSystemAccessPermissionGrant> write_grant =
+ handle_state_.write_grant;
+ const GlobalRenderFrameHostId frame_id = context().frame_id;
+ base::WeakPtr<FileSystemAccessHandleBase> weak_this = AsWeakPtr();
+
// 1. Request "read"-only permission.
if (mode == blink::mojom::FileSystemAccessPermissionMode::kRead) {
- handle_state_.read_grant->RequestPermission(
- context().frame_id,
+ read_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
- AsWeakPtr(), mode, std::move(callback)));
+ weak_this, mode, std::move(callback)));
return;
}
// 2. Request "write"-only permission.
if (mode == blink::mojom::FileSystemAccessPermissionMode::kWrite) {
- handle_state_.write_grant->RequestPermission(
- context().frame_id,
+ write_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
- AsWeakPtr(), mode, std::move(callback)));
+ weak_this, mode, std::move(callback)));
return;
}
@@ -200,31 +207,34 @@
// write permission status.
if (GetReadPermissionStatus() == PermissionStatus::ASK &&
GetWritePermissionStatus() == PermissionStatus::ASK) {
- handle_state_.read_grant->RequestPermission(
- context().frame_id,
+ read_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::DoNothing());
- handle_state_.write_grant->RequestPermission(
- context().frame_id,
+ if (!weak_this) {
+ return;
+ }
+ write_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
- AsWeakPtr(), mode, std::move(callback)));
+ weak_this, mode, std::move(callback)));
return;
}
if (GetReadPermissionStatus() == PermissionStatus::ASK) {
- handle_state_.read_grant->RequestPermission(
- context().frame_id,
+ read_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
- AsWeakPtr(), mode, std::move(callback)));
+ weak_this, mode, std::move(callback)));
return;
}
if (GetWritePermissionStatus() == PermissionStatus::ASK) {
- handle_state_.write_grant->RequestPermission(
- context().frame_id,
+ write_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
- AsWeakPtr(), mode, std::move(callback)));
+ weak_this, mode, std::move(callback)));
return;
}
NOTREACHED();
@@ -235,21 +245,23 @@
// the write permission request probably fails the same way. And we check
// the final permission status after the permission request completes
// anyway.
- handle_state_.read_grant->RequestPermission(
- context().frame_id,
+ read_grant->RequestPermission(
+ frame_id,
FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::DoNothing());
+ if (!weak_this) {
+ return;
+ }
}
// When the standalone write mode feature is disabled, we don't need to
// check the write permission status as it's always requested with read
// permission, i.e. only `GetReadWritePermissionStatus()` is valid here, and
// it must be ASK.
- handle_state_.write_grant->RequestPermission(
- context().frame_id,
- FileSystemAccessPermissionGrant::UserActivationState::kRequired,
+ write_grant->RequestPermission(
+ frame_id, FileSystemAccessPermissionGrant::UserActivationState::kRequired,
base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
- AsWeakPtr(), mode, std::move(callback)));
+ weak_this, mode, std::move(callback)));
// Both read & write permission is either granted or denied at this point.
}
diff --git a/content/browser/file_system_access/file_system_access_handle_base.h b/content/browser/file_system_access/file_system_access_handle_base.h
index 221b0b5c0..0f9a96f 100644
--- a/content/browser/file_system_access/file_system_access_handle_base.h
+++ b/content/browser/file_system_access/file_system_access_handle_base.h
@@ -56,7 +56,7 @@
FileSystemAccessHandleBase(const FileSystemAccessHandleBase&) = delete;
FileSystemAccessHandleBase& operator=(const FileSystemAccessHandleBase&) =
delete;
- ~FileSystemAccessHandleBase();
+ virtual ~FileSystemAccessHandleBase();
const storage::FileSystemURL& url() const { return url_; }
const SharedHandleState& handle_state() const { return handle_state_; }
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
index 0c8a2d7..cea167929 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
@@ -45,10 +45,12 @@
#include "components/permissions/permission_util.h"
#include "components/safe_browsing/buildflags.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/browser/fullscreen_types.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition_config.h"
#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
@@ -3599,6 +3601,129 @@
EXPECT_TRUE(permission_context()->HasExtendedPermissionForTesting(
kTestOrigin, kTestPathInfo, HandleType::kFile, GrantType::kWrite));
}
+// Simulates a delegate that performs custom actions, e.g. destroying
+// WebContents, releasing permission grants, or navigating frames, synchronously
+// when `ExitFullscreenModeForTab()` is invoked during
+// `ForSecurityDropFullscreen()`.
+class DropFullscreenDelegate : public content::WebContentsDelegate {
+ public:
+ explicit DropFullscreenDelegate(base::OnceClosure on_exit)
+ : on_exit_(std::move(on_exit)) {}
+
+ content::FullscreenState GetFullscreenState(
+ const content::WebContents* web_contents) const override {
+ content::FullscreenState state;
+ state.target_mode = content::FullscreenMode::kContent;
+ return state;
+ }
+
+ void ExitFullscreenModeForTab(content::WebContents* web_contents) override {
+ if (on_exit_) {
+ std::move(on_exit_).Run();
+ }
+ }
+
+ private:
+ base::OnceClosure on_exit_;
+};
+
+// Tests that `RequestPermission()` cleanly aborts without crashing if the
+// associated `WebContents` is destroyed while dropping fullscreen.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenExitDestroysWebContents) {
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+
+ DropFullscreenDelegate delegate(
+ base::BindLambdaForTesting([&]() { web_contents_.reset(); }));
+ web_contents_->SetDelegate(&delegate);
+
+ base::test::TestFuture<PermissionRequestOutcome> future;
+ grant->RequestPermission(frame_id(), UserActivationState::kRequired,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), PermissionRequestOutcome::kRequestAborted);
+}
+
+// Tests that `RequestPermission()` does not encounter a UAF if the caller drops
+// its reference to the grant while exiting fullscreen.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenReleasesGrantReference) {
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+ auto* raw_grant = grant.get();
+
+ // Dropping the grant here simulates the handle being destroyed (e.g. pipe
+ // disconnection) while exiting fullscreen, releasing the last external
+ // strong reference to the executing `PermissionGrantImpl`.
+ DropFullscreenDelegate delegate(
+ base::BindLambdaForTesting([&]() { grant.reset(); }));
+ web_contents_->SetDelegate(&delegate);
+
+ base::test::TestFuture<PermissionRequestOutcome> future;
+ raw_grant->RequestPermission(frame_id(), UserActivationState::kRequired,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), PermissionRequestOutcome::kUserGranted);
+ web_contents_->SetDelegate(nullptr);
+}
+
+// Tests that `RequestPermission()` safely detects frame navigation occurring
+// during fullscreen drop and aborts with `kInvalidFrame`.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenNavigatesFrame) {
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+
+ const url::Origin other_origin =
+ url::Origin::Create(GURL("https://other.com/"));
+ DropFullscreenDelegate delegate(base::BindLambdaForTesting([&]() {
+ content::WebContentsTester::For(web_contents())
+ ->NavigateAndCommit(other_origin.GetURL());
+ }));
+ web_contents_->SetDelegate(&delegate);
+
... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
index 0c8a2d7..cea167929 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
@@ -45,10 +45,12 @@
#include "components/permissions/permission_util.h"
#include "components/safe_browsing/buildflags.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "content/public/browser/fullscreen_types.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition_config.h"
#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
@@ -3599,6 +3601,129 @@
EXPECT_TRUE(permission_context()->HasExtendedPermissionForTesting(
kTestOrigin, kTestPathInfo, HandleType::kFile, GrantType::kWrite));
}
+// Simulates a delegate that performs custom actions, e.g. destroying
+// WebContents, releasing permission grants, or navigating frames, synchronously
+// when `ExitFullscreenModeForTab()` is invoked during
+// `ForSecurityDropFullscreen()`.
+class DropFullscreenDelegate : public content::WebContentsDelegate {
+ public:
+ explicit DropFullscreenDelegate(base::OnceClosure on_exit)
+ : on_exit_(std::move(on_exit)) {}
+
+ content::FullscreenState GetFullscreenState(
+ const content::WebContents* web_contents) const override {
+ content::FullscreenState state;
+ state.target_mode = content::FullscreenMode::kContent;
+ return state;
+ }
+
+ void ExitFullscreenModeForTab(content::WebContents* web_contents) override {
+ if (on_exit_) {
+ std::move(on_exit_).Run();
+ }
+ }
+
+ private:
+ base::OnceClosure on_exit_;
+};
+
+// Tests that `RequestPermission()` cleanly aborts without crashing if the
+// associated `WebContents` is destroyed while dropping fullscreen.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenExitDestroysWebContents) {
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+
+ DropFullscreenDelegate delegate(
+ base::BindLambdaForTesting([&]() { web_contents_.reset(); }));
+ web_contents_->SetDelegate(&delegate);
+
+ base::test::TestFuture<PermissionRequestOutcome> future;
+ grant->RequestPermission(frame_id(), UserActivationState::kRequired,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), PermissionRequestOutcome::kRequestAborted);
+}
+
+// Tests that `RequestPermission()` does not encounter a UAF if the caller drops
+// its reference to the grant while exiting fullscreen.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenReleasesGrantReference) {
+ FileSystemAccessPermissionRequestManager::FromWebContents(web_contents())
+ ->set_auto_response_for_test(PermissionAction::GRANTED);
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+ auto* raw_grant = grant.get();
+
+ // Dropping the grant here simulates the handle being destroyed (e.g. pipe
+ // disconnection) while exiting fullscreen, releasing the last external
+ // strong reference to the executing `PermissionGrantImpl`.
+ DropFullscreenDelegate delegate(
+ base::BindLambdaForTesting([&]() { grant.reset(); }));
+ web_contents_->SetDelegate(&delegate);
+
+ base::test::TestFuture<PermissionRequestOutcome> future;
+ raw_grant->RequestPermission(frame_id(), UserActivationState::kRequired,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), PermissionRequestOutcome::kUserGranted);
+ web_contents_->SetDelegate(nullptr);
+}
+
+// Tests that `RequestPermission()` safely detects frame navigation occurring
+// during fullscreen drop and aborts with `kInvalidFrame`.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenNavigatesFrame) {
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+
+ const url::Origin other_origin =
+ url::Origin::Create(GURL("https://other.com/"));
+ DropFullscreenDelegate delegate(base::BindLambdaForTesting([&]() {
+ content::WebContentsTester::For(web_contents())
+ ->NavigateAndCommit(other_origin.GetURL());
+ }));
+ web_contents_->SetDelegate(&delegate);
+
+ base::test::TestFuture<PermissionRequestOutcome> future;
+ grant->RequestPermission(frame_id(), UserActivationState::kRequired,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), PermissionRequestOutcome::kInvalidFrame);
+ web_contents_->SetDelegate(nullptr);
+}
+
+// Tests that `RequestPermission()` aborts with `kRequestAborted` if the grant's
+// active permission status transitions away from `ASK` (e.g. granted or denied
+// by another tab or parallel operation) while exiting fullscreen.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ RequestPermission_DropFullscreenPermissionStatusChanges) {
+ content::RenderFrameHostTester::For(web_contents_->GetPrimaryMainFrame())
+ ->SimulateUserActivation();
+
+ auto grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kFile, UserAction::kOpen);
+
+ // Request a directory grant for the same path during the fullscreen drop
+ // window to transition the existing file grant's status to DENIED.
+ DropFullscreenDelegate delegate(base::BindLambdaForTesting([&]() {
+ permission_context()->GetWritePermissionGrant(
+ kTestOrigin, kTestPathInfo, HandleType::kDirectory, UserAction::kOpen);
+ }));
+ web_contents_->SetDelegate(&delegate);
+
+ base::test::TestFuture<PermissionRequestOutcome> future;
+ grant->RequestPermission(frame_id(), UserActivationState::kRequired,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), PermissionRequestOutcome::kRequestAborted);
+ web_contents_->SetDelegate(nullptr);
+}
#endif // !BUILDFLAG(IS_ANDROID)
TEST_F(ChromeFileSystemAccessPermissionContextTest,
diff --git a/content/browser/file_system_access/file_system_access_handle_base_unittest.cc b/content/browser/file_system_access/file_system_access_handle_base_unittest.cc
index 3dd44b4..054c55a6 100644
--- a/content/browser/file_system_access/file_system_access_handle_base_unittest.cc
+++ b/content/browser/file_system_access/file_system_access_handle_base_unittest.cc
@@ -96,7 +96,7 @@
} // namespace
-class TestFileSystemAccessHandle : public FileSystemAccessHandleBase {
+class TestFileSystemAccessHandle final : public FileSystemAccessHandleBase {
public:
using FileSystemAccessHandleBase::GetRenamePermission;
using FileSystemAccessHandleBase::RenamePermission;
@@ -1840,4 +1840,227 @@
kAllowRenameWithoutOverwrite);
}
+
+class FileSystemAccessHandleDoRequestPermissionLifecycleTest
+ : public FileSystemAccessDoRequestPermissionTestBase {
+ protected:
+ bool is_worker() const override { return false; }
+};
+
+// Tests that `DoRequestPermission()` does not encounter a UAF when
+// `kFileSystemAccessWriteMode` is enabled and the handle is destroyed during
+// the first grant's `RequestPermission()` call. Verifies that the second grant
+// request is not invoked and the completion callback is dropped without
+// executing.
+TEST_F(FileSystemAccessHandleDoRequestPermissionLifecycleTest,
+ HandleDestroyedDuringFirstGrantRequest_WriteModeEnabled) {
+ base::test::ScopedFeatureList features;
+ features.InitAndEnableFeature(blink::features::kFileSystemAccessWriteMode);
+
+ auto handle = std::make_unique<TestFileSystemAccessHandle>(
+ manager_.get(),
+ FileSystemAccessManagerImpl::BindingContext(kTestStorageKey, kTestURL,
+ kFrameId),
+ FileSystemURL::CreateForTest(kTestStorageKey,
+ storage::kFileSystemTypeTest,
+ base::FilePath::FromUTF8Unsafe("/test")),
+ handle_state_);
+
+ EXPECT_CALL(*read_grant_, GetStatus())
+ .WillRepeatedly(testing::Return(PermissionStatus::ASK));
+ EXPECT_CALL(*write_grant_, GetStatus())
+ .WillRepeatedly(testing::Return(PermissionStatus::ASK));
+
+ // Simulates the handle being destroyed (e.g. pipe disconnect) during the
+ // first grant request.
+ EXPECT_CALL(*read_grant_,
+ RequestPermission_(kFrameId, UserActivationState::kRequired, _))
+ .WillOnce(
+ [&](GlobalRenderFrameHostId, UserActivationState,
+ base::OnceCallback<void(
+ FileSystemAccessPermissionGrant::PermissionRequestOutcome)>&
+ cb) {
+ handle.reset();
+ std::move(cb).Run(FileSystemAccessPermissionGrant::
+ PermissionRequestOutcome::kUserGranted);
+ });
+
+ // Because the handle is destroyed, the subsequent write grant request should
+ // not be invoked on a dangling handle.
+ EXPECT_CALL(*write_grant_,
+ RequestPermission_(testing::_, testing::_, testing::_))
+ .Times(0);
+
+ base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr,
+ PermissionStatus>
+ future;
+ handle->DoRequestPermission(FileSystemAccessPermissionMode::kReadWrite,
+ future.GetCallback());
+ EXPECT_FALSE(future.IsReady());
+}
+
+// Tests that `DoRequestPermission()` does not encounter a UAF when
+// `kFileSystemAccessWriteMode` is disabled and the handle is destroyed during
+// the first grant's `RequestPermission()` call. Verifies that the second grant
+// request is not invoked and the completion callback is dropped without
+// executing.
+TEST_F(FileSystemAccessHandleDoRequestPermissionLifecycleTest,
+ HandleDestroyedDuringFirstGrantRequest_WriteModeDisabled) {
+ base::test::ScopedFeatureList features;
+ features.InitAndDisableFeature(blink::features::kFileSystemAccessWriteMode);
+
+ auto handle = std::make_unique<TestFileSystemAccessHandle>(
+ manager_.get(),
+ FileSystemAccessManagerImpl::BindingContext(kTestStorageKey, kTestURL,
+ kFrameId),
+ FileSystemURL::CreateForTest(kTestStorageKey,
+ storage::kFileSystemTypeTest,
+ base::FilePath::FromUTF8Unsafe("/test")),
+ handle_state_);
+
+ EXPECT_CALL(*read_grant_, GetStatus())
+ .WillRepeatedly(testing::Return(PermissionStatus::ASK));
+ EXPECT_CALL(*write_grant_, GetStatus())
+ .WillRepeatedly(testing::Return(PermissionStatus::ASK));
+
+ // Simulates the handle being destroyed (e.g. pipe disconnect) during the
+ // first grant request.
+ EXPECT_CALL(*read_grant_,
+ RequestPermission_(kFrameId, UserActivationState::kRequired, _))
+ .WillOnce(
+ [&](GlobalRenderFrameHostId, UserActivationState,
+ base::OnceCallback<void(
+ FileSystemAccessPermissionGrant::PermissionRequestOutcome)>&
+ cb) {
+ handle.reset();
+ std::move(cb).Run(FileSystemAccessPermissionGrant::
+ PermissionRequestOutcome::kUserGranted);
+ });
+
+ // Because the handle is destroyed, the subsequent write grant request should
+ // not be invoked on a dangling handle.
+ EXPECT_CALL(*write_grant_,
+ RequestPermission_(testing::_, testing::_, testing::_))
+ .Times(0);
+
+ base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr,
+ PermissionStatus>
+ future;
+ handle->DoRequestPermission(FileSystemAccessPermissionMode::kReadWrite,
+ future.GetCallback());
+ EXPECT_FALSE(future.IsReady());
+}
+
+// Tests that `DoRequestPermission()` does not encounter a UAF when
+// `kFileSystemAccessWriteMode` is enabled and the handle is destroyed during
+// the second grant's `RequestPermission()` call. Verifies that the completion
+// callback is dropped without executing.
+TEST_F(FileSystemAccessHandleDoRequestPermissionLifecycleTest,
+ HandleDestroyedDuringSecondGrantRequest_WriteModeEnabled) {
+ base::test::ScopedFeatureList features;
+ features.InitAndEnableFeature(blink::features::kFileSystemAccessWriteMode);
+
+ auto handle = std::make_unique<TestFileSystemAccessHandle>(
+ manager_.get(),
+ FileSystemAccessManagerImpl::BindingContext(kTestStorageKey, kTestURL,
+ kFrameId),
+ FileSystemURL::CreateForTest(kTestStorageKey,
+ storage::kFileSystemTypeTest,
+ base::FilePath::FromUTF8Unsafe("/test")),
+ handle_state_);
+
+ EXPECT_CALL(*read_grant_, GetStatus())
+ .WillRepeatedly(testing::Return(PermissionStatus::ASK));
+ EXPECT_CALL(*write_grant_, GetStatus())
+ .WillRepeatedly(testing::Return(PermissionStatus::ASK));
+
+ EXPECT_CALL(*read_grant_,
+ RequestPermission_(kFrameId, UserActivationState::kRequired, _))
... (truncated)
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.
References
On This Page