Chrome · FileSystem
CVE-2026-79088
Logic Error in FileSystem
Overview
Medium
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 |
Files Changed
chrome/browser/file_system_access/chrome_file_system_access_permission_context.ccchrome/browser/file_system_access/chrome_file_system_access_permission_context_unittest.cc
Patch
From ab88b786ad83b611543df884ef7344d7ff92a906 Mon Sep 17 00:00:00 2001
From: Ming-Ying Chung <mych@chromium.org>
Date: Tue, 11 Aug 2026 18:30:26 -0700
Subject: [PATCH] [FSA] Maintain downgraded read permissions across file moves
A state tracking flaw in NotifyEntryMoved() leaves stale entries in
downgraded_read_paths when file entries are moved. When handle.remove()
downgrades a read grant to DENIED, the path is stored in
downgraded_read_paths to track potential origin file recreations.
However, moving the handle away and back leaves the original path in
downgraded_read_paths. Consequently, MaybeRestoreReadPermission()
inappropriately restores read access to GRANTED on a file the origin did
not author.
For example, consider an origin manipulating a target file handle:
(1) Initial state: Read status is GRANTED and write status is GRANTED
for "secret.txt".
(2) handle.remove(): Read status drops to DENIED, write status remains
GRANTED, and "secret.txt" is added to downgraded_read_paths.
(3) Victim recreates "secret.txt": Read status for origin remains
DENIED.
(4) handle.move('temp.txt'): Active grants migrate to "temp.txt", but
"secret.txt" remains in downgraded_read_paths.
(5) handle.move('secret.txt'): Grants migrate back to secret.txt, and
MaybeRestoreReadPermission() finds "secret.txt" in
downgraded_read_paths, incorrectly restoring read status to GRANTED.
Also drop the obsolete DCHECK in PermissionGrantImpl:UpdateGrantPath():
removing it is safe because DENIED read grants now represent a valid
runtime state. PermissionGrantImpl::UpdateGrantPath() asserted that
active grants being moved held GRANTED status, which caused Debug builds
to crash when moving handles with read grants downgraded to DENIED by
handle.remove(). Prior to kFileSystemAccessRevokeReadOnRemove, active
grants being moved were assumed to be GRANTED. Under
kFileSystemAccessRevokeReadOnRemove, revoked grants are retained in
read_grants with status DENIED to track state across operations, so
removing the DCHECK allows PermissionGrantImpl::UpdateGrantPath() to
safely update grant paths without altering permission checks.
In summary, this CL updates NotifyEntryMoved() to carry
downgraded_read_paths entries alongside active grants, drops the
obsolete assertion in PermissionGrantImpl::UpdateGrantPath(), and adds
dedicated unit tests to verify state migration and assertion safety in
Debug builds.
TAG=agy
CONV=214d42c9-1a4f-4404-85d5-800d9b0885eb
Bug: 496401361
Change-Id: I7844d3e7cfe5ec9363bd16e028665a94fc79e620
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8224042
Reviewed-by: Mingyu Lei <leimy@chromium.org>
Commit-Queue: Ming-Ying Chung <mych@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1677704}
---
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 e0d5734..f2400b7 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
@@ -1223,9 +1223,6 @@
return;
}
- DCHECK_EQ(old_path_it->second->GetActivePermissionStatus(),
- PermissionStatus::GRANTED);
-
auto* const grant_to_move = old_path_it->second.get();
if (allow_overwrite) {
@@ -2665,6 +2662,7 @@
features::kFileSystemAccessMoveWithOverwrite);
bool updated = false;
+ bool old_path_was_downgraded = false;
auto it = active_permissions_map_.find(origin);
if (it != active_permissions_map_.end()) {
// TODO(crbug.com/40245144): Consolidate superfluous child grants.
@@ -2672,6 +2670,14 @@
new_path, allow_overwrite);
PermissionGrantImpl::UpdateGrantPath(it->second.read_grants, old_path,
new_path, allow_overwrite);
+ if (base::FeatureList::IsEnabled(
+ blink::features::kFileSystemAccessRevokeReadOnRemove) &&
+ it->second.downgraded_read_paths.erase(old_path.path)) {
+ // The downgraded read grant moved along with the entry; carry the
+ // downgraded state to `new_path` so a later write there can restore it.
+ it->second.downgraded_read_paths.insert(new_path.path);
+ old_path_was_downgraded = true;
+ }
updated = true;
}
if (base::FeatureList::IsEnabled(
@@ -2702,7 +2708,10 @@
}
if (base::FeatureList::IsEnabled(
- blink::features::kFileSystemAccessRevokeReadOnRemove)) {
+ blink::features::kFileSystemAccessRevokeReadOnRemove) &&
+ !old_path_was_downgraded) {
+ // Only restore if the moved entry was readable at `old_path`; otherwise
+ // the origin has not authored the content now at `new_path`.
MaybeRestoreReadPermission(origin, new_path.path);
}
}
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 337d442..67f0e25 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
@@ -3795,6 +3795,103 @@
PermissionStatus::ASK);
}
+// Verifies that moving an entry with a downgraded read grant transfers the
+// downgraded state to the destination path, and moving the entry back to its
+// original path does not restore the read grant.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ NotifyEntryMoved_DowngradedReadGrantFollowsMove) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+ const auto temp_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("temp_file.txt"));
+
+ // Initialize read and write permissions for the target file path.
+ auto read_grant = permission_context()->GetReadPermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ auto write_grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+
+ // Calling `NotifyEntryRemoved()` downgrades the read grant and records the
+ // path in `downgraded_read_paths`.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+
+ // Calling `NotifyEntryMoved()` transfers the downgraded read state to the
+ // destination path.
+ permission_context()->NotifyEntryMoved(kTestOrigin, file_path_info,
+ temp_path_info);
+ EXPECT_EQ(read_grant->GetPath(), temp_path_info.path);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, temp_path_info.path));
+
+ // Moving the entry back to its original path retains the `DENIED` read grant
+ // because the origin has not authored new content at the destination.
+ permission_context()->NotifyEntryMoved(kTestOrigin, temp_path_info,
+ file_path_info);
+ EXPECT_EQ(read_grant->GetPath(), file_path_info.path);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, temp_path_info.path));
+
+ // Calling `NotifyEntryModified()` at the original path restores the read
+ // grant.
+ permission_context()->NotifyEntryModified(kTestOrigin, file_path_info);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+}
+
+// Verifies that moving a handle with a downgraded read grant successfully
+// updates the grant path without triggering assertion failures.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ NotifyEntryMoved_DowngradedReadGrantMigratesWithoutAssertionFailure) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+ const auto temp_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("temp_file.txt"));
+
+ // Initialize read and write grants.
+ auto read_grant = permission_context()->GetReadPermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ auto write_grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+
+ // Downgrade the read grant to `DENIED` via `NotifyEntryRemoved()`.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
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 337d442..67f0e25 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
@@ -3795,6 +3795,103 @@
PermissionStatus::ASK);
}
+// Verifies that moving an entry with a downgraded read grant transfers the
+// downgraded state to the destination path, and moving the entry back to its
+// original path does not restore the read grant.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ NotifyEntryMoved_DowngradedReadGrantFollowsMove) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+ const auto temp_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("temp_file.txt"));
+
+ // Initialize read and write permissions for the target file path.
+ auto read_grant = permission_context()->GetReadPermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ auto write_grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+
+ // Calling `NotifyEntryRemoved()` downgrades the read grant and records the
+ // path in `downgraded_read_paths`.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+
+ // Calling `NotifyEntryMoved()` transfers the downgraded read state to the
+ // destination path.
+ permission_context()->NotifyEntryMoved(kTestOrigin, file_path_info,
+ temp_path_info);
+ EXPECT_EQ(read_grant->GetPath(), temp_path_info.path);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, temp_path_info.path));
+
+ // Moving the entry back to its original path retains the `DENIED` read grant
+ // because the origin has not authored new content at the destination.
+ permission_context()->NotifyEntryMoved(kTestOrigin, temp_path_info,
+ file_path_info);
+ EXPECT_EQ(read_grant->GetPath(), file_path_info.path);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_TRUE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, temp_path_info.path));
+
+ // Calling `NotifyEntryModified()` at the original path restores the read
+ // grant.
+ permission_context()->NotifyEntryModified(kTestOrigin, file_path_info);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_FALSE(permission_context()->IsPathInDowngradedReadPathsForTesting(
+ kTestOrigin, file_path_info.path));
+}
+
+// Verifies that moving a handle with a downgraded read grant successfully
+// updates the grant path without triggering assertion failures.
+TEST_F(ChromeFileSystemAccessPermissionContextTest,
+ NotifyEntryMoved_DowngradedReadGrantMigratesWithoutAssertionFailure) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kFileSystemAccessRevokeReadOnRemove);
+ permission_context()->SetOriginHasExtendedPermissionForTesting(kTestOrigin);
+
+ const auto file_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("test_file.txt"));
+ const auto temp_path_info =
+ PathInfo(kTestPathInfo.path.AppendASCII("temp_file.txt"));
+
+ // Initialize read and write grants.
+ auto read_grant = permission_context()->GetReadPermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ auto write_grant = permission_context()->GetWritePermissionGrant(
+ kTestOrigin, file_path_info, HandleType::kFile, UserAction::kSave);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::GRANTED);
+ EXPECT_EQ(write_grant->GetStatus(), PermissionStatus::GRANTED);
+
+ // Downgrade the read grant to `DENIED` via `NotifyEntryRemoved()`.
+ permission_context()->NotifyEntryRemoved(kTestOrigin, file_path_info);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+
+ // Moving the handle invokes `PermissionGrantImpl::UpdateGrantPath()`.
+ // This operation must successfully update the grant path for a `DENIED`
+ // grant without triggering assertion failures in debug builds.
+ permission_context()->NotifyEntryMoved(kTestOrigin, file_path_info,
+ temp_path_info);
+ EXPECT_EQ(read_grant->GetPath(), temp_path_info.path);
+ EXPECT_EQ(read_grant->GetStatus(), PermissionStatus::DENIED);
+}
+
// Tests that calling NotifyEntryRemoved with a directory path also revokes
// read permission grants for descendants whose stored path differs only in
// case from the removed directory. Native file pickers on case-insensitive
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