diff --git a/internal/util/process.go b/internal/util/process.go index 38d9ef37b..6f0150e82 100644 --- a/internal/util/process.go +++ b/internal/util/process.go @@ -3,6 +3,7 @@ package util import ( + "errors" "os" "strings" "syscall" @@ -23,7 +24,13 @@ func IsProcessAlive(pid int) bool { if err != nil { return false } - if proc.Signal(syscall.Signal(0)) != nil { + // #2190: kill(pid, 0) EPERM means the process EXISTS but we lack + // permission to signal it (POSIX) - treating it as dead mirrors the + // exact bug Windows fixed in #1723 (a privileged daemon probed by an + // unprivileged caller: false-dead deletes the PID file and forks a + // second instance, or triggers concurrent crash recovery - the Unix + // twin of #1490). Only a real error (ESRCH et al.) means dead. + if err := proc.Signal(syscall.Signal(0)); err != nil && !errors.Is(err, syscall.EPERM) { return false } return !isZombieUnix(pid) diff --git a/internal/util/zz_issue2190_test.go b/internal/util/zz_issue2190_test.go new file mode 100644 index 000000000..068a08a38 --- /dev/null +++ b/internal/util/zz_issue2190_test.go @@ -0,0 +1,41 @@ +//go:build !windows + +package util + +// #2190 regression: kill(pid,0) EPERM means the process EXISTS but is +// not signalable by us (POSIX) - it was returned as dead, the Unix twin +// of the #1723 Windows bug (false-dead deletes PID files and forks +// second daemons / concurrent crash recovery). The EPERM branch itself +// needs a cross-privilege process (not unit-testable); these pin the +// surrounding semantics that must not regress alongside it. + +import ( + "os" + "syscall" + "testing" +) + +func TestIsProcessAliveBasicSemantics(t *testing.T) { + if !IsProcessAlive(os.Getpid()) { + t.Fatal("self must be alive") + } + // A definitely-dead PID: spawn and reap one. + r, w, err := os.Pipe() + if err != nil { + t.Skipf("pipe: %v", err) + } + defer r.Close() + defer w.Close() + pid, err := syscall.ForkExec("/bin/true", []string{"/bin/true"}, &syscall.ProcAttr{Files: []uintptr{r.Fd(), w.Fd(), w.Fd()}}) + if err != nil { + t.Skipf("forkexec: %v", err) + } + // Wait for exit; probe until dead (bounded). + for i := 0; i < 100; i++ { + if !IsProcessAlive(pid) { + return // dead, as expected + } + syscall.Wait4(pid, nil, syscall.WNOHANG, nil) + } + t.Fatal("reaped child must be reported dead") +}