Skip to content

Prevent terminated e2e runs from leaking tmux - #849

Merged
mariusvniekerk merged 9 commits into
mainfrom
t3code/diagnose-e2e-tmux-session-leaks
Aug 9, 2026
Merged

Prevent terminated e2e runs from leaking tmux#849
mariusvniekerk merged 9 commits into
mainfrom
t3code/diagnose-e2e-tmux-session-leaks

Conversation

@mariusvniekerk

@mariusvniekerk mariusvniekerk commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Terminated e2e runners can leave daemonized tmux servers behind because tmux outlives its parent, while ordinary defers, exit callbacks, and late graceful cleanup are skipped or cut short by signals.\n\n- Stop each Go e2e server's private tmux server before bounded handler draining, including reset and termination paths.\n- Track Playwright-owned children from spawn through confirmed exit, preserve the shared socket root while a child can still create tmux state, and keep repeated SIGINT/SIGTERM handling active during cleanup.\n- Build one run-owned e2e-server binary before Playwright starts, share it with every worker, and let only the builder remove it. This keeps the real server process in the runner's signal path instead of a go run wrapper.\n- Sweep sockets before and after child exit, removing stale sockets only when tmux verifies their server is gone and retaining ambiguous failures for recovery.\n- Synchronize Go test tmux startup with signal cleanup so termination cannot land between daemonization and ownership registration.\n- Recover dead-owner directories only when their resources belong to the current local user. Developer tmux servers remain outside the cleanup boundary.\n\ngenerated by a clanker

@roborev-ci

roborev-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

roborev: Combined Review (5438ee2)

Code review found one High-severity credential-exposure risk and four Medium-severity cleanup gaps.

High

  • Unauthenticated stale tmux socket connectionfrontend/tests/e2e-full/support/e2eServer.ts:137
    reapStaleE2ETmuxDirs trusts matching /tmp/kf-e2e-tmux-* directories and their owner.json without verifying ownership or permissions. A local attacker could provide a malicious Unix socket and capture environment data, potentially including CI credentials, when the runner invokes tmux -S <socket> kill-server. Require the directory, owner file, and socket to be owned by the current UID with private permissions—preferably under a private per-user runtime directory—and launch cleanup clients with a minimal, scrubbed environment.

Medium

  • Signal handling ends before bounded cleanupinternal/server/api_test.go:101, internal/server/kataapi/testmain_test.go:30, internal/server/workspacetest/testmain_test.go:39
    A SIGTERM during the subsequent 5–10-second cleanup window can immediately terminate the test binary and leak tmux resources. Call runCleanup() before stopSignalCleanup(); sync.Once already serializes concurrent cleanup safely.

  • Fresh standalone servers lack termination cleanupfrontend/tests/e2e-full/support/e2eServer.ts:903
    The freshProcess: true branch registers servers in standaloneServers but never calls installCleanup, so workers using only this path can leak server and tmux resources on termination. Install cleanup when the first standalone server is registered and add a termination test for this path.

  • Child liveness and PID escalation are unsafefrontend/tests/e2e-full/support/e2eServer.ts:506
    Checking only child.exitCode misses signal-terminated children, while child may represent the go run wrapper rather than serverPID. Cleanup can wait on an exited wrapper and later signal a stale, reused PID. Track both exitCode and signalCode, and escalate through a verified process handle or owned process group.

  • Real-tmux tests rely only on t.Cleanupcmd/e2e-server/main_test.go:556
    Terminating the Go test binary can bypass cleanup and leak test-created tmux daemons. Add package-level signal cleanup that tracks and kills all private sockets created by these tests before exit.


Reviewers: 2 done | Synthesis: codex, 19s | Total: 13m17s

mariusvniekerk added a commit that referenced this pull request Aug 6, 2026
The first shutdown hardening still registered some children only after readiness, let individual fresh servers remove a shared socket root, and could stop listening for signals before normal tmux cleanup completed. Those gaps left termination races where a starting child, a late socket, or a second signal could still strand test resources.

Keep process ownership from spawn through exit, signal all owned servers before sweeping and retry after exit, preserve failed cleanup roots, reject stale resources owned by another local user, and keep Go signal handling active until tracked tmux servers are gone.

Review context: roborev-ci PR #849 comment 5200866219 and local roborev jobs 10213 and 10214. PID reuse does not add cross-platform process-start introspection: it can only delay recovery, while incomplete owner files are written before any child or tmux socket can exist.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
@roborev-ci

roborev-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

roborev: Combined Review (e8fd2b2)

The shutdown cleanup is improved, but three medium-severity races can still leak tmux daemons or server processes.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:649 — The initial tmux cleanup removes the directory while server children are still shutting down. A child can create a socket between the scan and removal, unlinking a live server’s socket and preventing final cleanup. During pre-wait cleanup, kill discovered sockets without removing the directory; remove it only after all children exit and a final rescan handles late sockets.

  • cmd/e2e-server/main.go:963tmuxStopOnce suppresses cleanup after the initial pre-drain attempt. An in-flight request can start or restart tmux afterward, allowing termination or reset to leak the daemon. Retain the early kill, but perform an additional, ungated kill after HTTP handlers drain and immediately before exit.

  • frontend/tests/e2e-full/support/e2eServer.ts:506 — Before readiness, cleanup tracks only the go run wrapper PID. Signaling that wrapper may leave the actual e2e-server descendant orphaned, and the new test bypasses this path by spawning a prebuilt binary directly. Launch a directly managed binary or run the wrapper and descendant in a process group that cleanup terminates and waits for together.


Reviewers: 2 done | Synthesis: codex, 12s | Total: 6m59s

@roborev-ci

roborev-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

roborev: Combined Review (c667efd)

One-line verdict: Two medium-severity teardown races remain in the Playwright e2e server lifecycle.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:652 — Before readiness, default servers launched via go run are tracked only by the Go wrapper PID. Shutdown does not signal the process group, so the compiled child may survive, publish readiness later, and leak tmux state after the socket root is removed. Launch a prebuilt binary or use an owned process group and terminate the entire group during pre-readiness cleanup.

  • frontend/tests/e2e-full/support/e2eServer.ts:658shutdownOwnedServers clears ownership registries before child processes exit. Concurrent shutdown or standalone stop() can then remove the shared tmux root while children remain alive, reopening the late-socket race. Serialize shutdown with a shared in-flight promise and retain ownership, or track shutdown-in-progress state, until all captured children exit and the final socket sweep completes.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 9m43s

@mariusvniekerk mariusvniekerk self-assigned this Aug 6, 2026
@roborev-ci

roborev-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

roborev: Combined Review (f215e3e)

High-severity cleanup flaw remains; two medium-severity shutdown races may also leak resources or break active handlers.

High

  • frontend/tests/e2e-full/support/e2eServer.ts:595 — Before server info is available, termination targets child.pid. With the default go run command, this is the Go wrapper’s PID rather than the e2e binary’s, so killing the wrapper can orphan a running server after its ownership and tmux root are released. Launch the server in an owned process group and terminate/wait for that group, or execute a built binary directly. Add coverage using the default go run invocation.

Medium

  • cmd/e2e-server/main.go:963tmuxStopOnce permanently suppresses cleanup retries. A handler may create a tmux server after the early pre-drain kill, or that kill may time out, while the later close() cannot retry. Keep the early best-effort kill, then retry after handlers and workspace cleanup have drained using synchronization that prevents concurrent—not subsequent—attempts.

  • cmd/e2e-server/main.go:984 — If handler draining times out, close() immediately cleans workspaces, closes SQLite, and removes the temporary directory while old handlers may still be running. During an in-process reset, those handlers can access closed or deleted resources. Defer destructive cleanup until handlerWG completes; only the bounded shutdown path should return early.


Reviewers: 2 done | Synthesis: codex, 14s | Total: 9m22s

mariusvniekerk added a commit that referenced this pull request Aug 6, 2026
A terminated go run wrapper could exit while its compiled e2e server stayed alive. The runner then lost the child process it needed to stop, so tmux cleanup could finish while the real server was still running.

Build one run-owned server binary before Playwright starts and share it with every worker. Keep explicit binaries externally owned, and bind generated-binary cleanup to the process that built it so a terminated worker cannot remove the binary from its siblings.

Review context: roborev-ci PR #849 comment 5206859932.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
@roborev-ci

roborev-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

roborev: Combined Review (f786b39)

The changes introduce three medium-severity shutdown and cleanup race conditions; no security vulnerabilities were identified.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:840 — Playwright workers track spawned servers only in process-local sets. During normal worker exit, cleanupImmediately signals children without awaiting them, while the root runner may remove the shared tmux directory before those children finish exiting. A late-created socket could then be unlinked without terminating its tmux daemon. Persist child ownership in the shared directory or add asynchronous worker teardown that lets the root owner wait for every worker-owned server.

  • frontend/tests/e2e-full/support/e2eServer.ts:875 — If asynchronous cleanup rejects, process.exit(exitCode) is never reached. Since signalCleanupStarted remains true, later SIGINT/SIGTERM signals are ignored, potentially leaving the runner alive or causing an unhandled-rejection exit before cleanup completes. Catch and report cleanup failures, and invoke process.exit(exitCode) from a finally block.

  • cmd/e2e-server/main.go:984 — After handler draining times out, shutdown still deletes workspaces, closes SQLite, and removes the temporary directory while handlers remain active. During an in-process reset, those handlers can access destroyed state, causing request failures or races. Keep the early tmux stop bounded, but defer destructive cleanup until handlerWG finishes.


Reviewers: 2 done | Synthesis: codex, 26s | Total: 9m56s

mariusvniekerk added a commit that referenced this pull request Aug 7, 2026
Playwright workers can exit before their e2e servers finish, while the root runner previously had no shared ownership record to stop it from deleting their tmux socket root. The e2e server could also admit a new tmux session while its one-shot private-server kill was starting.

Publish child ownership across workers, close session admission before tmux teardown, and keep signal-driven runner exit reliable when cleanup reports an error. This keeps late sockets discoverable until every admitted creator and owned server has stopped.

Review outcomes for roborev-ci comment #849 (comment):

- Fixed: the root runner now waits for worker-published server processes before removing the shared tmux directory.
- Fixed: signal cleanup reports failures and exits from a finally block.
- User: not an issue — forced teardown must proceed after bounded handler draining; active handlers may fail after their resources are destroyed.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@roborev-ci

roborev-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (f3e19b7)

The test cleanup is improved overall, but one medium-severity orphan-process leak remains.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:878 — The root runner waits for worker-published server PIDs but never terminates them. If a worker crashes or receives SIGKILL before its exit handler runs, its e2e server can remain alive indefinitely, causing cleanup to time out and preserve the tmux root. Publish sufficient process identity for safe root-runner termination, and add a cross-process test that abruptly kills a worker and verifies both the server process and tmux root disappear.

Reviewers: 2 done | Synthesis: codex, 13s | Total: 12m31s

@roborev-ci

roborev-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (481ab95)

Review found two medium-severity reliability issues in tmux lifecycle handling.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:226 — Stale-root recovery deletes the tmux directory when its root owner is dead without checking live server-owner-*.json children. An orphaned worker may still be using that socket root. Preserve the root while any published server PID is live, and test the dead-root/live-server case.

  • cmd/e2e-server/main.go:1187 — The executable gate can mask an unavailable underlying tmux binary, suppressing PTY-owner fallback and advertising targets that cannot launch. Install the gate only when the underlying tmux command is executable; otherwise retain the unavailable command.


Reviewers: 2 done | Synthesis: codex, 6s | Total: 8m15s

mariusvniekerk added a commit that referenced this pull request Aug 7, 2026
The first shutdown hardening still registered some children only after readiness, let individual fresh servers remove a shared socket root, and could stop listening for signals before normal tmux cleanup completed. Those gaps left termination races where a starting child, a late socket, or a second signal could still strand test resources.

Keep process ownership from spawn through exit, signal all owned servers before sweeping and retry after exit, preserve failed cleanup roots, reject stale resources owned by another local user, and keep Go signal handling active until tracked tmux servers are gone.

Review context: roborev-ci PR #849 comment 5200866219 and local roborev jobs 10213 and 10214. PID reuse does not add cross-platform process-start introspection: it can only delay recovery, while incomplete owner files are written before any child or tmux socket can exist.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
mariusvniekerk added a commit that referenced this pull request Aug 7, 2026
A terminated go run wrapper could exit while its compiled e2e server stayed alive. The runner then lost the child process it needed to stop, so tmux cleanup could finish while the real server was still running.

Build one run-owned server binary before Playwright starts and share it with every worker. Keep explicit binaries externally owned, and bind generated-binary cleanup to the process that built it so a terminated worker cannot remove the binary from its siblings.

Review context: roborev-ci PR #849 comment 5206859932.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
mariusvniekerk added a commit that referenced this pull request Aug 7, 2026
Playwright workers can exit before their e2e servers finish, while the root runner previously had no shared ownership record to stop it from deleting their tmux socket root. The e2e server could also admit a new tmux session while its one-shot private-server kill was starting.

Publish child ownership across workers, close session admission before tmux teardown, and keep signal-driven runner exit reliable when cleanup reports an error. This keeps late sockets discoverable until every admitted creator and owned server has stopped.

Review outcomes for roborev-ci comment #849 (comment):

- Fixed: the root runner now waits for worker-published server processes before removing the shared tmux directory.
- Fixed: signal cleanup reports failures and exits from a finally block.
- User: not an issue — forced teardown must proceed after bounded handler draining; active handlers may fail after their resources are destroyed.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@mariusvniekerk
mariusvniekerk force-pushed the t3code/diagnose-e2e-tmux-session-leaks branch from 02e1d3b to d46e9ec Compare August 7, 2026 14:10
@roborev-ci

roborev-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (d46e9ec)

Medium-severity shutdown races and an external-server build regression remain.

Medium

  • cmd/e2e-server/main.go:981 — Shutdown may kill tmux without acquiring the creation lock. An admitted new-session can daemonize afterward, while tmuxStopOnce prevents further cleanup. Terminate or await the admitted creator before killing tmux; never proceed without owning the gate.

  • internal/testutil/testsignal/testsignal.go:35 — Signal cleanup runs concurrently with m.Run, allowing active tests to create tmux resources after one-shot cleanup and before exit. Block new resource creation at shutdown, await admitted creators, then clean up and exit.

  • frontend/playwright-e2e.config.ts:4 — The runner always builds a local server binary before checking for a reusable external server, unnecessarily requiring frontend and Go builds. Build lazily only when a local server must be spawned.


Reviewers: 2 done | Synthesis: codex, 11s | Total: 7m31s

@roborev-ci

roborev-ci Bot commented Aug 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (43f16c9)

Shutdown cleanup still has three medium-severity race conditions.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:281 — Shared owner files may disappear between readdir, lstat, and readFile. This routine concurrent cleanup is treated as an ownership failure, permanently retaining the tmux root. Treat ENOENT as progress and retry the scan; publish owner files atomically.

  • cmd/e2e-server/main.go:1061 — If handler draining times out, close destroys the database and temporary files while handlers remain active, causing use-after-close failures during resets. Defer destruction until handlers finish, or return on timeout and clean up asynchronously afterward.

  • cmd/e2e-server/main.go:992 — After five seconds, tmux shutdown proceeds without the creation lock. An admitted new-session command can create a server after kill-server, while tmuxStopOnce prevents further cleanup. Track and terminate the admitted creator, or wait for the barrier and perform a final server sweep.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 7m44s

mariusvniekerk and others added 9 commits August 9, 2026 12:26
Daemonized tmux servers can outlive both Go and Playwright runners, while the previous cleanup ran late during graceful shutdown or only fired a signal without waiting. Abruptly terminated test runs therefore accumulated private sessions and sockets on developer and CI hosts.

Make the test runner's ownership boundary recoverable across SIGTERM, timeouts, and prior crashed runs. Tmux teardown now happens before potentially blocking drains, child shutdown is bounded with escalation, and a later run can reap socket roots whose owner no longer exists.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
The first shutdown hardening still registered some children only after readiness, let individual fresh servers remove a shared socket root, and could stop listening for signals before normal tmux cleanup completed. Those gaps left termination races where a starting child, a late socket, or a second signal could still strand test resources.

Keep process ownership from spawn through exit, signal all owned servers before sweeping and retry after exit, preserve failed cleanup roots, reject stale resources owned by another local user, and keep Go signal handling active until tracked tmux servers are gone.

Review context: roborev-ci PR #849 comment 5200866219 and local roborev jobs 10213 and 10214. PID reuse does not add cross-platform process-start introspection: it can only delay recovery, while incomplete owner files are written before any child or tmux socket can exist.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
Concurrent fresh-server stops and repeated termination signals could release process ownership or unlink the shared socket root while another child was still exiting. Go test helpers also registered tmux only after daemonization, leaving a signal window where cleanup could miss a live server.

Keep ownership through observed exit, preserve the root during pre-exit sweeps, verify stale sockets before removal, and serialize Go tmux startup with cleanup. A real Node owner to Go server to workspace tmux regression now exercises the complete signal path.

Review context: local roborev jobs 10231 and 10232.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
Global shutdown could empty its ownership registries before children exited, allowing overlapping cleanup to remove the shared tmux root beneath a live process. A forced-stop timeout and an ambiguous tmux list error could also be mistaken for confirmed cleanup.

Serialize overlapping shutdown, keep every child registered until exit is observed, and accept only the explicit no-server result before clearing Go tracking. This keeps failed resources recoverable instead of silently forgetting them.

Review context: local roborev job 10241.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
A terminated go run wrapper could exit while its compiled e2e server stayed alive. The runner then lost the child process it needed to stop, so tmux cleanup could finish while the real server was still running.

Build one run-owned server binary before Playwright starts and share it with every worker. Keep explicit binaries externally owned, and bind generated-binary cleanup to the process that built it so a terminated worker cannot remove the binary from its siblings.

Review context: roborev-ci PR #849 comment 5206859932.

Generated with Codex (gpt-5.6-sol)
Co-authored-by: Codex <noreply@openai.com>
Playwright workers can exit before their e2e servers finish, while the root runner previously had no shared ownership record to stop it from deleting their tmux socket root. The e2e server could also admit a new tmux session while its one-shot private-server kill was starting.

Publish child ownership across workers, close session admission before tmux teardown, and keep signal-driven runner exit reliable when cleanup reports an error. This keeps late sockets discoverable until every admitted creator and owned server has stopped.

Review outcomes for roborev-ci comment #849 (comment):

- Fixed: the root runner now waits for worker-published server processes before removing the shared tmux directory.
- Fixed: signal cleanup reports failures and exits from a finally block.
- User: not an issue — forced teardown must proceed after bounded handler draining; active handlers may fail after their resources are destroyed.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
GitHub pull-request jobs use a detached merge checkout whose version-control status probe can fail after checkout credentials are removed. The Chromium and Firefox jobs therefore stopped while building the direct e2e server, before Playwright could start.

The run-owned test binary does not publish build version metadata. Disable Go VCS stamping for that disposable build so its result does not depend on checkout metadata.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
The e2e runner deliberately uses a missing tmux executable when a test must select the in-process PTY owner. Wrapping that path in the shutdown gate made it appear executable, so backend detection chose tmux and the kitty-keyboard browser test failed on every retry.

Leave the intentional no-tmux mode unwrapped. Ordinary e2e states still use the creation gate, while PTY-owner tests keep the unavailable-command signal that selects their required backend.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
Windows retains an exited process object while a handle remains open. The daemon CLI killed a verified process but kept the handle returned by FindProcess, so lifecycle commands treated the terminated PID as alive until their 70-second timeout.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@mariusvniekerk
mariusvniekerk force-pushed the t3code/diagnose-e2e-tmux-session-leaks branch from 43f16c9 to f3971fa Compare August 9, 2026 16:30
@roborev-ci

roborev-ci Bot commented Aug 9, 2026

Copy link
Copy Markdown

roborev: Combined Review (f3971fa)

The changes have three cleanup race/ownership issues, including one high-severity risk to concurrently running specs.

High

  • frontend/tests/e2e-full/support/e2eServer.ts:910 — Exit cleanup calls cleanupManagedServerProcess(managedChild) even when managedChild is null. The function then falls back to the inherited PLAYWRIGHT_E2E_SERVER_INFO_FILE, allowing workers and signal-test helpers to terminate the runner-owned shared server and disrupt concurrent specs.
    • Fix: Run cleanup only when the module owns a non-null managedChild, pass its local info file explicitly, and discard inherited shared-server ownership variables in subprocess helpers.

Medium

  • frontend/tests/e2e-full/support/e2eServer.ts:818 — Individual shutdown decides whether to remove ownedTmuxDir using only in-process server collections, ignoring live server-owner-*.json records from other processes. Stopping the creator’s last local server can therefore kill another worker’s tmux server and delete its shared socket root.

    • Fix: Check shared owner records before removing the root, or reserve root removal for coordinated global shutdown.
  • cmd/e2e-server/main.go:994 — After five seconds, tmuxCreationGate.stop proceeds without acquiring creation.lock. An already admitted new-session can create a tmux server after kill-server completes, while tmuxStopOnce prevents further cleanup.

    • Fix: Do not finalize shutdown without owning the gate; terminate and await the admitted creator on timeout, or retain retryable cleanup state and perform a final socket sweep.

Reviewers: 2 done | Synthesis: codex, 13s | Total: 10m21s

@mariusvniekerk
mariusvniekerk merged commit 60d4d7e into main Aug 9, 2026
17 checks passed
@mariusvniekerk
mariusvniekerk deleted the t3code/diagnose-e2e-tmux-session-leaks branch August 9, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant