CVE-2026-95303
Overview
Background
- `SmartCardProviderPrivateAPI`
- The browser-side handler that bridges the
chrome.smartCardProviderPrivateextension 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 mapsconnection_watchers_per_receiver_andconnection_receivers_per_watcher_. - Context/connection lifecycle
- A
SmartCardContextreceiver owns a set ofSmartCardConnectionreceivers 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.
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
- Establish context and connection
A smart-card provider extension (or renderer using the API) establishes a
SmartCardContextand opens aSmartCardConnection, causing a watcher remote and its entries in the bidirectional maps to be created. - Drop the context remote
The context receiver is disconnected (e.g.
context.reset()as in the addedDisconnectContextDisconnectsWatchertest), drivingOnMojoContextDisconnected()to remove each connection via the bareconnection_receivers_.Remove(). - Skip watcher teardown
Because
Remove()runs no disconnect handler, theSmartCardConnectionWatcherremote and its map entries are never released, leaking the watcher pipe and leavingSMART_CARD_GUARDin-use state stuck. - Accumulate stuck state
Repeating the disconnect leaks additional watcher pipes and desynchronizes
connection_watchers_per_receiver_andconnection_receivers_per_watcher_, degrading smart-card availability and consistency(inferred).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
forchrome/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.ccchrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_api.hchrome/browser/chromeos/extensions/smart_card_provider_private/smart_card_provider_private_apitest.cc
Audit Directions
- PatternAudit every use of
mojo::ReceiverSet::Remove()(andRemoteSet::Remove()) that unwinds objects normally torn down by a disconnect handler, sinceRemove()skips that handler and can strand associated remotes or map entries. - PatternLook 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. - PatternReview 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.
Patch
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();
Regression Test / PoC
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();