fix(gateway): verify pid identity before stop

This commit is contained in:
Alix-007 2026-04-07 10:39:05 +08:00
parent d38d9fade1
commit e8458e169c
4 changed files with 181 additions and 14 deletions

View file

@ -12,6 +12,11 @@ import (
"github.com/sipeed/picoclaw/pkg/pid" "github.com/sipeed/picoclaw/pkg/pid"
) )
type gatewayTarget struct {
data *pid.PidFileData
process *os.Process
}
func newStatusCommand() *cobra.Command { func newStatusCommand() *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "status", Use: "status",
@ -35,30 +40,36 @@ func newStopCommand() *cobra.Command {
} }
func gatewayStatusCmd(homePath string) error { func gatewayStatusCmd(homePath string) error {
data := pid.ReadPidFileWithCheck(homePath) target, err := resolveGatewayTarget(homePath)
if data == nil { if err != nil {
return err
}
if target == nil {
fmt.Println("Gateway status: stopped") fmt.Println("Gateway status: stopped")
return nil return nil
} }
fmt.Printf( fmt.Printf(
"Gateway status: running (PID: %d, host: %s, port: %d)\n", "Gateway status: running (PID: %d, host: %s, port: %d)\n",
data.PID, target.data.PID,
data.Host, target.data.Host,
data.Port, target.data.Port,
) )
return nil return nil
} }
func gatewayStopCmd(homePath string) error { func gatewayStopCmd(homePath string) error {
data := pid.ReadPidFileWithCheck(homePath) target, err := resolveGatewayTarget(homePath)
if data == nil { if err != nil {
return err
}
if target == nil {
return fmt.Errorf("gateway is not running") return fmt.Errorf("gateway is not running")
} }
process, err := os.FindProcess(data.PID) process := target.process
if err != nil { if process == nil {
return fmt.Errorf("failed to find gateway process (PID: %d): %w", data.PID, err) return fmt.Errorf("failed to find gateway process (PID: %d)", target.data.PID)
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -67,9 +78,31 @@ func gatewayStopCmd(homePath string) error {
err = process.Signal(syscall.SIGTERM) err = process.Signal(syscall.SIGTERM)
} }
if err != nil { if err != nil {
return fmt.Errorf("failed to stop gateway (PID: %d): %w", data.PID, err) return fmt.Errorf("failed to stop gateway (PID: %d): %w", target.data.PID, err)
} }
fmt.Printf("Sent stop signal to gateway (PID: %d)\n", data.PID) fmt.Printf("Sent stop signal to gateway (PID: %d)\n", target.data.PID)
return nil return nil
} }
func resolveGatewayTarget(homePath string) (*gatewayTarget, error) {
data := pid.ReadPidFileWithCheck(homePath)
if data == nil {
return nil, nil
}
process, err := os.FindProcess(data.PID)
if err != nil {
return nil, fmt.Errorf("failed to find gateway process (PID: %d): %w", data.PID, err)
}
err = verifyGatewayProcessIdentity(data.PID)
if err != nil {
return nil, err
}
return &gatewayTarget{
data: data,
process: process,
}, nil
}

View file

@ -72,7 +72,7 @@ func TestGatewayStopCmdNotRunning(t *testing.T) {
assert.Contains(t, err.Error(), "gateway is not running") assert.Contains(t, err.Error(), "gateway is not running")
} }
func TestGatewayStopCmdRunningProcess(t *testing.T) { func TestGatewayStatusCmdRejectsNonGatewayPID(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("requires POSIX signal semantics") t.Skip("requires POSIX signal semantics")
} }
@ -89,6 +89,51 @@ func TestGatewayStopCmdRunningProcess(t *testing.T) {
writeGatewayPidFile(t, homePath, sleepCmd.Process.Pid) writeGatewayPidFile(t, homePath, sleepCmd.Process.Pid)
_, err := captureGatewayStdout(t, func() error {
return gatewayStatusCmd(homePath)
})
require.Error(t, err)
assert.Contains(t, err.Error(), "non-gateway process")
}
func TestGatewayStopCmdRejectsNonGatewayPID(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("requires POSIX signal semantics")
}
homePath := t.TempDir()
sleepCmd := exec.Command("sleep", "30")
require.NoError(t, sleepCmd.Start())
t.Cleanup(func() {
if sleepCmd.Process != nil {
_ = sleepCmd.Process.Kill()
}
_ = sleepCmd.Wait()
})
writeGatewayPidFile(t, homePath, sleepCmd.Process.Pid)
err := gatewayStopCmd(homePath)
require.Error(t, err)
assert.Contains(t, err.Error(), "non-gateway process")
}
func TestGatewayStopCmdRunningProcess(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("requires POSIX signal semantics")
}
homePath := t.TempDir()
helperCmd := startGatewayHelperProcess(t)
t.Cleanup(func() {
if helperCmd.Process != nil {
_ = helperCmd.Process.Kill()
}
})
writeGatewayPidFile(t, homePath, helperCmd.Process.Pid)
output, err := captureGatewayStdout(t, func() error { output, err := captureGatewayStdout(t, func() error {
return gatewayStopCmd(homePath) return gatewayStopCmd(homePath)
}) })
@ -98,7 +143,7 @@ func TestGatewayStopCmdRunningProcess(t *testing.T) {
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {
done <- sleepCmd.Wait() done <- helperCmd.Wait()
}() }()
select { select {
@ -110,3 +155,39 @@ func TestGatewayStopCmdRunningProcess(t *testing.T) {
t.Fatal("gateway process did not exit after stop signal") t.Fatal("gateway process did not exit after stop signal")
} }
} }
func startGatewayHelperProcess(t *testing.T) *exec.Cmd {
t.Helper()
exePath, err := os.Executable()
require.NoError(t, err)
cmd := exec.Command(
exePath,
"-test.run=TestGatewayCommandHelperProcess",
"--",
"gateway",
)
cmd.Env = append(os.Environ(), "GO_WANT_GATEWAY_HELPER_PROCESS=1")
require.NoError(t, cmd.Start())
return cmd
}
func TestGatewayCommandHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_GATEWAY_HELPER_PROCESS") != "1" {
return
}
for i, arg := range os.Args {
if arg != "--" {
continue
}
args := os.Args[i+1:]
if len(args) > 0 && args[0] == "gateway" {
select {}
}
break
}
os.Exit(2)
}

View file

@ -0,0 +1,41 @@
//go:build linux
package gateway
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strconv"
)
func verifyGatewayProcessIdentity(processID int) error {
targetExe, err := os.Readlink(filepath.Join("/proc", strconv.Itoa(processID), "exe"))
if err != nil {
return fmt.Errorf("failed to inspect gateway process executable (PID: %d): %w", processID, err)
}
currentExe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to inspect current executable: %w", err)
}
if filepath.Base(targetExe) != filepath.Base(currentExe) {
return fmt.Errorf("pid file points to a non-gateway process (PID: %d)", processID)
}
rawCmdline, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(processID), "cmdline"))
if err != nil {
return fmt.Errorf("failed to inspect gateway process command line (PID: %d): %w", processID, err)
}
argv := bytes.Split(rawCmdline, []byte{0})
for _, arg := range argv[1:] {
if string(arg) == "gateway" || string(arg) == "g" {
return nil
}
}
return fmt.Errorf("pid file points to a non-gateway process (PID: %d)", processID)
}

View file

@ -0,0 +1,12 @@
//go:build !linux
package gateway
import "fmt"
func verifyGatewayProcessIdentity(processID int) error {
return fmt.Errorf(
"gateway process identity verification is not supported on this platform (PID: %d)",
processID,
)
}