Chrome · FontAccess
CVE-2026-106310
Logic Error in FontAccess
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/font_access/font_access_manager_unittest.cc |
modified |
Files Changed
third_party/blink/common/features.ccthird_party/blink/public/common/features.hcontent/browser/font_access/font_access_manager.cccontent/browser/font_access/font_access_manager_unittest.cccontent/browser/font_access/README.md
Patch
From 0d5b704308d57e7551b5995a3e0e5b375a1ccf90 Mon Sep 17 00:00:00 2001
From: Koji Ishii <kojii@chromium.org>
Date: Tue, 08 Sep 2026 22:05:10 -0700
Subject: [PATCH] Check `IsActive()` in `FontAccessManager::EnumerateLocalFonts()`
This patch adds `IsActive()` check in `FontAccessManager::
EnumerateLocalFonts()` before it accesses `frame_tree_node()`,
in the same way as sibling services such as `HidService`,
`SerialService`, `WebUsbService`, etc.
It prevents `UpdateUserActivationState()` to accidentally
succeed when the frame is gone.
Fixed: 556250086
Change-Id: I9f28b8beb29a6839236e5dfd4cfb515f295fa7d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8369603
Commit-Queue: Koji Ishii <kojii@chromium.org>
Reviewed-by: Chris Harrelson <chrishtr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694340}
---
diff --git a/content/browser/font_access/README.md b/content/browser/font_access/README.md
index 298990ee..ab9ef85 100644
--- a/content/browser/font_access/README.md
+++ b/content/browser/font_access/README.md
@@ -17,7 +17,8 @@
* `FontAccessManager`: `content::FontAccessManager` implements
`blink::mojom::FontAccessManager`, providing a way to enumerate local fonts.
It checks for the requirements to access local fonts such as user permission,
- page visibility and transient user activation. Once all the requirements are
+ page visibility, transient user activation, and that the requesting frame is
+ active before user activation is consumed. Once all the requirements are
met, it returns the fonts data from `FontEnumerationCache`. In terms of code
ownership, there is one `FontAccessManager` per `StoragePartitionImpl`;
`Frame`s are bound to `FontAccessManager` via a `BindingContext`.
diff --git a/content/browser/font_access/font_access_manager.cc b/content/browser/font_access/font_access_manager.cc
index 59fc6d4..256d222 100644
--- a/content/browser/font_access/font_access_manager.cc
+++ b/content/browser/font_access/font_access_manager.cc
@@ -146,7 +146,14 @@
base::ReadOnlySharedMemoryRegion());
return;
}
- if (!rfh->frame_tree_node()->UpdateUserActivationState(
+
+ // Ensure the requesting document is still active and consume transient user
+ // activation to prevent stale/pending-deletion frames from consuming user
+ // gestures from newly committed documents or other frames in the frame tree.
+ if ((!rfh->IsActive() &&
+ base::FeatureList::IsEnabled(
+ blink::features::kFontAccessCheckFrameIsActive)) ||
+ !rfh->frame_tree_node()->UpdateUserActivationState(
blink::mojom::UserActivationUpdateType::kConsumeTransientActivation,
blink::mojom::UserActivationNotificationType::kNone)) {
std::move(callback).Run(
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index fc3a5ba..990b42fa 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -691,6 +691,7 @@
"filter",
"one_euro_filter");
+BASE_FEATURE(kFontAccessCheckFrameIsActive, base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kBlockPartialResponseWithoutRange,
base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 17a0c7d..dbcc3e68d 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -476,6 +476,10 @@
std::string,
kFilteringScrollPredictionFilterParam);
+// When enabled, FontAccessManager ensures the requesting frame is active
+// before consuming transient user activation.
+// Kill switch for crbug.com/556250086.
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kFontAccessCheckFrameIsActive);
// Block partial responses (206, 416) for requests without a Range header.
BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kBlockPartialResponseWithoutRange);
diff --git a/content/browser/font_access/font_access_manager_unittest.cc b/content/browser/font_access/font_access_manager_unittest.cc
index e994830..041b406 100644
--- a/content/browser/font_access/font_access_manager_unittest.cc
+++ b/content/browser/font_access/font_access_manager_unittest.cc
@@ -314,6 +314,78 @@
EXPECT_FALSE(region.IsValid());
}
+TEST_F(FontAccessManagerTest, EnumerationFailsWhenInactive) {
+ AskGrantPermission();
+ SimulateUserActivation();
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ EXPECT_FALSE(main_rfh()->IsActive());
+
+ const auto [status, region] = manager_sync_->EnumerateLocalFonts();
+ EXPECT_EQ(status, FontEnumerationStatus::kNeedsUserActivation);
+ EXPECT_FALSE(region.IsValid());
+
+ // Transient user activation must NOT have been consumed.
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+}
+
+TEST_F(FontAccessManagerTest,
+ EnumerationDoesNotConsumeTreeActivationWhenInactive) {
+ AskGrantPermission();
+
+ TestRenderFrameHost* child_rfh = main_test_rfh()->AppendChild("child");
+ child_rfh->InitializeRenderFrameIfNeeded();
+
+ mojo::Remote<blink::mojom::FontAccessManager> child_remote;
+ manager_->BindReceiver(child_rfh->GetGlobalId(),
+ child_remote.BindNewPipeAndPassReceiver());
+ FontAccessManagerSync child_sync(child_remote.get());
+
+ // Arm user activation on both frames.
+ child_rfh->SimulateUserActivation();
+ SimulateUserActivation();
+ EXPECT_TRUE(child_rfh->HasTransientUserActivation());
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+
+ // Transition child frame to inactive state.
+ child_rfh->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ EXPECT_FALSE(child_rfh->IsActive());
+ EXPECT_TRUE(main_rfh()->IsActive());
+
+ // Calling enumeration on the inactive child must fail without consuming
+ // activation on the main frame.
+ const auto [status, region] = child_sync.EnumerateLocalFonts();
+ EXPECT_EQ(status, FontEnumerationStatus::kNeedsUserActivation);
+ EXPECT_FALSE(region.IsValid());
+
+ // Main frame's transient activation must remain intact.
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+}
+
+TEST_F(FontAccessManagerTest, EnumerationWhenInactiveWithKillSwitchDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ blink::features::kFontAccessCheckFrameIsActive);
+
+ AskGrantPermission();
+ SimulateUserActivation();
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ EXPECT_FALSE(main_rfh()->IsActive());
+
+ const auto [status, region] = manager_sync_->EnumerateLocalFonts();
+ // With kill switch disabled, activation is consumed by
+ // UpdateUserActivationState.
+ EXPECT_FALSE(main_rfh()->HasTransientUserActivation());
+}
+
} // namespace
} // namespace content
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/font_access/font_access_manager_unittest.cc b/content/browser/font_access/font_access_manager_unittest.cc
index e994830..041b406 100644
--- a/content/browser/font_access/font_access_manager_unittest.cc
+++ b/content/browser/font_access/font_access_manager_unittest.cc
@@ -314,6 +314,78 @@
EXPECT_FALSE(region.IsValid());
}
+TEST_F(FontAccessManagerTest, EnumerationFailsWhenInactive) {
+ AskGrantPermission();
+ SimulateUserActivation();
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ EXPECT_FALSE(main_rfh()->IsActive());
+
+ const auto [status, region] = manager_sync_->EnumerateLocalFonts();
+ EXPECT_EQ(status, FontEnumerationStatus::kNeedsUserActivation);
+ EXPECT_FALSE(region.IsValid());
+
+ // Transient user activation must NOT have been consumed.
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+}
+
+TEST_F(FontAccessManagerTest,
+ EnumerationDoesNotConsumeTreeActivationWhenInactive) {
+ AskGrantPermission();
+
+ TestRenderFrameHost* child_rfh = main_test_rfh()->AppendChild("child");
+ child_rfh->InitializeRenderFrameIfNeeded();
+
+ mojo::Remote<blink::mojom::FontAccessManager> child_remote;
+ manager_->BindReceiver(child_rfh->GetGlobalId(),
+ child_remote.BindNewPipeAndPassReceiver());
+ FontAccessManagerSync child_sync(child_remote.get());
+
+ // Arm user activation on both frames.
+ child_rfh->SimulateUserActivation();
+ SimulateUserActivation();
+ EXPECT_TRUE(child_rfh->HasTransientUserActivation());
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+
+ // Transition child frame to inactive state.
+ child_rfh->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ EXPECT_FALSE(child_rfh->IsActive());
+ EXPECT_TRUE(main_rfh()->IsActive());
+
+ // Calling enumeration on the inactive child must fail without consuming
+ // activation on the main frame.
+ const auto [status, region] = child_sync.EnumerateLocalFonts();
+ EXPECT_EQ(status, FontEnumerationStatus::kNeedsUserActivation);
+ EXPECT_FALSE(region.IsValid());
+
+ // Main frame's transient activation must remain intact.
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+}
+
+TEST_F(FontAccessManagerTest, EnumerationWhenInactiveWithKillSwitchDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ blink::features::kFontAccessCheckFrameIsActive);
+
+ AskGrantPermission();
+ SimulateUserActivation();
+ EXPECT_TRUE(main_rfh()->HasTransientUserActivation());
+
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ EXPECT_FALSE(main_rfh()->IsActive());
+
+ const auto [status, region] = manager_sync_->EnumerateLocalFonts();
+ // With kill switch disabled, activation is consumed by
+ // UpdateUserActivationState.
+ EXPECT_FALSE(main_rfh()->HasTransientUserActivation());
+}
+
} // namespace
} // namespace content
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