aboutsummaryrefslogtreecommitdiffhomepage
path: root/test/sandbox
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-10-01 22:34:51 +0900
committerOphestra <cat@gensokyo.uk>2026-10-02 18:10:05 +0900
commit8ddc826bd02ff0d58123e1af5de2f48ae0dbc7c9 (patch)
tree74d13485a177c442154a4c343062efa128dc1ab2 /test/sandbox
parent4b19686109b0962ea68dfe58feafffe3bac9c202 (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.go83
-rw-r--r--test/sandbox/ptrace.go153
-rw-r--r--test/sandbox/seccomp.patch18
-rw-r--r--test/sandbox/sum_amd64.go7
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"
+)