Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse of released resource in FontAccess
DescriptionUse of released resource in FontAccess
ComponentFontAccess
Bug ClassLogic Error
Tracker556250086
Fix commit0d5b704308d5 (chromium/src) +87/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/font_access/font_access_manager_unittest.cc
modified

Files Changed

  • third_party/blink/common/features.cc
  • third_party/blink/public/common/features.h
  • content/browser/font_access/font_access_manager.cc
  • content/browser/font_access/font_access_manager_unittest.cc
  • content/browser/font_access/README.md
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.