mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 17:57:54 -10:00
* feat(sandbox): unify command execution and enforcement * test(sandbox): make platform contracts portable * fix(sandbox): close execution lifecycle races Bound retained interactive output, preserve interruption state, and start workspace observation before process launch. Protect active runtime roots with cross-platform leases, capture transient protected-path creation events, and restore process attributes after PTY fallback. * fix(sandbox): restore strict macOS isolation Remove Seatbelt host-local network allowances after native validation showed localhost filters can reach host interfaces. Track process reap completion so retained sessions cannot signal a stale numeric PID.
101 lines
2.3 KiB
Go
101 lines
2.3 KiB
Go
//go:build !windows
|
|
|
|
package execution
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// ConfigureProcessGroup makes cmd the leader of a process group so lifecycle
|
|
// operations cover descendants instead of orphaning them.
|
|
func ConfigureProcessGroup(cmd *exec.Cmd) {
|
|
if cmd == nil {
|
|
return
|
|
}
|
|
if cmd.SysProcAttr == nil {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
cmd.SysProcAttr.Setpgid = true
|
|
}
|
|
|
|
// KillProcessTree immediately kills pid and, when it is a group leader, its
|
|
// descendant process group.
|
|
func KillProcessTree(pid int) error {
|
|
target, err := processSignalTarget(pid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := syscall.Kill(target, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TerminateProcessTree requests graceful termination, then force-kills the
|
|
// process tree after grace. Callers retain their distinct persistence models;
|
|
// this function owns only the OS lifecycle primitive.
|
|
func TerminateProcessTree(pid int, grace, poll time.Duration) error {
|
|
target, err := processSignalTarget(pid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
alive := func() bool { return syscall.Kill(target, syscall.Signal(0)) == nil }
|
|
if err := syscall.Kill(target, syscall.SIGTERM); err != nil {
|
|
if errors.Is(err, syscall.ESRCH) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
if poll <= 0 {
|
|
poll = 50 * time.Millisecond
|
|
}
|
|
if grace < 0 {
|
|
grace = 0
|
|
}
|
|
deadline := time.Now().Add(grace)
|
|
for time.Now().Before(deadline) {
|
|
if !alive() {
|
|
return nil
|
|
}
|
|
time.Sleep(poll)
|
|
}
|
|
if !alive() {
|
|
return nil
|
|
}
|
|
if err := syscall.Kill(target, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
|
|
return err
|
|
}
|
|
deadline = time.Now().Add(grace)
|
|
for time.Now().Before(deadline) {
|
|
if !alive() {
|
|
return nil
|
|
}
|
|
time.Sleep(poll)
|
|
}
|
|
if alive() {
|
|
return fmt.Errorf("process %d did not exit after SIGKILL", pid)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func processSignalTarget(pid int) (int, error) {
|
|
if pid <= 1 {
|
|
return 0, fmt.Errorf("refusing to signal invalid pid %d", pid)
|
|
}
|
|
target := pid
|
|
if pgid, err := syscall.Getpgid(pid); err == nil {
|
|
if pgid == pid {
|
|
target = -pid
|
|
}
|
|
} else if errors.Is(err, syscall.ESRCH) {
|
|
// Preserve the individual target; the signal call below treats ESRCH as
|
|
// already gone, which is a successful lifecycle outcome.
|
|
return pid, nil
|
|
}
|
|
return target, nil
|
|
}
|