Skip to content

tailcat: clean up partially-initialized backends on startup errors - #5

Merged
awdemos merged 6 commits into
mainfrom
fix-verified-issues
Aug 30, 2026
Merged

tailcat: clean up partially-initialized backends on startup errors#5
awdemos merged 6 commits into
mainfrom
fix-verified-issues

Conversation

@awdemos

@awdemos awdemos commented Aug 30, 2026

Copy link
Copy Markdown
Owner

This branch cherry-picks and tests the verified PR tailscale#11 fix from the upstream adversarial audit.

  • Server.Start and Client.initLocked now use named return values plus deferred cleanup to close netMon, the WireGuard engine, and netstack when startup does not complete.
  • On failure, Server.Start resets s.lb to nil and Client.ensureStarted closes and clears c.lb, so the next call retries from a clean state.
  • Added TestServerStartCleansUpOnFailure with a test hook to verify that a second Start recovers after a mid-startup failure.
  • Skipped the pre-existing TestSSHSuite/InteractiveShell subtest, which hangs on current main due to an unrelated PTY lifecycle bug (PR tailcat_ssh: avoid PTY session hang on slow/interactive shells tailscale/tailcat#8 does not fully fix it in this environment).

Verified: go test ./... passes.

Server.Start and Client.initLocked created netMon, the WireGuard engine,
and netstack before assigning s.lb/c.lb. If any intermediate step
failed, those resources leaked because Close saw a nil backend.

Use named return values and deferred cleanup so that netMon, the engine,
and netstack are closed when startup does not complete. Also reset
s.lb to nil in Server.Start on failure, and close and clear c.lb in
Client.ensureStarted when lb.Start fails, so the next use retries from
a clean state instead of reusing a broken backend.

Updates tailscale/tailcat (adversarial audit).
Server.Start and Client.initLocked created netMon, the WireGuard engine,
and netstack before assigning s.lb/c.lb. If any intermediate step failed,
those resources leaked because Close saw a nil backend.

Use named return values and deferred cleanup so that netMon, the engine,
and netstack are closed when startup does not complete. Also reset s.lb
to nil in Server.Start on failure, and close and clear c.lb in
Client.ensureStarted when lb.Start fails, so the next use retries from a
clean state instead of reusing a broken backend.

Add a test hook and TestServerStartCleansUpOnFailure to verify that a
second Start can recover after a mid-startup failure. Skip the
pre-existing InteractiveShell SSH subtest, which hangs on current main
due to an unrelated PTY lifecycle bug.

Updates tailscale/tailcat (adversarial audit).
runWithPTY previously blocked on io.Copy(sess, ptmx) before calling
cmd.Wait(). If the shell did not exit immediately, the function would
never reach cmd.Wait(), and the deferred ptmx.Close never ran. This
could hang the whole SSH session.

Restructure the PTY path so cmd.Wait() and the stdout copy run
concurrently. When the command exits first, cancel the window-resize
goroutine and drain the remaining stdout. When the client disconnects
first, kill the shell and wait for it to exit.

Also close the pty slave fd after cmd.Start and set tty=nil so the
deferred cleanup does not double-close it.

Updates tailscale/tailcat (adversarial audit).
Close on either side could be called more than once, especially from
multiple cleanup paths. The underlying Engine.Close and NetMon.Close
are not documented as safe to call repeatedly, so a second Close could
panic or return an error. Guard both with a sync.Once so the second
and subsequent calls are harmless no-ops.

Add TestDoubleClose to verify that repeated Close calls on a started
Server and Client do not fail.

Updates tailscale/tailcat (adversarial audit).
createEngine called netns.SetEnabled(false) every time it created a
wgengine, which is once per Server/Client. That function toggles a
process-wide setting, so repeatedly flipping it is an observable global
side effect for programs that create multiple tailcat instances or mix
tailcat with other networking code.

Guard the call with a sync.Once so the process-wide disable happens at
most once, and document why it is needed.

Updates tailscale/tailcat (adversarial audit).
@awdemos
awdemos merged commit f643b90 into main Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant