fix(process): reject uncatchable SIGKILL/SIGSTOP at Signal/SignalPID (#919 Cerberus)
Service.Signal and Service.SignalPID now return ErrUncatchableSignal
("signal NN cannot be caught") immediately when caller passes
syscall.SIGKILL or syscall.SIGSTOP. Prevents silent-failure where
handler is registered but never fires.
Tests cover both ID and PID rejection paths.
Closes tasks.lthn.sh/view.php?id=919
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
4f21e14c89
commit
7aea06990d
2 changed files with 61 additions and 0 deletions
29
service.go
29
service.go
|
|
@ -30,6 +30,7 @@ var (
|
|||
ErrProcessNotRunning = coreerr.E("", "process is not running", nil)
|
||||
ErrStdinNotAvailable = coreerr.E("", "stdin not available", nil)
|
||||
ErrContextRequired = coreerr.E("", "context is required", nil)
|
||||
ErrUncatchableSignal = coreerr.E("", "signal cannot be caught", nil)
|
||||
)
|
||||
|
||||
// Service manages process execution with Core IPC integration.
|
||||
|
|
@ -458,6 +459,10 @@ func (s *Service) KillPID(pid int) error {
|
|||
//
|
||||
// _ = svc.Signal("proc-1", syscall.SIGTERM)
|
||||
func (s *Service) Signal(id string, sig os.Signal) error {
|
||||
if err := validateCatchableSignals(sig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -471,6 +476,10 @@ func (s *Service) Signal(id string, sig os.Signal) error {
|
|||
//
|
||||
// _ = svc.SignalPID(1234, syscall.SIGTERM)
|
||||
func (s *Service) SignalPID(pid int, sig os.Signal) error {
|
||||
if err := validateCatchableSignals(sig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pid <= 0 {
|
||||
return ServiceError("pid must be positive", nil)
|
||||
}
|
||||
|
|
@ -491,6 +500,26 @@ func (s *Service) SignalPID(pid int, sig os.Signal) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateCatchableSignals(signals ...os.Signal) error {
|
||||
for _, sig := range signals {
|
||||
sysSig, ok := sig.(syscall.Signal)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch sysSig {
|
||||
case syscall.SIGKILL, syscall.SIGSTOP:
|
||||
return coreerr.E(
|
||||
"Service.validateCatchableSignals",
|
||||
core.Sprintf("signal %d cannot be caught", int(sysSig)),
|
||||
ErrUncatchableSignal,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a completed process from the list.
|
||||
//
|
||||
// Example:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package process
|
|||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
// Note: AX-6 — internal concurrency primitive; structural per RFC §2
|
||||
"sync"
|
||||
|
|
@ -590,6 +591,37 @@ func TestService_KillPID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestService_Signal(t *testing.T) {
|
||||
t.Run("rejects uncatchable signals", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
send func(syscall.Signal) error
|
||||
}{
|
||||
{
|
||||
name: "by id",
|
||||
send: func(sig syscall.Signal) error {
|
||||
return svc.Signal("nonexistent", sig)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "by pid",
|
||||
send: func(sig syscall.Signal) error {
|
||||
return svc.SignalPID(999999, sig)
|
||||
},
|
||||
},
|
||||
} {
|
||||
for _, sig := range []syscall.Signal{syscall.SIGKILL, syscall.SIGSTOP} {
|
||||
t.Run(tc.name+"/"+strconv.Itoa(int(sig)), func(t *testing.T) {
|
||||
err := tc.send(sig)
|
||||
requireError(t, err)
|
||||
assertErrorIs(t, err, ErrUncatchableSignal)
|
||||
assertContains(t, err.Error(), "signal "+strconv.Itoa(int(sig))+" cannot be caught")
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("signals running process by id", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue