Files
CyberStrikeAI/internal/processguard/guard_test.go
T

161 lines
4.1 KiB
Go

//go:build !windows
package processguard
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"testing"
"time"
)
func testID() string {
return fmt.Sprintf("%08x-1111-4111-8111-%012x", os.Getpid(), uint64(time.Now().UnixNano())&0xffffffffffff)
}
func testOptions() Options {
return Options{CgroupRoot: os.Getenv("CSAI_TEST_CGROUP_ROOT"), MaxProcesses: 64, MemoryMaxBytes: 256 << 20}
}
func closeTestGroup(t *testing.T, g Group) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := g.Close(ctx); err != nil {
t.Error(err)
}
}
func startTestCommand(g Group, command string) (*exec.Cmd, error) {
cmd := exec.Command("sh", "-c", command)
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
launch, err := g.Prepare(cmd)
if err != nil {
return nil, err
}
defer launch.Dispose()
if err = cmd.Start(); err != nil {
return nil, err
}
if err = launch.Commit(); err != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return nil, err
}
return cmd, nil
}
func readPID(t *testing.T, path string) int {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
b, err := os.ReadFile(path)
if err == nil {
pid, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err == nil && pid > 0 {
return pid
}
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("no PID written to %s", path)
return 0
}
func waitGone(t *testing.T, pid int) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if syscall.Kill(pid, 0) == syscall.ESRCH {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("PID %d survived cleanup", pid)
}
func TestGuardianReapsAfterOwnerSIGKILL(t *testing.T) {
pidPath := filepath.Join(t.TempDir(), "pid")
owner := exec.Command(os.Args[0], "-test.run=^TestGuardianOwnerHelper$")
owner.Env = append(os.Environ(), "CSAI_GUARD_TEST_OWNER=1", "CSAI_GUARD_TEST_PID="+pidPath)
if err := owner.Start(); err != nil {
t.Fatal(err)
}
defer func() { _ = owner.Process.Kill(); _ = owner.Wait() }()
pid := readPID(t, pidPath)
if err := owner.Process.Kill(); err != nil {
t.Fatal(err)
}
_ = owner.Wait()
waitGone(t, pid)
}
func TestGuardianOwnerHelper(t *testing.T) {
if os.Getenv("CSAI_GUARD_TEST_OWNER") != "1" {
t.Skip("subprocess helper")
}
g, err := NewWithOptions(testID(), testOptions())
if err != nil {
t.Fatal(err)
}
command := fmt.Sprintf("echo $$ > %q; exec sleep 300", os.Getenv("CSAI_GUARD_TEST_PID"))
cmd, err := startTestCommand(g, command)
if err != nil {
t.Fatal(err)
}
go cmd.Wait()
select {}
}
func TestGroupCloseAndAdmission(t *testing.T) {
g, err := NewWithOptions(testID(), testOptions())
if err != nil {
t.Fatal(err)
}
defer closeTestGroup(t, g)
cmd, err := startTestCommand(g, "exec sleep 300")
if err != nil {
t.Fatal(err)
}
reaped := make(chan struct{})
go func() { _ = cmd.Wait(); close(reaped) }()
closeTestGroup(t, g)
<-reaped
waitGone(t, cmd.Process.Pid)
if _, err = g.Prepare(exec.Command("sh", "-c", "true")); err == nil {
t.Fatal("closed containment accepted a command")
}
}
func TestLaunchGateOwnerDisappearsBeforeCommit(t *testing.T) {
if runtime.GOOS == "linux" && testOptions().CgroupRoot != "" {
t.Skip("cgroup assignment is atomic without a gate")
}
g, err := NewWithOptions(testID(), Options{})
if err != nil {
t.Fatal(err)
}
defer closeTestGroup(t, g)
file := filepath.Join(t.TempDir(), "should-not-exist")
cmd := exec.Command("sh", "-c", fmt.Sprintf("echo escaped > %q", file))
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
launch, err := g.Prepare(cmd)
if err != nil {
t.Fatal(err)
}
if err = cmd.Start(); err != nil {
t.Fatal(err)
}
launch.Dispose() // simulate owner crashing before watchdog registration
_ = cmd.Wait()
if _, err = os.Stat(file); !os.IsNotExist(err) {
t.Fatal("unregistered child executed user code")
}
}
func TestRequiredIsolationFailsClosed(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows has Job Objects")
}
if _, err := NewWithOptions(testID(), Options{Mode: "required"}); err == nil {
t.Fatal("required isolation silently downgraded")
}
}