Commit a582e3d398

a582e3d39894297342158889a5765630b4704e9d

parent: eed104c810

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-23 05:48 UTC

sshd, api: end a command when its reader goes away

Ref #250
cmd/gitbayd/system.go +1 −1
@@ -93,7 +93,7 @@ func shellCmd() *cobra.Command {
9393 fmt.Fprintf(os.Stderr, "gitbay control plane: interactive shells are not available.\nTry: ssh <host> help\n")
9494 os.Exit(protocol.ExitUsage)
9595 }
96 code := sshd.Exec(cfg, st, user, key.Scope, key.Fingerprint, cmdline, os.Stdin, os.Stdout, os.Stderr)
96 code := sshd.Exec(cfg, st, user, key.Scope, key.Fingerprint, cmdline, os.Stdin, os.Stdout, os.Stderr, nil)
9797 st.Close()
9898 os.Exit(code)
9999 return nil
internal/httpd/api.go +1
@@ -73,6 +73,7 @@ func (s *Server) apiCmd(w http.ResponseWriter, r *http.Request) {
7373 JSON: true,
7474 ViaAPI: true,
7575 ReadOnly: scope == "read",
76 Done: r.Context().Done(),
7677 }
7778 code := control.Dispatch(ctx, req.Argv)
7879
internal/httpd/apiread.go +1
@@ -62,6 +62,7 @@ func (s *Server) apiRead(w http.ResponseWriter, r *http.Request) {
6262 JSON: true,
6363 ViaAPI: true,
6464 ReadOnly: true,
65 Done: r.Context().Done(),
6566 }
6667 code := control.Dispatch(ctx, argv)
6768
internal/sshd/sshd.go +15 −4
@@ -238,7 +238,17 @@ func (s *Server) handleSession(sconn *ssh.ServerConn, ch ssh.Channel, reqs <-cha
238238 continue
239239 }
240240 req.Reply(true, nil)
241 code := s.runExec(sconn, ch, payload.Command)
241 // x/crypto closes reqs when the client closes the channel. That
242 // is how a follow learns nobody is reading: the CLI's shared
243 // connection outlives a Ctrl-C, the channel does not.
244 done := make(chan struct{})
245 go func() {
246 for r := range reqs {
247 r.Reply(false, nil)
248 }
249 close(done)
250 }()
251 code := s.runExec(sconn, ch, payload.Command, done)
242252 sendExit(ch, code)
243253 return
244254 case "shell":
@@ -260,7 +270,7 @@ func sendExit(ch ssh.Channel, code int) {
260270 ch.SendRequest("exit-status", false, ssh.Marshal(&msg))
261271}
262272
263func (s *Server) runExec(sconn *ssh.ServerConn, ch ssh.Channel, cmdline string) int {
273func (s *Server) runExec(sconn *ssh.ServerConn, ch ssh.Channel, cmdline string, done <-chan struct{}) int {
264274 ext := sconn.Permissions.Extensions
265275 if blob := ext["anon-key"]; blob != "" {
266276 return s.runAnonymous(ch, blob, cmdline)
@@ -273,7 +283,7 @@ func (s *Server) runExec(sconn *ssh.ServerConn, ch ssh.Channel, cmdline string)
273283 return protocol.ExitDenied
274284 }
275285 _ = s.st.TouchSSHKey(keyID)
276 return Exec(s.cfg, s.st, user, ext["scope"], ext["key-fp"], cmdline, ch, ch, ch.Stderr())
286 return Exec(s.cfg, s.st, user, ext["scope"], ext["key-fp"], cmdline, ch, ch, ch.Stderr(), done)
277287}
278288
279289// runAnonymous handles a session from an unregistered key: the register
@@ -304,7 +314,7 @@ func (s *Server) runAnonymous(ch ssh.Channel, keyB64, cmdline string) int {
304314// single dispatch path shared by the embedded listener and the system-sshd
305315// forced command (gitbayd shell).
306316func Exec(cfg config.Config, st *store.Store, user store.User, scope, source, cmdline string,
307 stdin io.Reader, stdout, stderr io.Writer) int {
317 stdin io.Reader, stdout, stderr io.Writer, done <-chan struct{}) int {
308318 if user.Disabled {
309319 fmt.Fprintln(stderr, "this account is disabled; contact the instance admin")
310320 return protocol.ExitDenied
@@ -341,6 +351,7 @@ func Exec(cfg config.Config, st *store.Store, user store.User, scope, source, cm
341351 Stdin: stdin,
342352 Stdout: stdout,
343353 Stderr: stderr,
354 Done: done,
344355 }
345356 return control.Dispatch(ctx, argv)
346357}