fix(gateway): verify pid identity before stop
This commit is contained in:
parent
d38d9fade1
commit
e8458e169c
4 changed files with 181 additions and 14 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
41
cmd/picoclaw/internal/gateway/process_identity_linux.go
Normal file
41
cmd/picoclaw/internal/gateway/process_identity_linux.go
Normal 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)
|
||||||
|
}
|
||||||
12
cmd/picoclaw/internal/gateway/process_identity_other.go
Normal file
12
cmd/picoclaw/internal/gateway/process_identity_other.go
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue