diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-10-04 01:05:10 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-10-06 19:41:06 +0900 |
| commit | a7383510fb05abc98b992240cb76ad4b6c598956 (patch) | |
| tree | 90881e9d6952e050128f1378d66452c7d733d012 /test/sandbox/main.go | |
| parent | b452e1047ccd3e1826da16416430e6638270155b (diff) | |
test/sandbox: migrate tests
This significantly improves performance, removing overhead of nix,
python, and virtualisation. Running this in an unprivileged container
required patching the kernel, but since special runner setup was already
needed, that was an acceptable tradeoff.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'test/sandbox/main.go')
| -rw-r--r-- | test/sandbox/main.go | 410 |
1 files changed, 410 insertions, 0 deletions
diff --git a/test/sandbox/main.go b/test/sandbox/main.go new file mode 100644 index 00000000..311b9f58 --- /dev/null +++ b/test/sandbox/main.go @@ -0,0 +1,410 @@ +//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 ( + "bytes" + "context" + "encoding/json" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "syscall" + + "hakurei.app/check" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/store" + + "hakurei.app/test/internal/testsuite" + "hakurei.app/test/sandbox/testdata" +) + +// mustScanFor continuously scans the proc filesystem and calls f for each entry +// visited. +func mustScanFor(f func(ps *testsuite.StatScanner) bool) int { + var ps testsuite.StatScanner + + for ps.Scan() { + if f(&ps) { + break + } + } + if err := ps.Err(); err != nil { + log.Fatal(err) + } + return ps.Stat().PID +} + +// mustStart starts a hakurei container and returns the pid of a process within +// the container. This process must be terminated by the caller. +func mustStart( + ctx context.Context, + serial uint64, + username string, + files ...*os.File, +) (pid int, done <-chan error) { + _serial := strconv.FormatUint(serial, 10) + _, done = testsuite.MustStartAs( + ctx, username, files, + "hakurei", "exec", + "sleep", "infinity", _serial, + ) + + var stat syscall.Stat_t + pid = mustScanFor(func(s *testsuite.StatScanner) bool { + select { + case err := <-done: + if err == nil { + log.Fatal("test process terminated unexpectedly") + } + log.Fatal(err) + default: + break + } + + if s.Stat().Comm != "sleep" { + return false + } + + if args, err := s.Stat().Args(); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } else if !slices.Equal(args, []string{ + "sleep", + "infinity", + _serial, + }) { + return false + } + + if err := s.Stat().Stat(&stat); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } + + id := hst.ToUser[uint32](0, 0) + if stat.Uid != id || stat.Gid != id { + return false + } + + return true + }) + return +} + +func main() { + go testsuite.ReceiveSignals() + username := testsuite.GetUser().Username + + // the signal handler does not wait for termination + ctx := context.Background() + + if err := os.MkdirAll("/opt/test-helper/bin", 0755); err != nil { + log.Fatal(err) + } + + var testToolDone <-chan error + { + cmd := exec.Command( + "go", "build", + "-o", "/opt/test-helper/bin", + "-tags=tester", + "-trimpath", + "./test/sandbox/tester", + ) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + testToolDone = testsuite.MustStart(cmd) + } + + var wg sync.WaitGroup + defer wg.Wait() + + var serial atomic.Uint64 + newSerial := func() uint64 { serial.Add(1); return serial.Load() } + + testsuite.MustRunAs( + username, "-i", + "hakurei", "exec", "capsh", "--print", + ) + wg.Go(func() { + defer log.Println("validated capabilities/securebits in user namespace") + + testsuite.MustRunAs( + username, "-i", + "hakurei", "exec", "capsh", "--has-no-new-privs", + ) + + for _, p := range []byte{'a', 'b', 'i', 'p'} { + testsuite.MustFailAs( + username, "-i", + "hakurei", "exec", "capsh", "--has-"+string(p)+"=CAP_SYS_ADMIN", + ) + } + testsuite.MustFailAs( + username, "-i", + "hakurei", "exec", "umount", "-R", "/dev", + ) + }) + + wg.Go(func() { + defer log.Println("validated pd seccomp outcome") + + c, cancel := context.WithCancel(ctx) + defer cancel() + + pid, done := mustStart(c, newSerial(), username) + testsuite.MustCheckFilter(pid, testdata.SumPD) + if err := testsuite.FilterTerminated(<-done); err != nil { + log.Fatal(err) + } + }) + + wg.Go(func() { + defer log.Println("validated fd leak") + + c, cancel := context.WithCancel(ctx) + defer cancel() + + pid, done := mustStart(c, newSerial(), username, os.Stdin, os.Stdout, os.Stderr) + prefix := filepath.Join(fhs.Proc, strconv.Itoa(pid), "fd") + + var fail bool + if entries, err := os.ReadDir(prefix); err != nil { + log.Fatal(err.Error()) + } else { + for _, ent := range entries { + var fd int + if fd, err = strconv.Atoi(ent.Name()); err != nil { + log.Fatal(err.Error()) + } + + // skip standard streams + if fd <= 2 { + continue + } + fail = true + + var d string + if d, err = os.Readlink(filepath.Join( + prefix, + ent.Name(), + )); err != nil { + log.Fatal(err.Error()) + } + log.Printf("extra fd %d -> %s", fd, d) + } + } + if fail { + log.Fatal("file descriptors leaked") + } + + if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { + log.Fatalf("cannot terminate anchor: %v", err) + } else if err = testsuite.FilterTerminated(<-done); err != nil { + log.Fatal(err) + } + }) + + if err := os.MkdirAll(testsuite.XDGRuntimeDir, 0700); err != nil { + log.Fatal(err) + } else if err = os.Chown(testsuite.XDGRuntimeDir, 1000, 1000); err != nil { + log.Fatal(err) + } + + var swg sync.WaitGroup + defer swg.Wait() + dbusEnv := testsuite.MustStartSessionBus(username) + testsuite.MustStartSway(&swg, username, dbusEnv) + defer testsuite.TerminateSway(username) + testsuite.MustStartPipeWire(username, dbusEnv) + + if err := <-testToolDone; err != nil { + log.Fatal(err) + } + log.Println("created test helper") + + s := store.New(check.MustAbs("/tmp/hakurei.0/state")) + for name, tc := range testdata.All() { + wg.Go(func() { + cmd := exec.Command( + "sudo", + "-u", username, + "-C", "6", + "TERM=xterm", + testsuite.XDGRuntimeEnv, + testsuite.WaylandEnv, + "DISPLAY=:0", + dbusEnv, + "--", + + "script", "/dev/null", + "-E", "always", + "-qec", + "hakurei run "+ + "--identifier-fd=5"+ + " 4 1>&3", + ) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + } + var output bytes.Buffer + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, &output, &output + + var err error + var notify, _notify, _conf, conf, ident, _ident *os.File + if notify, _notify, err = os.Pipe(); err != nil { + log.Fatal(err) + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _notify) + if _conf, conf, err = os.Pipe(); err != nil { + log.Fatal(err) + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _conf) + if ident, _ident, err = os.Pipe(); err != nil { + log.Fatal(err) + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _ident) + + done := testsuite.MustStart(cmd) + wg.Go(func() { + _err := <-done + log.Printf("completed test case %s\n%s", name, output.String()) + if _err != nil { + log.Fatalf("test case %s: %v", name, _err) + } + }) + + if err = json.NewEncoder(conf).Encode(&tc.Hakurei); err != nil { + log.Fatal(err) + } else if err = conf.Close(); err != nil { + log.Fatal(err) + } + + var id hst.ID + if _, err = io.ReadFull(ident, id[:]); err != nil { + log.Fatal(err) + } else if err = ident.Close(); err != nil { + log.Fatal(err) + } + + if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil { + log.Fatal(err) + } else if err = notify.Close(); err != nil { + log.Fatal(err) + } + + var ( + ok bool + p hst.State + ) + entries, copyError := s.All() + for entry := range entries { + if entry.ID == id { + ok = true + if _, err = entry.Load(&p, nil); err != nil { + log.Fatal(err) + } + break + } + } + if err = copyError(); err != nil { + log.Fatal(err) + } + if !ok { + log.Fatalf("instance %s is not present in store", id) + } + + var stat syscall.Stat_t + pid := mustScanFor(func(ps *testsuite.StatScanner) bool { + select { + case err = <-done: + if err == nil { + log.Fatal("test process terminated unexpectedly") + } + log.Fatal(err) + default: + break + } + + if ps.Stat().Comm != "test-helper" { + return false + } + + var args []string + if args, err = ps.Stat().Args(); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } else if !slices.Equal(args, tc.Hakurei.Container.Args) { + return false + } + + if err = ps.Stat().Stat(&stat); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } + + uid := hst.ToUser[uint32](0, uint32(tc.Hakurei.Identity)) + if stat.Uid != uid || stat.Gid != uid { + return false + } + + var t []byte + if t, err = os.ReadFile(filepath.Join( + fhs.Proc, + strconv.Itoa(ps.Stat().PPID), + "stat", + )); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } + + var _stat testsuite.Stat + if err = _stat.UnmarshalText(t); err != nil { + log.Fatal(err) + } + if _stat.PPID != p.ShimPID { + return false + } + + return true + }) + + testsuite.MustCheckFilter( + pid, + tc.Sum, + ) + }) + } + + wg.Wait() + + if dents, err := os.ReadDir("/tmp"); err != nil { + log.Fatal(err) + } else { + for _, dent := range dents { + if name := dent.Name(); strings.HasPrefix(name, ".hakurei-shim-") { + log.Fatalf("leftover shim work dir %q", name) + } + } + } +} |
