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 <[email protected]> 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 <[email protected]> Reviewed-by: Chris Harrelson <[email protected]> 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