diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-10-01 22:34:51 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-10-02 18:10:05 +0900 |
| commit | 8ddc826bd02ff0d58123e1af5de2f48ae0dbc7c9 (patch) | |
| tree | 74d13485a177c442154a4c343062efa128dc1ab2 /test/sandbox | |
| parent | 4b19686109b0962ea68dfe58feafffe3bac9c202 (diff) | |
test/sandbox: migrate tests
This benefits even more than the cmd/sharefs test suite, the slow
python-based test script was a major bottleneck. Replacing the
nix-represented test cases with compound literals also significantly
increases readability.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'test/sandbox')
| -rw-r--r-- | test/sandbox/main.go | 83 | ||||
| -rw-r--r-- | test/sandbox/ptrace.go | 153 | ||||
| -rw-r--r-- | test/sandbox/seccomp.patch | 18 | ||||
| -rw-r--r-- | test/sandbox/sum_amd64.go | 7 |
4 files changed, 261 insertions, 0 deletions
diff --git a/test/sandbox/main.go b/test/sandbox/main.go new file mode 100644 index 00000000..fd129577 --- /dev/null +++ b/test/sandbox/main.go @@ -0,0 +1,83 @@ +//go:build testsuite + +// The sandbox test program runs cmd/hakurei with configurations simulating +// several common workloads and inspects the resulting container states. +package main + +import ( + "context" + "log" + "slices" + "sync" + "syscall" + + "hakurei.app/hst" + "hakurei.app/test/internal/testsuite" +) + +func main() { + go testsuite.ReceiveSignals() + username := testsuite.GetUsername() + + // the signal handler does not wait for termination + ctx := context.Background() + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Go(func() { + log.Println("checking pd seccomp outcome") + c, cancel := context.WithCancel(ctx) + defer cancel() + + _, done := testsuite.MustStartAs( + c, username, nil, + "hakurei", "exec", "sleep", "infinity", + ) + + var ( + s testsuite.StatScanner + + stat syscall.Stat_t + ) + for s.Scan() { + select { + case err := <-done: + if err == nil { + log.Fatal("test process terminated unexpectedly") + } + log.Fatal(err) + default: + break + } + + if s.Stat().Comm != "sleep" { + continue + } + + if args, err := s.Stat().Args(); err != nil { + log.Fatal(err) + } else if !slices.Equal(args, []string{"sleep", "infinity"}) { + continue + } + + if err := s.Stat().Stat(&stat); err != nil { + log.Fatal(err) + } + + id := hst.ToUser[uint32](0, 0) + if stat.Uid != id || stat.Gid != id { + continue + } + + break + } + if err := s.Err(); err != nil { + log.Fatal(err) + } + mustCheckFilter(s.Stat().PID, pdSum) + if err := <-done; err != nil { + log.Fatal(err) + } + }) +} diff --git a/test/sandbox/ptrace.go b/test/sandbox/ptrace.go new file mode 100644 index 00000000..2647b19a --- /dev/null +++ b/test/sandbox/ptrace.go @@ -0,0 +1,153 @@ +//go:build testsuite + +package main + +import ( + "crypto/sha512" + "encoding/hex" + "errors" + "fmt" + "log" + "os" + "syscall" + "unsafe" +) + +const ( + // PTRACE_ATTACH attaches to the process specified in pid. + PTRACE_ATTACH = 16 + // PTRACE_DETACH restarts the stopped tracee as for PTRACE_CONT, but first + // detaches from it. + PTRACE_DETACH = 17 + + // PTRACE_SECCOMP_GET_FILTER allows the tracer to dump the tracee's classic + // BPF filters. + PTRACE_SECCOMP_GET_FILTER = 0x420c +) + +// ptrace wraps the ptrace syscall. +func ptrace( + op uintptr, + pid, addr int, + data unsafe.Pointer, +) (r uintptr, errno syscall.Errno) { + r, _, errno = syscall.Syscall6( + syscall.SYS_PTRACE, + op, + uintptr(pid), + uintptr(addr), + uintptr(data), + 0, 0, + ) + return +} + +// ptraceAttach attaches to the process referred to by pid. +func ptraceAttach(pid int) error { + if _, errno := ptrace(PTRACE_ATTACH, pid, 0, nil); errno != 0 { + return os.NewSyscallError("PTRACE_ATTACH", errno) + } + + var status syscall.WaitStatus + for { + if _, err := syscall.Wait4( + pid, + &status, + syscall.WALL, + nil, + ); err != nil { + if errors.Is(err, syscall.EINTR) { + continue + } + return os.NewSyscallError("wait4", err) + } + break + } + + return nil +} + +// ptraceDetach detaches from the attached process referred to by pid. +func ptraceDetach(pid int) error { + if _, errno := ptrace(PTRACE_DETACH, pid, 0, nil); errno != 0 { + return os.NewSyscallError("PTRACE_DETACH", errno) + } + return nil +} + +// getFilter dumps the specified tracee's cBPF filter at the specified index +// and returns the resulting payload. T must be eight bytes long and must not +// contain pointers. +func getFilter[T comparable](pid, index int) ([]T, error) { + if s := unsafe.Sizeof(*new(T)); s != 8 { + return nil, fmt.Errorf("invalid filter block size %d", s) + } + + var buf []T + if n, errno := ptrace( + PTRACE_SECCOMP_GET_FILTER, + pid, index, nil, + ); errno != 0 { + return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno) + } else { + buf = make([]T, n) + } + if _, errno := ptrace( + PTRACE_SECCOMP_GET_FILTER, + pid, index, unsafe.Pointer(&buf[0]), + ); errno != 0 { + return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno) + } + return buf, nil +} + +// checkFilter checks the process at pid to have its first filter's contents +// match the specified sha512 checksum in hexadecimal string representation. +func checkFilter(pid int, sum string) (err error) { + if err = ptraceAttach(pid); err != nil { + return + } + defer func() { + if detachErr := ptraceDetach(pid); err == nil { + err = detachErr + } + }() + + var ( + buf [][8]byte + want []byte + ) + + if want, err = hex.DecodeString(sum); err != nil { + return + } + + h := sha512.New() + if buf, err = getFilter[[8]byte](pid, 0); err != nil { + return err + } else { + for _, w := range buf { + h.Write(w[:]) + } + } + + if got := h.Sum(nil); string(got) != string(want) { + return fmt.Errorf( + "bad filter\n\t got: %s\n\twant: %s", + hex.EncodeToString(got), + hex.EncodeToString(want), + ) + } + return +} + +// mustCheckFilter is like checkFilter, but terminates the test suite if a +// non-nil error is returned. Otherwise, the tracee is interrupted after it +// resumes. +func mustCheckFilter(pid int, sum string) { + if err := checkFilter(pid, sum); err != nil { + log.Fatal(err) + } else if err = syscall.Kill(pid, syscall.SIGINT); err != nil { + log.Fatalf("cannot terminate tracee: %v", err) + } +} diff --git a/test/sandbox/seccomp.patch b/test/sandbox/seccomp.patch new file mode 100644 index 00000000..ddabc71e --- /dev/null +++ b/test/sandbox/seccomp.patch @@ -0,0 +1,18 @@ +diff --git a/kernel/seccomp.c b/kernel/seccomp.c +index 25f62867a16d..7b63ccc8daf4 100644 +--- a/kernel/seccomp.c ++++ b/kernel/seccomp.c +@@ -2216,8 +2216,12 @@ long seccomp_get_filter(struct task_struct *task, unsigned long filter_off, + struct seccomp_filter *filter; + struct sock_fprog_kern *fprog; + long ret; ++ struct user_namespace *user_ns = current_user_ns(); + +- if (!capable(CAP_SYS_ADMIN) || ++ if (in_userns(user_ns, task_cred_xxx(task, user_ns))) { ++ if (!ns_capable(user_ns, CAP_SYS_ADMIN)) ++ return -EACCES; ++ } else if (!capable(CAP_SYS_ADMIN) || + current->seccomp.mode != SECCOMP_MODE_DISABLED) { + return -EACCES; + } diff --git a/test/sandbox/sum_amd64.go b/test/sandbox/sum_amd64.go new file mode 100644 index 00000000..da03f12b --- /dev/null +++ b/test/sandbox/sum_amd64.go @@ -0,0 +1,7 @@ +//go:build testsuite + +package main + +const ( + pdSum = "c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a" +) |
