From 072a9959cc3147d71457ffcb849c102eba9c1e5c Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 12:59:32 +0000 Subject: [PATCH] fix(pilotctl): atomic symlink replacement to close TOCTOU race on pilot.log (PILOT-291) The daemon start path removes the pilot.log symlink then creates a new one pointing at pilot-{pid}.log. The gap between os.Remove and os.Symlink is a TOCTOU window where a local attacker with write access to ~/.pilot/ can swap the target. Fix: write the symlink to a .tmp path first, then os.Rename over the canonical path. os.Rename is atomic on the same filesystem on Linux, closing the window entirely. Closes PILOT-291 --- cmd/pilotctl/main.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cmd/pilotctl/main.go b/cmd/pilotctl/main.go index 0a37bc3d..3dea39d8 100644 --- a/cmd/pilotctl/main.go +++ b/cmd/pilotctl/main.go @@ -2352,9 +2352,14 @@ func cmdDaemonStart(args []string) { logFile.Close() os.Rename(tmpLogPath, pidLogPath) // Update pilot.log symlink to point at the current PID's log. + // Atomically replace via temp file to avoid TOCTOU race (the + // gap between Remove and Symlink is exploitable by a local + // attacker with write access to the config directory). symPath := logFilePath() - os.Remove(symPath) - os.Symlink(pidLogPath, symPath) + tmpSymPath := symPath + ".tmp" + os.Remove(tmpSymPath) // clean stale temp from prior crash + os.Symlink(pidLogPath, tmpSymPath) + os.Rename(tmpSymPath, symPath) if !jsonOutput { fmt.Fprintf(os.Stderr, "starting daemon (pid %d, socket %s)...", pid, socketPath)