diff options
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) + } + } + } +} |
