Skip to content

fix(pairing): keep the addCu listener deactivated whenever a trusted addCu device exists#92

Open
kirollosnct wants to merge 3 commits into
devfrom
fix/pairing-startup-listener-guard
Open

fix(pairing): keep the addCu listener deactivated whenever a trusted addCu device exists#92
kirollosnct wants to merge 3 commits into
devfrom
fix/pairing-startup-listener-guard

Conversation

@kirollosnct

Copy link
Copy Markdown
Member

Per pairing spec §4.3, processing of addCu-requests SHALL stay deactivated as long as the pairing with the trusted devZ is intended. Two entry points violated this:

  1. Startup — startPairingService enabled the pairing listener unconditionally in listener mode, even when a trusted addCu device had been restored from persistence. Beyond the direct §4.3 violation, this broke the §4.3 1.b.i trust transfer after a reboot: a new request arriving during the 15-minute replacement window was evaluated and deferred — but only after its digest was recorded and the listener stopped itself, so the post-window re-evaluation rejected the re-announced request as a replay, permanently locking the new devZ out.
  2. RegisterRemoteService after start — registering a trusted addCu device never touched the listener; it was only disarmed indirectly once the SHIP handshake reached ConnectionStateCompleted. For an offline devZ the listener stayed armed indefinitely, and the window is exploitable: a third devZ announcing a valid request lands in OnPairingSuccess, finds the just-registered device as trusted addCu with a differing fingerprint, and — since no replacement window is armed on this path — immediately steals the trust via the 1.b.i transfer logic.

Fix

  • startPairingService guards listener activation on GetTrustedAddCuDevice(): with a trusted addCu device present, the listener stays off and the §4.3 1.a replacement timer owns reactivation. A request announced during the window stays un-consumed in the mDNS cache and receives a clean first evaluation once the timer expires.
  • RegisterRemoteService stops the listener when the registered service is a trusted addCu device and the Hub has started — the mirror of UnregisterRemoteService's addCu path, which reactivates it.

Together: the listener is off whenever a trusted addCu device exists, regardless of how or when the trust was established. Reconnection keeps processing off (§4.3 1.b.ii) and manual removal still reactivates immediately (§4.3 2.b), both unchanged.

…ice is trusted

startPairingService enabled the pairing listener unconditionally in
listener mode. Per pairing spec §4.3, processing addCu-requests SHALL
stay deactivated as long as the pairing with the trusted devZ is
intended — and a restart does not change that intent. With a trusted
addCu device restored from persistence, the listener therefore started
while it had to be off.

Beyond the §4.3 violation this made the 1.b.i trust transfer
unreachable after a reboot: a new request arriving during the first 15
minutes was evaluated by the active listener, deferred by the
replacement-window check — but only after its digest was recorded and
the listener stopped itself. The post-window re-evaluation then
rejected the request as a replay, locking the new devZ out.

Guard the startup activation on GetTrustedAddCuDevice(): with a
trusted addCu device present, the listener stays off and the §4.3 1.a
replacement timer armed by startAddCuReplacementTimersForOfflineDevices
owns the reactivation. A request announced during the window now stays
un-consumed in the mDNS cache and gets a clean first evaluation after
the timer expires. Reconnection of the trusted device keeps processing
off (§4.3 1.b.ii), and manual removal still reactivates immediately
(§4.3 2.b).
…evice is trusted via RegisterRemoteService

RegisterRemoteService marked the service trusted and triggered a
connection attempt, but never touched the pairing listener. The listener
was only disarmed indirectly once the SHIP handshake with the device
reached ConnectionStateCompleted — leaving it armed for the seconds
until an online devZ connects, and indefinitely for an offline one
(e.g. a persisted pairing restored after Hub start).

Per pairing spec §4.3, registering trust in an addCu device expresses
the pairing intent, so processing of addCu-requests must deactivate at
registration. The window is not just non-compliant but exploitable:
with the listener wrongly armed, a third devZ announcing a valid
addCu-request lands in OnPairingSuccess, where the just-registered
device is found as the trusted addCu device with a differing
fingerprint — and since RegisterRemoteService arms no replacement
window, the §4.3 1.b.i transfer logic immediately untrusts the device
that was just registered and trusts the announcer instead.

Stop the pairing listener in RegisterRemoteService when the registered
service is a trusted addCu device and the Hub has started. This mirrors
UnregisterRemoteService's addCu path (which reactivates) and completes
the invariant of the startup guard: the listener is off whenever a
trusted addCu device exists, regardless of how or when the trust was
established. Registration before Start is already covered by the
startup guard in startPairingService.

Stopping the listener alone, however, would strand an offline devZ:
nothing on this path arms the §4.3 1.a replacement timer, so the
listener would stay deactivated indefinitely with manual removal
(UnregisterRemoteService) the only recovery — inconsistent with the
startup path, where startAddCuReplacementTimersForOfflineDevices arms
the timer for exactly this kind of trusted-offline device. So when the
registered addCu device has no live connection, arm the replacement
timer too; a completed connection cancels it via
StopAddCuReplacementTimer. This also gives defense in depth: with the
window armed, OnPairingSuccess defers a competing announcement (via
IsInReplacementWindow) instead of transferring trust immediately.
Hub.Shutdown stopped the announcement lifetime timers but never touched
the AddCu replacement tracker. Whenever a trusted addCu device is offline
at shutdown — the normal state, armed at startup by
startAddCuReplacementTimersForOfflineDevices or by RegisterRemoteService
— the 15-minute replacement timer stayed live across teardown.

Two consequences:

- Post-shutdown resurrection: up to 15 minutes after Shutdown, the
  time.AfterFunc fires handleAddCuReplacementTimeout, which calls
  reactivatePairingListener -> enablePairingListener (re-creating a
  listener on an already-shut-down pairing service) and
  RequestPairingEntries (calling into already-shut-down mDNS).
- Memory retention: the pending timer keeps a reference to the Hub, so
  the whole object graph cannot be collected until the window elapses.

Add AddCuReplacementTracker.StopAll — mirroring
AnnouncementLifetimeTracker.StopAll — which unconditionally stops the
active timer and clears tracking state, and call it from Hub.Shutdown
next to the existing lifetime-tracker teardown.
@kirollosnct
kirollosnct force-pushed the fix/pairing-startup-listener-guard branch from f247969 to f5a50f8 Compare July 14, 2026 09:24
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 93.096% (+0.1%) from 92.972% — fix/pairing-startup-listener-guard into dev

@kirollosnct
kirollosnct marked this pull request as ready for review July 19, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants