fix(macos): orphaned SSH tunnels from crashed app instances keep running and squat the preferred local port#100489
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 7:20 PM ET / 23:20 UTC. Summary PR surface: Other +291. Total +291 across 4 files. Reproducibility: yes. Current main records RemotePortTunnel PIDs but the PortGuardian sweep ignores recorded tunnel ports and skips remote-mode termination; the linked issue and PR body provide a concrete macOS kill/relaunch path. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the guarded recorded-PID reaper after maintainer review accepts the limited process-reaping risk and final CI gates finish; keep broad remote-mode listener killing disabled. Do we have a high-confidence way to reproduce the issue? Yes. Current main records RemotePortTunnel PIDs but the PortGuardian sweep ignores recorded tunnel ports and skips remote-mode termination; the linked issue and PR body provide a concrete macOS kill/relaunch path. Is this the best way to solve the issue? Yes. The PR fixes the right owner boundary by reaping only recorded orphaned SSH tunnels while preserving the existing remote-mode rule that arbitrary gateway-port listeners are not killed. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9d2d51729699. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Other +291. Total +291 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
3c0fcb8 to
f543538
Compare
…ing and squat the preferred local port (openclaw#100489) * fix(macos): reap orphaned SSH tunnels that survive app crashes and squat the preferred local port * chore(i18n): sync native app string inventory for PortGuardian changes
…ing and squat the preferred local port (openclaw#100489) * fix(macos): reap orphaned SSH tunnels that survive app crashes and squat the preferred local port * chore(i18n): sync native app string inventory for PortGuardian changes
Closes #100477
What Problem This Solves
Fixes an issue where the macOS app leaked
ssh -N -Ltunnel processes when it crashed, was force-killed, or a dev build was stopped from Xcode. The orphaned tunnels reparent to launchd and run forever, holding SSH connections to the remote host — and one of them usually squats the preferred local gateway port (18789), so every new tunnel falls back to a random ephemeral port. Real-world evidence on one dev machine: 10 orphans accumulated over 2 days, one holding 18789.Why This Change Was Made
PortGuardianalready records tunnel pids inport-guard.jsonbut never used them:sweep()only inspected the canonical gateway port and refuses to kill anything in remote mode. This change adds orphan reaping driven by those records. A recorded pid is killed only when all three hold: the process still exists, its live command line matches the app's exact tunnel signature (ssh … -N -L <recordedLocalPort>:127.0.0.1:<remotePort>), and its parent is launchd (ppid 1, i.e. the owning app instance died). Tunnels whose parent is still alive — including concurrent OpenClaw instances (prod + dev shareport-guard.json) — are never touched; records whose pid is gone or was reused by an unrelated process are dropped without killing. Reaping runs on everyPortGuardian.sweep(startup, mode changes) and beforeRemotePortTunnel.createpicks a local port, so the preferred port is actually free for the current launch, not just the next one.User Impact
Remote-mode users no longer accumulate zombie SSH tunnels after app crashes or force-kills. The preferred local gateway port is reclaimed automatically on the next launch/sweep, which stops the ephemeral-port drift that made open Dashboard windows go stale on every tunnel restart (#100476).
A recorded pid is killed only when all four gates hold: the process still exists, its live command line matches the exact tunnel signature, its kernel start time predates the record (pid-reuse guard — a look-alike tunnel on the same port that started after the record was written is never touched), and its parent is launchd (ppid 1). After a successful kill the reaper waits for the process to actually exit so the freed port is immediately reusable by the caller.
Evidence
-N, non-ssh binaries, non-loopback forwards), the per-record classification (gone → drop, pid reused → drop without kill, started-after-record → drop, orphaned → reap, parent alive → keep, unreadable command → keep), and the disk/memory record merge that picks up a crashed sibling instance's records.ssh -N -L(viaProxyCommand='sleep 60'— no network traffic, port never bound), verifies it reparents to launchd, and asserts the live sysctl/ps classification path returns reap for the matching record and drop for mismatched-port and predating-record variants.swift test --package-path apps/macos --filter "port guardian": 8 tests passed (0.196s).swift test --package-path apps/macos --parallel: 632 tests, 113 suites — only failure was the known env-race flake inMacNodeRuntimeTests("browser proxy rejects disabled browser control"), which passes in isolation and is untouched by this diff (CI already retries this lane for that reason).ssh … -N -L 18789:127.0.0.1:18789, parent = running dev OpenClaw instance) classifies as keep; the 10-orphan pileup from the issue is exactly the launchd-parented shape the reaper kills.swiftformat --lintclean on changed files; Codex autoreview run to a clean pass (two accepted findings fixed: persist disk-only record removals; start-time pid-reuse gate).