Merge pull request #3000 from chengzhichao-xydt/codex/pid-verify-process-identity

fix(pid): verify process identity in singleton PID check
This commit is contained in:
Mauro
2026-06-05 00:02:55 +02:00
committed by GitHub
3 changed files with 56 additions and 2 deletions
+8 -1
View File
@@ -64,7 +64,14 @@ func WritePidFile(homePath, host string, port int) (*PidFileData, error) {
// pass the isProcessRunning check, blocking new gateway starts.
// Treat recorded PID 1 as always stale.
if data.PID != 1 && isProcessRunning(data.PID) {
return nil, fmt.Errorf("gateway is already running (PID: %d, version: %s)", data.PID, data.Version)
// Verify the process is actually a picoclaw instance.
// If the PID was reused by an unrelated process
// (e.g. systemd-resolved after a kill -9), treat
// the PID file as stale and proceed with startup.
if isPicoclawProcess(data.PID) {
return nil, fmt.Errorf("gateway is already running (PID: %d, version: %s)", data.PID, data.Version)
}
logger.Warnf("found pid file (PID: %d) but process is not picoclaw", data.PID)
}
logger.Warnf("not running (PID: %d) so will remove the pid file: %s", data.PID, pidPath)
}
+16 -1
View File
@@ -4,7 +4,9 @@ package pid
import (
"errors"
"fmt"
"os"
"strings"
"syscall"
)
@@ -18,7 +20,7 @@ func isProcessRunning(pid int) bool {
if err != nil {
return false
}
// Signal(nil) does not kill the process but checks existence on Unix.
// Signal(0) does not kill the process but checks existence on Unix.
err = p.Signal(syscall.Signal(0))
if err == nil {
return true
@@ -27,3 +29,16 @@ func isProcessRunning(pid int) bool {
// EPERM means the process exists but we are not allowed to signal it.
return errors.As(err, &errno) && errno == syscall.EPERM
}
// isPicoclawProcess reads /proc/<pid>/comm to confirm the process name
// contains "picoclaw". Returns false when the comm file can be read and
// the name does not match (e.g., PID was reused by an unrelated process).
// Returns true if /proc/<pid>/comm is unreadable so the call site falls
// back to trusting the liveness check alone.
func isPicoclawProcess(pid int) bool {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err != nil {
return true // cannot verify — trust liveness check
}
return strings.Contains(strings.TrimSpace(string(data)), "picoclaw")
}
+32
View File
@@ -3,6 +3,7 @@
package pid
import (
"strings"
"syscall"
"unsafe"
)
@@ -12,6 +13,7 @@ var (
procOpenProcess = kernel32.NewProc("OpenProcess")
procGetExitCodeProcess = kernel32.NewProc("GetExitCodeProcess")
procCloseHandle = kernel32.NewProc("CloseHandle")
procQueryFullProcessImageNameW = kernel32.NewProc("QueryFullProcessImageNameW")
processQueryLimitedInformation = uint32(0x1000)
stillActive = uint32(259)
)
@@ -40,3 +42,33 @@ func isProcessRunning(pid int) bool {
}
return exitCode == stillActive
}
// isPicoclawProcess uses QueryFullProcessImageNameW to confirm the
// process image name contains "picoclaw". Returns false when the name
// clearly does not match. Returns true if the query fails, falling
// back to trusting the liveness check alone.
func isPicoclawProcess(pid int) bool {
handle, _, _ := procOpenProcess.Call(
uintptr(processQueryLimitedInformation),
0,
uintptr(pid),
)
if handle == 0 {
return true // cannot open — trust liveness check
}
defer procCloseHandle.Call(handle)
var buf [260]uint16
var size uint32 = 260
ret, _, _ := procQueryFullProcessImageNameW.Call(
uintptr(handle),
0, // WIN32_NAME_FORMAT
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if ret == 0 {
return true // cannot verify — trust liveness check
}
name := strings.ToLower(syscall.UTF16ToString(buf[:size]))
return strings.Contains(name, "picoclaw")
}