You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SSH local forward survives desktop update and relaunch #14268
After T3 Code Nightly installed an update and relaunched on macOS, an SSH forward from the old desktop process remained open. The new desktop process opened a second forward to the same MacBook Pro on my local network.
What I observed
September 28, 2026, 22:05 UTC: T3 opened a forward to my MacBook Pro on local port 62377 (PID 60751).
September 29, 2026, 14:28:12 UTC: the desktop trace recorded desktop.updates.install and desktop.updates.installDownloadedUpdate.
14:28:18 UTC: a new desktop instance started.
14:28:37 UTC: it opened a forward to the same MacBook Pro on local port 54619 (PID 60341).
After the relaunch, PID 60751 was still listening on port 62377 and had parent PID 1. PID 60341 belonged to the current desktop process.
I found and stopped 12 orphaned T3 SSH forwards in total. The retained trace lets me tie one of them, PID 60751, to this update. Older trace files have rotated away, so I cannot say what left the other 11 running.
Expected behavior
The old desktop instance closes its SSH forwards before the update relaunches T3 Code.
Investigation notes
The retained trace has no successful tunnel-close event for PID 60751 around the update. Events may be lost as the app exits, but the process was still running after relaunch.
The update path stops backend instances before calling quitAndInstall. The updater quit handler permits that quit without waiting for the normal desktop shutdown path. SSH forward cleanup appears to depend on the SSH environment manager closing during shutdown. This is a possible cause, not a confirmed one.
Environment
macOS
T3 Code Nightly after update: 0.0.43-nightly.20260929.2428
Desktop-managed SSH connections
Related report
#4144 reports stale local forwards during SSH reconnects. This report concerns an update and relaunch.
Confirmed. This is a real leak on the desktop update quit path, and it is still present on main. It is not a duplicate of #4144.
installDownloadedUpdate stops backend pool instances, then calls quitAndInstall. That stop exists because app.quit() exits before the runtime scope can run finalizers (apps/desktop/src/updates/DesktopUpdates.ts). The updater listener then sets updaterQuitAllowed and returns immediately, on purpose, so the macOS native relaunch is not cancelled (apps/desktop/src/app/DesktopLifecycle.ts). The following before-quit sees that flag and returns without DesktopShutdown.request.
The desktop runtime stays blocked on shutdown.awaitRequest (apps/desktop/src/app/DesktopApp.ts), so the layer scope never closes. Local forwards are ssh -N -L children (packages/ssh/src/tunnel.ts). They are only signaled from the SshEnvironmentManager scope finalizer (SIGTERM, then force-kill). A parent exit does not deliver that signal, and -n means the forward does not exit when the parent’s stdin pipe closes. The child is reparented to PID 1 and keeps listening. That matches PID 60751 still bound to port 62377 after the 14:28 UTC relaunch, and the new instance opening a second forward on port 54619.
#4144 is a different failure: reconnects inside a live desktop process can replace a slow tunnel before the previous child is reaped. #10105 serialized connect and disconnect per target; it does not run on this quit path. The other 11 orphans are not identified by the retained trace, so they may be earlier quits or that reconnect leak. This report stands on the one forward tied to the update.
A fix has to reap the local ssh children before quitAndInstall, the same way backends are stopped. Routing the updater through the normal async shutdown is not enough: that handler is required to return without delaying the relaunch. The existing tunnel finalizer also stops a managed remote server after killing the local process; this bug only requires the local listener to die so the relaunched app does not stack another forward onto the same host.
Activity
juliusmarminge commentedon Sep 29, 2026
Triage
Confirmed. This is a real leak on the desktop update quit path, and it is still present on
main. It is not a duplicate of #4144.installDownloadedUpdatestops backend pool instances, then callsquitAndInstall. That stop exists becauseapp.quit()exits before the runtime scope can run finalizers (apps/desktop/src/updates/DesktopUpdates.ts). The updater listener then setsupdaterQuitAllowedand returns immediately, on purpose, so the macOS native relaunch is not cancelled (apps/desktop/src/app/DesktopLifecycle.ts). The followingbefore-quitsees that flag and returns withoutDesktopShutdown.request.The desktop runtime stays blocked on
shutdown.awaitRequest(apps/desktop/src/app/DesktopApp.ts), so the layer scope never closes. Local forwards aressh -N -Lchildren (packages/ssh/src/tunnel.ts). They are only signaled from theSshEnvironmentManagerscope finalizer (SIGTERM, then force-kill). A parent exit does not deliver that signal, and-nmeans the forward does not exit when the parent’s stdin pipe closes. The child is reparented to PID 1 and keeps listening. That matches PID 60751 still bound to port 62377 after the 14:28 UTC relaunch, and the new instance opening a second forward on port 54619.#4144 is a different failure: reconnects inside a live desktop process can replace a slow tunnel before the previous child is reaped. #10105 serialized connect and disconnect per target; it does not run on this quit path. The other 11 orphans are not identified by the retained trace, so they may be earlier quits or that reconnect leak. This report stands on the one forward tied to the update.
A fix has to reap the local
sshchildren beforequitAndInstall, the same way backends are stopped. Routing the updater through the normal async shutdown is not enough: that handler is required to return without delaying the relaunch. The existing tunnel finalizer also stops a managed remote server after killing the local process; this bug only requires the local listener to die so the relaunched app does not stack another forward onto the same host.