diff --git a/cmd/shelley/main.go b/cmd/shelley/main.go index 34c17cb6ac9e31cb3335a6747de1b372f9128c83..ce42c7f81e73284c11cf9e553219bd5b9986c2d2 100644 --- a/cmd/shelley/main.go +++ b/cmd/shelley/main.go @@ -56,13 +56,6 @@ func main() { flag.Parse() args := flag.Args() - // Apply seccomp filter early, before spawning any child processes. - // This prevents child processes from killing shelley. - // Turns out this doesn't work, because it blocks sudo, which we want to work. - // if err := seccomp.BlockKillSelf(); err != nil { - // slog.Info("seccomp filter not installed", "error", err) - // } - if len(args) == 0 { flag.Usage() os.Exit(1) diff --git a/cmd/shelley/seccomp_test.go b/cmd/shelley/seccomp_test.go deleted file mode 100644 index b58d9f818287dfde76a94259106da21a9c550a62..0000000000000000000000000000000000000000 --- a/cmd/shelley/seccomp_test.go +++ /dev/null @@ -1,108 +0,0 @@ -//go:build linux - -package main - -import ( - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "testing" - - "shelley.exe.dev/seccomp" -) - -// TestSeccompIntegration tests that the seccomp filter is installed -// automatically and prevents child processes from killing the parent. -func TestSeccompIntegration(t *testing.T) { - if os.Getenv("TEST_SECCOMP_HELPER") == "1" { - runSeccompHelper(t) - return - } - - // Re-exec this test in a subprocess - cmd := exec.Command(os.Args[0], "-test.run=TestSeccompIntegration$", "-test.v") - cmd.Env = append(os.Environ(), "TEST_SECCOMP_HELPER=1") - output, err := cmd.CombinedOutput() - t.Logf("Helper output:\n%s", output) - if err != nil { - t.Fatalf("Helper failed: %v", err) - } -} - -func runSeccompHelper(t *testing.T) { - pid := os.Getpid() - t.Logf("Helper PID: %d", pid) - - // Install seccomp filter (same as -seccomp flag does in main) - if err := seccomp.BlockKillSelf(); err != nil { - t.Fatalf("BlockKillSelf failed: %v", err) - } - t.Log("Seccomp filter installed") - - // Spawn a child that tries to kill us - script := fmt.Sprintf("kill -TERM %d 2>&1; echo exit=$?", pid) - cmd := exec.Command("sh", "-c", script) - output, _ := cmd.CombinedOutput() - t.Logf("Kill attempt output: %s", output) - - // Verify the kill was blocked (output should contain "Operation not permitted" or exit=1) - outStr := string(output) - if !strings.Contains(outStr, "Operation not permitted") && !strings.Contains(outStr, "exit=1") { - t.Fatalf("Expected kill to fail with Operation not permitted, got: %s", outStr) - } - - t.Log("SUCCESS: Child's kill attempt was blocked") -} - -// TestSeccompPreservesKillOthers verifies that with seccomp enabled, -// we can still kill other processes (not ourselves). -func TestSeccompPreservesKillOthers(t *testing.T) { - if os.Getenv("TEST_SECCOMP_KILL_OTHERS") == "1" { - runSeccompKillOthersHelper(t) - return - } - - // Re-exec this test in a subprocess - cmd := exec.Command(os.Args[0], "-test.run=TestSeccompPreservesKillOthers$", "-test.v") - cmd.Env = append(os.Environ(), "TEST_SECCOMP_KILL_OTHERS=1") - output, err := cmd.CombinedOutput() - t.Logf("Helper output:\n%s", output) - if err != nil { - t.Fatalf("Helper failed: %v", err) - } -} - -func runSeccompKillOthersHelper(t *testing.T) { - // Install seccomp filter - if err := seccomp.BlockKillSelf(); err != nil { - t.Fatalf("BlockKillSelf failed: %v", err) - } - t.Log("Seccomp filter installed") - - // Start a sleep process - sleepCmd := exec.Command("sleep", "60") - if err := sleepCmd.Start(); err != nil { - t.Fatalf("Failed to start sleep: %v", err) - } - sleepPid := sleepCmd.Process.Pid - t.Logf("Started sleep process with PID %d", sleepPid) - - // Kill the sleep process via a child shell - this should work - script := fmt.Sprintf("kill -TERM %d 2>&1; echo exit=$?", sleepPid) - cmd := exec.Command("sh", "-c", script) - output, _ := cmd.CombinedOutput() - t.Logf("Kill output: %s", output) - - // Verify the sleep process was killed (exit=0) - if !strings.Contains(string(output), "exit=0") { - t.Fatalf("Expected kill to succeed, got: %s", output) - } - - sleepCmd.Wait() - t.Log("SUCCESS: Killing other processes still works") -} - -// Silence unused import warning -var _ = strconv.Itoa diff --git a/seccomp/arch_linux_amd64.go b/seccomp/arch_linux_amd64.go deleted file mode 100644 index ea08c62930a85cf491d2bac83b738c526e339eb7..0000000000000000000000000000000000000000 --- a/seccomp/arch_linux_amd64.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build linux && amd64 - -package seccomp - -import "golang.org/x/sys/unix" - -const ( - auditArch = unix.AUDIT_ARCH_X86_64 - sysKill = 62 - sysTkill = 200 - sysTgkill = 234 - sysPidfdSendSignal = 424 -) diff --git a/seccomp/arch_linux_arm64.go b/seccomp/arch_linux_arm64.go deleted file mode 100644 index 27b3240cc67b30a57da9c6472259d3880b984f44..0000000000000000000000000000000000000000 --- a/seccomp/arch_linux_arm64.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build linux && arm64 - -package seccomp - -import "golang.org/x/sys/unix" - -const ( - auditArch = unix.AUDIT_ARCH_AARCH64 - sysKill = 129 - sysTkill = 130 - sysTgkill = 131 - sysPidfdSendSignal = 424 -) diff --git a/seccomp/seccomp_linux.go b/seccomp/seccomp_linux.go deleted file mode 100644 index 6d8214b2d7a9bb893e76711039ab0e7d28bf62ca..0000000000000000000000000000000000000000 --- a/seccomp/seccomp_linux.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build linux - -// Package seccomp provides a seccomp filter to prevent child processes -// from killing the parent process. -// -// Note: We use raw BPF instead of github.com/seccomp/libseccomp-golang -// because that library requires cgo and links against libseccomp. -// This pure-Go implementation avoids the cgo dependency. -package seccomp - -import ( - "fmt" - "os" - "unsafe" - - "golang.org/x/sys/unix" -) - -// BPF instruction constants -const ( - bpfLD = 0x00 - bpfW = 0x00 - bpfABS = 0x20 - bpfJMP = 0x05 - bpfJEQ = 0x10 - bpfRET = 0x06 - bpfK = 0x00 -) - -// seccomp_data offsets -const ( - offsetNr = 0 // syscall number (int, 4 bytes) - offsetArch = 4 // architecture (u32, 4 bytes) - offsetArgs = 16 // args[0] starts at offset 16 (u64 each) -) - -// bpfStmt creates a BPF statement (no jump targets) -func bpfStmt(code uint16, k uint32) unix.SockFilter { - return unix.SockFilter{Code: code, Jt: 0, Jf: 0, K: k} -} - -// bpfJump creates a BPF jump instruction -func bpfJump(code uint16, k uint32, jt, jf uint8) unix.SockFilter { - return unix.SockFilter{Code: code, Jt: jt, Jf: jf, K: k} -} - -// BlockKillSelf installs a seccomp filter that prevents any process from -// sending signals to the current process via kill(2) and related syscalls -// (tkill, tgkill). -// This must be called before spawning child processes. -// The filter is inherited by child processes. -// -// The filter is installed with SECCOMP_FILTER_FLAG_TSYNC to synchronize -// across all threads in the process, ensuring child processes spawned -// from any goroutine will inherit the filter. -func BlockKillSelf() error { - pid := uint32(os.Getpid()) - // Negative PID in two's complement (for blocking kill(-pid, sig) which - // sends signals to the process group) - negPid := uint32(-int32(pid)) - - // Build BPF filter program that blocks kill/tkill/tgkill - // when arg0 (target pid) matches our pid or -pid. - // - // The filter structure: - // 1. Load and check architecture - // 2. Load syscall number - // 3. Check if it's one of the signal-sending syscalls - // 4. If so, check if arg0 == our pid OR arg0 == -our pid - // 5. If targeting us, return EPERM; otherwise allow - filter := []unix.SockFilter{ - // [0] Load architecture - bpfStmt(bpfLD|bpfW|bpfABS, offsetArch), - // [1] If not our arch, jump to allow (end of filter) - bpfJump(bpfJMP|bpfJEQ|bpfK, auditArch, 0, 12), // skip to ALLOW at [14] - - // [2] Load syscall number - bpfStmt(bpfLD|bpfW|bpfABS, offsetNr), - - // [3] Check for kill - bpfJump(bpfJMP|bpfJEQ|bpfK, sysKill, 4, 0), // match -> check pid at [8] - // [4] Check for tkill - bpfJump(bpfJMP|bpfJEQ|bpfK, sysTkill, 3, 0), // match -> check pid at [8] - // [5] Check for tgkill (arg0 is tgid, arg2 is tid - we check arg0) - bpfJump(bpfJMP|bpfJEQ|bpfK, sysTgkill, 2, 0), // match -> check pid at [8] - - // [6-7] Jump to allow for non-matching syscalls - bpfJump(bpfJMP|bpfJEQ|bpfK, 0xFFFFFFFF, 0, 7), // never matches, always jumps to ALLOW at [14] - bpfStmt(bpfRET|bpfK, unix.SECCOMP_RET_ALLOW), // [7] unreachable filler - - // [8] Load first argument (target PID) - lower 32 bits - bpfStmt(bpfLD|bpfW|bpfABS, offsetArgs), - // [9] Check if target PID matches our PID (positive) - bpfJump(bpfJMP|bpfJEQ|bpfK, pid, 3, 0), // if our pid, jump to EPERM at [13] - // [10] Check if target PID matches -our PID (for process group kills) - bpfJump(bpfJMP|bpfJEQ|bpfK, negPid, 2, 0), // if -our pid, jump to EPERM at [13] - - // [11] Not targeting us, allow - bpfStmt(bpfRET|bpfK, unix.SECCOMP_RET_ALLOW), - - // [12] Unreachable filler - bpfStmt(bpfRET|bpfK, unix.SECCOMP_RET_ALLOW), - - // [13] Return EPERM for signal syscalls targeting our process - bpfStmt(bpfRET|bpfK, unix.SECCOMP_RET_ERRNO|uint32(unix.EPERM)), - - // [14] Allow the syscall - bpfStmt(bpfRET|bpfK, unix.SECCOMP_RET_ALLOW), - } - - // Set NO_NEW_PRIVS to allow unprivileged seccomp - if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil { - return fmt.Errorf("prctl(PR_SET_NO_NEW_PRIVS): %w", err) - } - - // Install the seccomp filter - prog := unix.SockFprog{ - Len: uint16(len(filter)), - Filter: &filter[0], - } - - // Use seccomp() syscall with SECCOMP_FILTER_FLAG_TSYNC to apply the filter - // to all threads in the process. This ensures that child processes spawned - // from any goroutine (which may run on different OS threads) will inherit - // the filter. - _, _, errno := unix.Syscall(unix.SYS_SECCOMP, unix.SECCOMP_SET_MODE_FILTER, unix.SECCOMP_FILTER_FLAG_TSYNC, uintptr(unsafe.Pointer(&prog))) - if errno != 0 { - return fmt.Errorf("seccomp(SECCOMP_SET_MODE_FILTER, TSYNC): %w", errno) - } - - return nil -} diff --git a/seccomp/seccomp_linux_test.go b/seccomp/seccomp_linux_test.go deleted file mode 100644 index 6700718b20fb4a50bc90eb75fb8fe186aaf2e0f1..0000000000000000000000000000000000000000 --- a/seccomp/seccomp_linux_test.go +++ /dev/null @@ -1,180 +0,0 @@ -//go:build linux - -package seccomp - -import ( - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "syscall" - "testing" -) - -func TestBlockKillSelf(t *testing.T) { - // This test must run in a subprocess because seccomp filters are inherited - // by child processes and cannot be removed once installed. - if os.Getenv("TEST_SECCOMP_SUBPROCESS") == "1" { - runSeccompTestSubprocess(t) - return - } - - // Re-exec this test in a subprocess - cmd := exec.Command(os.Args[0], "-test.run=TestBlockKillSelf$", "-test.v") - cmd.Env = append(os.Environ(), "TEST_SECCOMP_SUBPROCESS=1") - output, err := cmd.CombinedOutput() - t.Logf("Subprocess output:\n%s", output) - if err != nil { - t.Fatalf("Subprocess failed: %v", err) - } -} - -func runSeccompTestSubprocess(t *testing.T) { - pid := os.Getpid() - t.Logf("Running seccomp test in subprocess with PID %d", pid) - - // Install the seccomp filter - if err := BlockKillSelf(); err != nil { - t.Fatalf("BlockKillSelf failed: %v", err) - } - t.Log("Seccomp filter installed") - - // Now spawn a child process that tries to kill us - // We use a shell command because we need a separate process - cmd := exec.Command("sh", "-c", "kill -TERM "+strconv.Itoa(pid)+" 2>&1; echo exit_code=$?") - output, _ := cmd.CombinedOutput() - t.Logf("Kill attempt output: %s", output) - - // The kill should have failed with EPERM - // If we're still alive, the seccomp filter worked! - t.Log("We survived the kill attempt!") - - // Also verify we can still kill other things (like a sleep process) - sleepCmd := exec.Command("sleep", "60") - if err := sleepCmd.Start(); err != nil { - t.Fatalf("Failed to start sleep: %v", err) - } - sleepPid := sleepCmd.Process.Pid - - // Kill the sleep process - this should work - if err := syscall.Kill(sleepPid, syscall.SIGTERM); err != nil { - t.Errorf("Failed to kill sleep process: %v", err) - } - sleepCmd.Wait() - t.Logf("Successfully killed sleep process %d", sleepPid) - - // Try to kill ourselves directly - this should fail - err := syscall.Kill(pid, syscall.SIGTERM) - if err == nil { - t.Fatal("Expected kill of self to fail, but it succeeded") - } - if err != syscall.EPERM { - t.Fatalf("Expected EPERM, got %v", err) - } - t.Logf("Kill of self correctly returned EPERM") - - // Try to kill using negative PID (process group kill) - this should also fail - err = syscall.Kill(-pid, syscall.SIGTERM) - if err == nil { - t.Fatal("Expected kill of -self to fail, but it succeeded") - } - if err != syscall.EPERM { - t.Fatalf("Expected EPERM for negative PID, got %v", err) - } - t.Logf("Kill of -self correctly returned EPERM") -} - -func TestBlockKillSelf_ChildCannotKillParent(t *testing.T) { - // This is the main test: verify that after installing seccomp, - // a child process cannot kill the parent (shelley) process. - if os.Getenv("TEST_SECCOMP_CHILD_SUBPROCESS") == "1" { - runChildCannotKillParentSubprocess(t) - return - } - - // Re-exec this test in a subprocess - cmd := exec.Command(os.Args[0], "-test.run=TestBlockKillSelf_ChildCannotKillParent$", "-test.v") - cmd.Env = append(os.Environ(), "TEST_SECCOMP_CHILD_SUBPROCESS=1") - output, err := cmd.CombinedOutput() - t.Logf("Subprocess output:\n%s", output) - if err != nil { - t.Fatalf("Subprocess failed: %v", err) - } -} - -func runChildCannotKillParentSubprocess(t *testing.T) { - pid := os.Getpid() - t.Logf("Parent process PID: %d", pid) - - // Install the seccomp filter BEFORE spawning children - if err := BlockKillSelf(); err != nil { - t.Fatalf("BlockKillSelf failed: %v", err) - } - t.Log("Seccomp filter installed in parent") - - // Spawn a child process that tries to kill the parent using positive PID - // The child inherits the seccomp filter, which blocks kill(parent_pid, ...) - script := fmt.Sprintf(` -echo "Child attempting to kill parent PID %d" -kill -TERM %d 2>&1 -result=$? -echo "kill exit code: $result" -if [ $result -ne 0 ]; then - echo "SUCCESS: kill was blocked" - exit 0 -else - echo "FAILURE: kill succeeded (parent should be dead)" - exit 1 -fi -`, pid, pid) - - cmd := exec.Command("sh", "-c", script) - output, err := cmd.CombinedOutput() - t.Logf("Child output (positive PID):\n%s", output) - - // Check that the child reported success (kill was blocked) - if err != nil { - t.Fatalf("Child process reported failure (positive PID): %v", err) - } - - // Verify the output contains our success message - if !strings.Contains(string(output), "SUCCESS: kill was blocked") { - t.Fatalf("Expected success message in output (positive PID)") - } - - // We're still alive! - t.Logf("Parent (PID %d) survived child's positive PID kill attempt", pid) - - // Now test with negative PID (process group kill) - negScript := fmt.Sprintf(` -echo "Child attempting to kill parent process group with PID -%d" -kill -TERM -%d 2>&1 -result=$? -echo "kill exit code: $result" -if [ $result -ne 0 ]; then - echo "SUCCESS: kill -pid was blocked" - exit 0 -else - echo "FAILURE: kill -pid succeeded (parent should be dead)" - exit 1 -fi -`, pid, pid) - - negCmd := exec.Command("sh", "-c", negScript) - negOutput, negErr := negCmd.CombinedOutput() - t.Logf("Child output (negative PID):\n%s", negOutput) - - // Check that the child reported success (kill was blocked) - if negErr != nil { - t.Fatalf("Child process reported failure (negative PID): %v", negErr) - } - - // Verify the output contains our success message - if !strings.Contains(string(negOutput), "SUCCESS: kill -pid was blocked") { - t.Fatalf("Expected success message in output (negative PID)") - } - - // We're still alive! - t.Logf("Parent (PID %d) survived child's negative PID kill attempt", pid) -} diff --git a/seccomp/seccomp_other.go b/seccomp/seccomp_other.go deleted file mode 100644 index a3a313da5cf34dec4070e4cc8e5c80132b1e21a8..0000000000000000000000000000000000000000 --- a/seccomp/seccomp_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !linux - -package seccomp - -// BlockKillSelf is a no-op on non-Linux systems. -// Seccomp is a Linux-specific feature. -func BlockKillSelf() error { - return nil -}