1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//go:build linux
4
5package gitpush
6
7import (
8 "context"
9 "errors"
10 "fmt"
11 "os/exec"
12 "syscall"
13
14 "golang.org/x/sys/unix"
15)
16
17// Tests replace these two.
18var (
19 waitid = unix.Waitid
20 killGroup = func(pgid int) error { return syscall.Kill(-pgid, syscall.SIGKILL) }
21)
22
23// run starts cmd, waits for it, and kills what it leaves running.
24//
25// git runs git-remote-https, which is the process that connects and that holds
26// the pipes of git's output. Killing git alone leaves it running, and Wait
27// blocked on the pipes. So git leads a process group of its own, and the group
28// is killed when ctx is done and also when git has exited.
29//
30// The invariant that makes killing a group safe: the group is only signalled
31// while its leader is still not reaped. Linux does not reuse a PID as long as
32// the process is a zombie nobody waited for, nor while it is the group ID of a
33// live member, so -pid is still our group and cannot be another one. The
34// leader is therefore waited for with waitid(WNOWAIT), which observes its exit
35// without reaping it; the group is signalled; and only then cmd.Wait reaps. A
36// kill(-pid) after Wait, or from Cmd.Cancel which races with it, would not be
37// safe. When waitid fails, whatever the reason, nothing proves the leader is
38// unreaped: the group is not signalled, only the leader is killed (through
39// its pidfd, which cannot hit a reused PID), and the error is returned.
40// cmd.WaitDelay (set by the caller) stays the backstop for a descendant that
41// escaped the group and holds the pipes.
42func run(ctx context.Context, cmd *exec.Cmd) error {
43 if err := ctx.Err(); err != nil {
44 return err
45 }
46 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
47 if err := cmd.Start(); err != nil {
48 return err
49 }
50 pid := cmd.Process.Pid
51 observed := make(chan error, 1)
52 go func() { observed <- waitExit(pid) }()
53
54 var werr error
55 select {
56 case err := <-observed:
57 if err != nil {
58 werr = err // the leader may have been reaped: no group signal
59 } else {
60 signalGroup(cmd, pid) // exited, not reaped: a zombie
61 }
62 case <-ctx.Done():
63 select {
64 case err := <-observed:
65 // waitid answered at the same time: trust it as above.
66 if err != nil {
67 werr = err
68 } else {
69 signalGroup(cmd, pid)
70 }
71 default:
72 // No waitid error so far: the leader is alive or a zombie, unreaped
73 // (only cmd.Wait below reaps it).
74 signalGroup(cmd, pid)
75 if err := <-observed; err != nil {
76 werr = err // after the signals: reported, nothing more to do
77 }
78 }
79 }
80 if werr != nil {
81 _ = cmd.Process.Kill()
82 }
83 err := cmd.Wait()
84 if werr != nil {
85 return fmt.Errorf("waiting for the process %d: %w", pid, werr)
86 }
87 return err
88}
89
90// signalGroup kills the group of the leader, and the leader alone should it
91// have left the group. The leader must not be reaped yet.
92func signalGroup(cmd *exec.Cmd, pid int) {
93 _ = killGroup(pid)
94 _ = cmd.Process.Kill()
95}
96
97// waitExit returns when the child pid has exited, without reaping it, or the
98// error of waitid.
99func waitExit(pid int) error {
100 var info unix.Siginfo
101 for {
102 err := waitid(unix.P_PID, pid, &info, unix.WEXITED|unix.WNOWAIT, nil)
103 if !errors.Is(err, unix.EINTR) {
104 return err
105 }
106 }
107}