Skip to content

Commit f912c31

Browse files
JAORMXclaude
andcommitted
Fix session deadlock on command exit
After the remote command exits, the channel→pty goroutine blocks forever on ch.Read() because the SSH channel is still open. The channel is only closed by handleSession's defer which can't run until runWithPTY returns — creating a circular dependency. Remove the WaitGroup entirely. The goroutine is cleaned up when handleSession closes the channel after runWithPTY returns. This matches how dropbear and OpenSSH handle session teardown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e41c36f commit f912c31

1 file changed

Lines changed: 2 additions & 13 deletions

File tree

internal/guest/sshd/session.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"log/slog"
1414
"os"
1515
"os/exec"
16-
"sync"
1716
"syscall"
1817
"unsafe" //nolint:gosec // TIOCSWINSZ ioctl requires unsafe pointer
1918

@@ -259,26 +258,16 @@ func (s *Server) runWithPTY(ch ssh.Channel, cmd *exec.Cmd, state *sessionState,
259258
}()
260259

261260
// Bidirectional copy.
262-
var wg sync.WaitGroup
263-
wg.Add(1)
264261
go func() {
265-
defer wg.Done()
266262
_, _ = io.Copy(ptmx, ch)
267263
}()
268264

269265
_, _ = io.Copy(ch, ptmx)
270266

271-
// Wait for process to exit.
267+
// Wait for process to exit. The channel→pty goroutine will be
268+
// cleaned up when handleSession closes the channel after we return.
272269
err = cmd.Wait()
273270

274-
// Close the channel to unblock the channel→pty goroutine which is
275-
// stuck on ch.Read(). Without this, wg.Wait() deadlocks because
276-
// the goroutine never returns — the channel stays open until
277-
// handleSession's defer, which can't run until we return.
278-
_ = ch.CloseWrite()
279-
280-
wg.Wait()
281-
282271
return exitCode(err)
283272
}
284273

0 commit comments

Comments
 (0)