From b008a8b3e426f15cb5bb8f3d2bcaa63b855c8f78 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 13:29:51 +0000 Subject: [PATCH] fix(pilotctl): atomically claim PID file with O_CREAT|O_EXCL (PILOT-292) Two concurrent 'pilotctl daemon start' invocations race between the stale-PID cleanup and the os.WriteFile that records the new daemon PID. The second starter overwrites the first's PID file, orphaning a daemon with no PID-file entry. Fix: claim the PID file with O_CREAT|O_EXCL immediately after the stale-PID check. The atomic open-or-create ensures only one start succeeds; a concurrent second invocation sees the lock file and exits before spawning a second daemon. The existing WriteFile after proc.Start() overwrites the placeholder with the real PID. Closes PILOT-292 --- cmd/pilotctl/main.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/pilotctl/main.go b/cmd/pilotctl/main.go index 0a37bc3d..d7a87a37 100644 --- a/cmd/pilotctl/main.go +++ b/cmd/pilotctl/main.go @@ -2288,6 +2288,18 @@ func cmdDaemonStart(args []string) { os.Remove(pidFilePath()) } + // Atomically claim the PID file to prevent concurrent daemon starts. + // O_CREAT|O_EXCL ensures only one pilotctl daemon start can succeed; + // a second concurrent invocation fails here before spawning a daemon. + if f, err := os.OpenFile(pidFilePath(), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600); err != nil { + fatalHint("already_exists", + "stop it first with: pilotctl daemon stop", + "another daemon start is in progress (PID file locked)") + } else { + _, _ = f.WriteString("0\n") + f.Close() + } + daemonArgs, socketPath := buildDaemonArgs(args) // Clean up stale socket