Medium watch CVSS 6.5 chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncomplete cleanup in SmartCard
DescriptionIncomplete cleanup in SmartCard
ComponentSmartCard
Sub-componentSmartCard
Bug ClassLogic Error
Action tierwatch — dos-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517442714
Fix commit5813d728ad7e (chromium/src) +37/-12
CWECWE-459
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-22
Patch gap27 days — fix public in source before the advisory

Background

`SmartCardProviderPrivateAPI`
The browser-side handler that bridges the chrome.smartCardProviderPrivate extension API to PC/SC smart-card operations, owning the Mojo receivers and remotes for contexts, connections, and watchers.
`mojo::ReceiverSet::Remove()`
A Mojo helper that removes a receiver by id without invoking its disconnect handler, so any teardown normally driven by disconnection is skipped.
`SmartCardConnectionWatcher`
A Mojo remote associated with each connection that signals in-use (SMART_CARD_GUARD) state; it is tracked in the bidirectional maps connection_watchers_per_receiver_ and connection_receivers_per_watcher_.
Context/connection lifecycle
A SmartCardContext receiver owns a set of SmartCardConnection receivers whose PC/SC handles become invalid when the context is released, requiring symmetric teardown of every connection and its watcher.

Root Cause Analysis

When a SmartCardContext receiver disconnected, OnMojoContextDisconnected() iterated context_data.connection_receiver_ids and called connection_receivers_.Remove() directly on each one. Because mojo::ReceiverSet::Remove() does not run the connection’s disconnect handler, the associated SmartCardConnectionWatcher remote in connection_watchers_ and the entries in connection_watchers_per_receiver_ / connection_receivers_per_watcher_ were never released, violating the invariant that a removed connection always tears down its watcher and keeps the two maps symmetric. The result was a leaked watcher pipe and a SMART_CARD_GUARD in-use state that stayed stuck, since the watcher was never disconnected.

The fix introduces RemoveConnection(), which erases both map entries, removes the watcher remote via connection_watchers_.Remove(), and then removes the connection receiver, and routes OnMojoContextDisconnected(), OnMojoConnectionDisconnected(), and OnMojoWatcherPipeClosed() through it so every disconnect path performs complete, symmetric cleanup.

Key insight
The single mistake was using mojo::ReceiverSet::Remove() to drop connection receivers, which bypasses the disconnect handler and therefore skips watcher/map teardown; the fix centralizes that teardown in RemoveConnection() and calls it from every disconnect path so watcher removal and map symmetry are guaranteed regardless of how the connection ends.

Attack Path

  1. Establish context and connection A smart-card provider extension (or renderer using the API) establishes a SmartCardContext and opens a SmartCardConnection, causing a watcher remote and its entries in the bidirectional maps to be created.
  2. Drop the context remote The context receiver is disconnected (e.g. context.reset() as in the added DisconnectContextDisconnectsWatcher test), driving OnMojoContextDisconnected() to remove each connection via the bare connection_receivers_.Remove().
  3. Skip watcher teardown Because Remove() runs no disconnect handler, the SmartCardConnectionWatcher remote and its map entries are never released, leaking the watcher pipe and leaving SMART_CARD_GUARD in-use state stuck.
  4. Accumulate stuck state Repeating the disconnect leaks additional watcher pipes and desynchronizes connection_watchers_per_receiver_ and connection_receivers_per_watcher_, degrading smart-card availability and consistency (inferred).

Impact Assessment

An attacker able to drive the smart-card provider API can force incomplete cleanup in the browser process, leaking watcher Mojo pipes and leaving SMART_CARD_GUARD in-use state stuck so smart-card resources appear perpetually busy. The precondition is the ability to establish and then disconnect a SmartCardContext with an open connection on a ChromeOS build exposing chrome.smartCardProviderPrivate. The advisory classifies this as a LogicError (incomplete cleanup) and states no in-the-wild exploitation; any escalation beyond resource leakage and stuck state is (inferred).

Changed Functions

FunctionChangeNotes
for
chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.cc
modified

Files Changed

  • chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.cc
  • chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.h
  • chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc

Audit Directions

  • Pattern
    Audit every use of mojo::ReceiverSet::Remove() (and RemoteSet::Remove()) that unwinds objects normally torn down by a disconnect handler, since Remove() skips that handler and can strand associated remotes or map entries.
  • Pattern
    Look for bidirectional lookup maps (like connection_watchers_per_receiver_ / connection_receivers_per_watcher_) where entries are removed on only one code path, checking that all disconnect routes keep both sides symmetric.
  • Pattern
    Review lifecycle handlers that iterate owned child receivers on parent teardown to confirm each child’s full cleanup runs, not just its removal from the set.
From 5813d728ad7e489f8ffe82d034a2ff81367d12ee Mon Sep 17 00:00:00 2001
From: Zgroza Klimek <[email protected]>
Date: Wed, 26 Aug 2026 08:45:43 -0700
Subject: [PATCH] SmartCard: drop watcher remote on context loss

When a SmartCardContext receiver is disconnected,
SmartCardProviderPrivateAPI removes each SmartCardConnection receiver
via ReceiverSet::Remove(). Because ReceiverSet::Remove() does not run
the disconnect handler, the associated SmartCardConnectionWatcher remote
and its entries in the bidirectional maps were never released, leaking
the watcher pipe and leaving SMART_CARD_GUARD in-use state stuck.

Add RemoveConnection() to encapsulate both the connection receiver
removal and watcher teardown. Call it from OnMojoContextDisconnected(),
OnMojoConnectionDisconnected(), and OnMojoWatcherPipeClosed(),
guaranteeing map symmetry and pipe closure on every disconnect path.

Add a browser test to assert that dropping the context remote reliably
triggers watcher disconnection.

Bug: 517442714
Change-Id: Idd251c639662822d0d96cdff79062e8c6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8296440
Reviewed-by: Andrew Rayskiy <[email protected]>
Owners-Override: Andrew Rayskiy <[email protected]>
Commit-Queue: Zgroza (Luke) Klimek <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1686417}
---
diff --git a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.cc b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.cc
index 1972294..808ebbe 100644
--- a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.cc
+++ b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.cc
@@ -428,9 +428,10 @@
   // Disconnect all mojom::SmartCardConnection receivers created on this context
   // as their handles will all become invalid at PC/SC level once the context
   // is released.
-  for (mojo::ReceiverId connection_receiver_id :
-       context_data.connection_receiver_ids) {
-    connection_receivers_.Remove(connection_receiver_id);
+  auto connection_receiver_ids =
+      std::move(context_data.connection_receiver_ids);
+  for (mojo::ReceiverId connection_receiver_id : connection_receiver_ids) {
+    RemoveConnection(connection_receiver_id);
   }
 
   RunOrQueueRequest(
@@ -446,13 +447,7 @@
         FROM_HERE, disconnect_observer_);
   }
 
-  // Break the watcher pipe.
-  auto it = connection_watchers_per_receiver_.find(
-      connection_receivers_.current_receiver());
-  if (it != connection_watchers_per_receiver_.end()) {
-    connection_watchers_.Remove(it->second);
-    connection_watchers_per_receiver_.erase(it);
-  }
+  RemoveConnection(connection_receivers_.current_receiver());
 
   auto callback =
       base::BindOnce(&SmartCardProviderPrivateAPI::OnScardHandleDisconnected,
@@ -1653,8 +1648,20 @@
   if (it == connection_receivers_per_watcher_.end()) {
     return;
   }
-  connection_receivers_.Remove(it->second);
-  connection_receivers_per_watcher_.erase(it);
+  mojo::ReceiverId connection_receiver_id = it->second;
+  RemoveConnection(connection_receiver_id);
+}
+
+void SmartCardProviderPrivateAPI::RemoveConnection(
+    mojo::ReceiverId connection_receiver_id) {
+  auto it = connection_watchers_per_receiver_.find(connection_receiver_id);
+  if (it != connection_watchers_per_receiver_.end()) {
+    mojo::RemoteSetElementId watcher_id = it->second;
+    connection_watchers_per_receiver_.erase(it);
+    connection_receivers_per_watcher_.erase(watcher_id);
+    connection_watchers_.Remove(watcher_id);
+  }
+  connection_receivers_.Remove(connection_receiver_id);
 }
 
 void SmartCardProviderPrivateAPI::NotifyConnectionUsed() {
diff --git a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.h b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.h
index 9a8fdbef..2c8c1a8 100644
--- a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.h
+++ b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.h
@@ -333,6 +333,10 @@
   // the connection.
   void OnMojoWatcherPipeClosed(mojo::RemoteSetElementId watcher_id);
 
+  // Removes a connection receiver and tears down its associated connection
+  // watcher and tracking maps. Safe to call even if already disconnected.
+  void RemoveConnection(mojo::ReceiverId connection_receiver_id);
+
   // This may only be called only when processing a received method call on
   // SmartCardConnection().
   void NotifyConnectionUsed();
diff --git a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
index c2197591..2a318db 100644
--- a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
+++ b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
@@ -1123,6 +1123,20 @@
 }
 
 IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
+                       DisconnectContextDisconnectsWatcher) {
+  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs});
+  auto [context, connection] = CreateContextAndConnection();
+  ASSERT_TRUE(context.is_bound());
+  ASSERT_TRUE(connection.is_bound());
+
+  auto disconnect_count = connections_watcher_.GetTimesDisconnected();
+  connections_watcher_.ClearDisconnectFuture();
+  context.reset();
+  connections_watcher_.WaitForDisconnect();
+  EXPECT_EQ(disconnect_count + 1, connections_watcher_.GetTimesDisconnected());
+}
+
+IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                        WatcherDisconnectionSeversConnection) {
   LoadFakeProviderExtension({kEstablishContextJs, kConnectJs});
   auto [context, connection] = CreateContextAndConnection();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
index c2197591..2a318db 100644
--- a/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
+++ b/chrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
@@ -1123,6 +1123,20 @@
 }
 
 IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
+                       DisconnectContextDisconnectsWatcher) {
+  LoadFakeProviderExtension({kEstablishContextJs, kConnectJs});
+  auto [context, connection] = CreateContextAndConnection();
+  ASSERT_TRUE(context.is_bound());
+  ASSERT_TRUE(connection.is_bound());
+
+  auto disconnect_count = connections_watcher_.GetTimesDisconnected();
+  connections_watcher_.ClearDisconnectFuture();
+  context.reset();
+  connections_watcher_.WaitForDisconnect();
+  EXPECT_EQ(disconnect_count + 1, connections_watcher_.GetTimesDisconnected());
+}
+
+IN_PROC_BROWSER_TEST_F(SmartCardProviderPrivateApiTest,
                        WatcherDisconnectionSeversConnection) {
   LoadFakeProviderExtension({kEstablishContextJs, kConnectJs});
   auto [context, connection] = CreateContextAndConnection();
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.