Skip to content

fix(macos): orphaned SSH tunnels from crashed app instances keep running and squat the preferred local port#100489

Merged
steipete merged 2 commits into
mainfrom
claude/frosty-ptolemy-fc8ab1
Jul 6, 2026
Merged

fix(macos): orphaned SSH tunnels from crashed app instances keep running and squat the preferred local port#100489
steipete merged 2 commits into
mainfrom
claude/frosty-ptolemy-fc8ab1

Conversation

@steipete

@steipete steipete commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Closes #100477

What Problem This Solves

Fixes an issue where the macOS app leaked ssh -N -L tunnel 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

PortGuardian already records tunnel pids in port-guard.json but 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 share port-guard.json) — are never touched; records whose pid is gone or was reused by an unrelated process are dropped without killing. Reaping runs on every PortGuardian.sweep (startup, mode changes) and before RemotePortTunnel.create picks 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

  • New unit tests cover the tunnel-command signature matcher (rejects wrong port, missing -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.
  • A real-process integration test spawns an actual orphaned ssh -N -L (via ProxyCommand='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).
  • Full local suite swift test --package-path apps/macos --parallel: 632 tests, 113 suites — only failure was the known env-race flake in MacNodeRuntimeTests ("browser proxy rejects disabled browser control"), which passes in isolation and is untouched by this diff (CI already retries this lane for that reason).
  • Live machine check: the one currently running tunnel (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 --lint clean on changed files; Codex autoreview run to a clean pass (two accepted findings fixed: persist disk-only record removals; start-time pid-reuse gate).

@openclaw-barnacle openclaw-barnacle Bot added app: macos App: macos size: M maintainer Maintainer-authored PR labels Jul 5, 2026
@clawsweeper

clawsweeper Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 5, 2026, 7:20 PM ET / 23:20 UTC.

Summary
The PR adds guarded macOS PortGuardian cleanup for recorded orphaned SSH tunnel processes and tests the command matching, record classification, and real-process classification path.

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.

  • Process termination surface: 1 new recorded-pid SSH reaper. Maintainers should notice that this bug fix adds an automatic TERM/KILL path for macOS ssh tunnel children.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #100477
Summary: This PR is the candidate fix for the open macOS orphaned SSH tunnel leak issue, with one related closed dashboard-port issue that shares the tunnel-port instability symptom but has a distinct dashboard refresh root cause.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] The PR adds an automatic TERM/KILL path for recorded ssh tunnel PIDs; the merge-critical assumption is that the command, parent, and start-time gates keep it limited to orphaned OpenClaw tunnels.

Maintainer options:

  1. Accept the guarded reaper (recommended)
    Accept the limited process-reaping risk because the patch gates killing on recorded PID, ssh tunnel signature, start-time sanity, and launchd parent with focused tests and live proof.
  2. Ask for one more macOS replay
    Require a maintainer-run macOS replay of crash, relaunch, and preferred-port reuse before landing if the process-kill boundary needs extra confidence.
  3. Pause for a narrower reaper
    Pause this PR only if maintainers want an even stricter ownership marker before any automatic SSH process termination is allowed.

Next step before merge

  • No repair lane is needed because this review found no blocking patch defect; the maintainer-labeled PR needs normal maintainer review and landing judgment around the process-reaping risk.

Security
Cleared: No concrete security or supply-chain concern was found; the diff changes macOS process cleanup only and constrains killing to recorded same-user SSH tunnel processes with multiple ownership gates.

Review details

Best 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 changes

Label changes:

  • add P1: The PR targets a remote-mode macOS resource leak that can leave users with orphaned SSH tunnels and a squatted preferred gateway port.
  • add merge-risk: 🚨 availability: If the new process classifier were too broad, merging could terminate a user's SSH tunnel process; the patch mitigates this with record, command, parent, and start-time gates.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix Swift test output, a real-process orphaned ssh classification test, and a live-machine classification check showing a live parent is kept while the orphan shape is reaped.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes after-fix Swift test output, a real-process orphaned ssh classification test, and a live-machine classification check showing a live parent is kept while the orphan shape is reaped.

Label justifications:

  • P1: The PR targets a remote-mode macOS resource leak that can leave users with orphaned SSH tunnels and a squatted preferred gateway port.
  • merge-risk: 🚨 availability: If the new process classifier were too broad, merging could terminate a user's SSH tunnel process; the patch mitigates this with record, command, parent, and start-time gates.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes after-fix Swift test output, a real-process orphaned ssh classification test, and a live-machine classification check showing a live parent is kept while the orphan shape is reaped.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix Swift test output, a real-process orphaned ssh classification test, and a live-machine classification check showing a live parent is kept while the orphan shape is reaped.
Evidence reviewed

PR surface:

Other +291. Total +291 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 0 0 0 0
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 4 295 4 +291
Total 4 295 4 +291

What I checked:

Likely related people:

  • steipete: Local history shows prior macOS PortGuardian/RemotePortTunnel work by Peter Steinberger, and this PR plus the linked issue provide the current macOS reproduction and fix. (role: recent area contributor and current fix author; confidence: high; commits: 92fb0caf35ab, 5efed492089e, cf67e374c0da; files: apps/macos/Sources/OpenClaw/PortGuardian.swift, apps/macos/Sources/OpenClaw/RemotePortTunnel.swift)
  • teslamint: PR metadata and git history tie the remote-mode PortGuardian no-kill invariant to the Docker Desktop protection change this PR must preserve. (role: adjacent remote-mode policy contributor; confidence: medium; commits: 2bfe18851056, 0cd05b11d075; files: apps/macos/Sources/OpenClaw/PortGuardian.swift, apps/macos/Tests/OpenClawIPCTests/LowCoverageHelperTests.swift)
  • drobison00: RemotePortTunnel history includes the trusted SSH host-key hardening PR by Devin Robison, making them relevant to SSH tunnel behavior review. (role: adjacent SSH tunnel hardening contributor; confidence: medium; commits: 114b87caf293, 8e837df1c2e3; files: apps/macos/Sources/OpenClaw/RemotePortTunnel.swift)
  • Tyler Yust: CONTRIBUTING.md lists Tyler Yust for the macOS app area, which is relevant routing context for maintainer review. (role: listed macOS app area owner; confidence: medium; files: CONTRIBUTING.md, apps/macos/Sources/OpenClaw/PortGuardian.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jul 5, 2026
@steipete
steipete force-pushed the claude/frosty-ptolemy-fc8ab1 branch from 3c0fcb8 to f543538 Compare July 6, 2026 01:40
@steipete
steipete merged commit 4a5bdc6 into main Jul 6, 2026
73 checks passed
@steipete
steipete deleted the claude/frosty-ptolemy-fc8ab1 branch July 6, 2026 01:53
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 6, 2026
…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
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: macos App: macos maintainer Maintainer-authored PR merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: macOS app leaks orphaned SSH tunnel processes; PortGuardian never reaps them and they squat the preferred local port

1 participant