Skip to content

fix(connect): clean up leaked onConnect/WebUSB/BroadcastChannel listeners#28242

Draft
mroz22 wants to merge 3 commits into
developfrom
mroz22/isolated-listener-leaks
Draft

fix(connect): clean up leaked onConnect/WebUSB/BroadcastChannel listeners#28242
mroz22 wants to merge 3 commits into
developfrom
mroz22/isolated-listener-leaks

Conversation

@mroz22
Copy link
Copy Markdown
Contributor

@mroz22 mroz22 commented Jun 1, 2026

Summary

Three isolated listener / subscription leak fixes, split out of the broader audit in #27978 so they can be reviewed independently. Each touches a single file in a distinct package and adds the missing cleanup.

Fixes

  • @trezor/connect-commonServiceWorkerWindowChannel
    connect() added a chrome.runtime.onConnect listener via an inline closure that was never removed. onConnect is a global Chrome event (unlike port.onMessage, which is port-scoped), so the leaked closure survived the whole extension lifetime and re-connect() stacked more. Now the listener is stored on the instance, registration is guarded against duplicates, and disconnect() removes it.

  • @trezor/react-native-usbWebUSB
    set onconnect / set ondisconnect called onDeviceConnected/Disconnect() without storing the returned EventSubscription, so reassigning a handler leaked the native Android/iOS listener and assigning null registered a broken callback instead of removing. Setters now .remove() the previous subscription and accept null to clear.

  • @trezor/connect-explorertrezorConnectActions.init()
    Deeplink mode created a new BroadcastChannel('trezor_connect_callback') on every init() without closing the previous one. BroadcastChannel stays registered internally until close(), so repeated init stacked channels that all replied to every callback URL. Now the previous channel is closed before a new one is created.

Why split out

Three single-file, single-concern fixes in packages no other PR in the leak-audit stream touches. Each is trivially reviewable and independently revertible.

Related PRs (listener-leak audit stream)

Test plan

  • lint:js clean on all three changed files
  • CI type-check + unit (full monorepo build)

🤖 Generated with Claude Code

🌐 Preview deployments

🌐 Suite Web preview: https://dev.suite.sldev.cz/suite-web/mroz22/isolated-listener-leaks/web/

🔍 Currents Test Results

🔍 Suite web test results: View in Currents

🔒 Quarantined E2E Tests

Trezor Suite (web) — 10 test(s)
Test Type
Account metadata > dropbox provider 🤖 auto
Export transactions > Go to account and try to export all possible variants (pdf, csv, json) 🤖 auto
Passphrase with cardano > verify cardano address behind passphrase 🤖 auto
Cardano > Basic cardano walkthrough 🤖 auto
Public Keys > Check ada XPUB 🤖 auto
Account types suite > Add-account-types-non-BTC-coins 🤖 auto
Quarantine test: "Onboarding - create wallet,Success (basic)" 🙋 manual
Quarantine test: "Database migration,Db migration between: release/22.5/web => develop/web" 🙋 manual
Quarantine test: "Multiple sessions,Session overtaken by another" 🙋 manual
Quarantine test: "Recovery T2T1 - dry run,Recovery after partial recovery" 🙋 manual

Updated: 2026-06-01T15:08:09.674Z • 10 test(s) total

mroz22 added 3 commits June 1, 2026 16:48
`ServiceWorkerWindowChannel.connect()` never removed the
`chrome.runtime.onConnect` listener in `disconnect()` and had no
guard against duplicate registration. Audit 18 additional packages
(transport, connect-web, connect-common, suite-desktop-native,
websocket-client, blockchain-link, request-manager, connect-mobile)
with 18 new false positives documented in reports.
`WebUSB` setters in `react-native-usb/src/index.ts` now store the
`EventSubscription` returned by `ReactNativeUsbModule.addListener()`
and call `.remove()` on overwrite or null assignment. Audit 8
additional packages (suite-desktop-api, trezor-user-env-link,
transport-native-usb, connect-cli, react-native-usb/ios, components)
with 9 new false positives documented in reports.
In `connect-explorer/trezorConnectActions.ts` each repeated `init()`
call in deeplink mode created a new BroadcastChannel without closing
the previous one. Audit connect-explorer and connect-examples as
false positives.
@mroz22 mroz22 added code Code improvements no-project This label is used to specify that PR doesn't need to be added to a project labels Jun 1, 2026
@mroz22 mroz22 requested a review from Copilot June 2, 2026 01:28
@mroz22
Copy link
Copy Markdown
Contributor Author

mroz22 commented Jun 2, 2026

CI Maintenance — 2026-06-02

All CI checks green. No reviews detected.

Actions taken this session:

  • Copilot review requested

Generated by Claude Code

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Three small, independently revertible listener/subscription cleanup fixes split out of the broader leak audit in #27978. Each adds the missing teardown for a long-lived listener so repeated init()/connect() no longer stack handlers.

Changes:

  • ServiceWorkerWindowChannel.connect() stores the chrome.runtime.onConnect callback, guards against duplicate registration, and removes it in disconnect().
  • WebUSB.onconnect/ondisconnect setters now store the returned EventSubscription, .remove() the previous one on overwrite, and accept null to clear.
  • trezorConnectActions.init() closes the previous deeplink BroadcastChannel before creating a new one.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/connect-common/src/messageChannel/serviceworker-window.ts Track onConnect listener as an instance field; guard duplicate registration; remove in disconnect().
packages/react-native-usb/src/index.ts Store and dispose EventSubscription returned by onDeviceConnected/onDeviceDisconnect; accept null to clear.
packages/connect-explorer/src/actions/trezorConnectActions.ts Close the previous module-scoped BroadcastChannel before creating a new one on re-init.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code Code improvements no-project This label is used to specify that PR doesn't need to be added to a project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants