aboutsummaryrefslogtreecommitdiffhomepage
path: root/test/internal/sandbox/assert.go
diff options
context:
space:
mode:
Diffstat (limited to 'test/internal/sandbox/assert.go')
-rw-r--r--test/internal/sandbox/assert.go272
1 files changed, 272 insertions, 0 deletions
diff --git a/test/internal/sandbox/assert.go b/test/internal/sandbox/assert.go
new file mode 100644
index 00000000..82c928b0
--- /dev/null
+++ b/test/internal/sandbox/assert.go
@@ -0,0 +1,272 @@
+//go:build testtool
+
+/*
+Package sandbox provides utilities for checking sandbox outcome.
+
+This package must never be used outside integration tests, there is a much better native implementation of mountinfo
+in the public sandbox/vfs package. Files in this package are excluded by the build system to prevent accidental misuse.
+*/
+package sandbox
+
+import (
+ "crypto/sha512"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "io/fs"
+ "log"
+ "net"
+ "os"
+ "path"
+ "syscall"
+)
+
+var (
+ assert = log.New(os.Stderr, "sandbox: ", 0)
+ printfFunc = assert.Printf
+ fatalfFunc = assert.Fatalf
+)
+
+func printf(format string, v ...any) { printfFunc(format, v...) }
+func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
+
+type TestCase struct {
+ Env []string `json:"env"`
+ FS *FS `json:"fs"`
+ Mount []*MountinfoEntry `json:"mount"`
+ Seccomp bool `json:"seccomp"`
+
+ TrySocket string `json:"try_socket,omitempty"`
+ SocketAbstract bool `json:"socket_abstract,omitempty"`
+ SocketPathname bool `json:"socket_pathname,omitempty"`
+}
+
+type T struct {
+ FS fs.FS
+
+ MountsPath string
+}
+
+func (t *T) MustCheckFile(wantFilePath string) {
+ var want *TestCase
+ mustDecode(wantFilePath, &want)
+ t.MustCheck(want)
+}
+
+func mustAbs(s string) string {
+ if !path.IsAbs(s) {
+ fatalf("[FAIL] %q is not absolute", s)
+ panic("unreachable")
+ }
+ return s
+}
+
+func (t *T) MustCheck(want *TestCase) {
+ checkWritableDirPaths := []string{
+ "/dev/shm",
+ "/tmp",
+ os.Getenv("XDG_RUNTIME_DIR"),
+ }
+ for _, a := range checkWritableDirPaths {
+ pathname := path.Join(mustAbs(a), ".hakurei-check")
+ if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil {
+ fatalf("[FAIL] %s", err)
+ } else if err = os.Remove(pathname); err != nil {
+ fatalf("[FAIL] %s", err)
+ } else {
+ printf("[ OK ] %s is writable", a)
+ }
+ }
+
+ if want.Env != nil {
+ var (
+ fail bool
+ i int
+ got string
+ )
+ for i, got = range os.Environ() {
+ if i == len(want.Env) {
+ fatalf("got more than %d environment variables", len(want.Env))
+ }
+ if got != want.Env[i] {
+ fail = true
+ printf("[FAIL] %s", got)
+ } else {
+ printf("[ OK ] %s", got)
+ }
+ }
+
+ i++
+ if i != len(want.Env) {
+ fatalf("got %d environment variables, want %d", i, len(want.Env))
+ }
+
+ if fail {
+ fatalf("[FAIL] some environment variables did not match")
+ }
+ } else {
+ printf("[SKIP] skipping environ check")
+ }
+
+ if want.FS != nil && t.FS != nil {
+ if err := want.FS.Compare(".", t.FS); err != nil {
+ fatalf("%v", err)
+ }
+ } else {
+ printf("[SKIP] skipping fs check")
+ }
+
+ if want.Mount != nil {
+ var fail bool
+ m := mustParseMountinfo(t.MountsPath)
+ i := 0
+ for ent := range m.Entries() {
+ if i == len(want.Mount) {
+ fatalf("got more than %d entries", i)
+ }
+ if !ent.EqualWithIgnore(want.Mount[i], "//ignore") {
+ fail = true
+ printf("[FAIL] %s", ent)
+ } else {
+ printf("[ OK ] %s", ent)
+ }
+
+ i++
+ }
+ if err := m.Err(); err != nil {
+ fatalf("%v", err)
+ }
+
+ if i != len(want.Mount) {
+ fatalf("got %d entries, want %d", i, len(want.Mount))
+ }
+
+ if fail {
+ fatalf("[FAIL] some mount points did not match")
+ }
+ } else {
+ printf("[SKIP] skipping mounts check")
+ }
+
+ if want.Seccomp {
+ if trySyscalls() != nil {
+ os.Exit(1)
+ }
+ } else {
+ printf("[SKIP] skipping seccomp check")
+ }
+
+ if want.TrySocket != "" {
+ abstractConn, abstractErr := net.Dial("unix", "@"+want.TrySocket)
+ pathnameConn, pathnameErr := net.Dial("unix", want.TrySocket)
+ ok := true
+
+ if abstractErr == nil {
+ if err := abstractConn.Close(); err != nil {
+ ok = false
+ log.Printf("Close: %v", err)
+ }
+ }
+ if pathnameErr == nil {
+ if err := pathnameConn.Close(); err != nil {
+ ok = false
+ log.Printf("Close: %v", err)
+ }
+ }
+
+ abstractWantErr := error(syscall.EPERM)
+ pathnameWantErr := error(syscall.ENOENT)
+ if want.SocketAbstract {
+ abstractWantErr = nil
+ }
+ if want.SocketPathname {
+ pathnameWantErr = nil
+ }
+
+ if !errors.Is(abstractErr, abstractWantErr) {
+ ok = false
+ log.Printf("abstractErr: %v, want %v", abstractErr, abstractWantErr)
+ }
+ if !errors.Is(pathnameErr, pathnameWantErr) {
+ ok = false
+ log.Printf("pathnameErr: %v, want %v", pathnameErr, pathnameWantErr)
+ }
+
+ if !ok {
+ os.Exit(1)
+ }
+ }
+}
+
+func MustCheckFilter(pid int, want string) {
+ err := CheckFilter(pid, want)
+ if err == nil {
+ return
+ }
+
+ var perr *ptraceError
+ if !errors.As(err, &perr) {
+ fatalf("%s", err)
+ }
+ switch perr.op {
+ case "PTRACE_ATTACH":
+ fatalf("cannot attach to process %d: %v", pid, err)
+ case "PTRACE_SECCOMP_GET_FILTER":
+ if perr.errno == syscall.ENOENT {
+ fatalf("seccomp filter not installed for process %d", pid)
+ }
+ fatalf("cannot get filter: %v", err)
+ default:
+ fatalf("cannot check filter: %v", err)
+ }
+
+ *(*int)(nil) = 0 // not reached
+}
+
+func CheckFilter(pid int, want string) error {
+ if err := ptraceAttach(pid); err != nil {
+ return err
+ }
+ defer func() {
+ if err := ptraceDetach(pid); err != nil {
+ printf("cannot detach from process %d: %v", pid, err)
+ }
+ }()
+
+ h := sha512.New()
+
+ if buf, err := getFilter[[8]byte](pid, 0); err != nil {
+ return err
+ } else {
+ for _, b := range buf {
+ h.Write(b[:])
+ }
+ }
+
+ if got := hex.EncodeToString(h.Sum(nil)); got != want {
+ printf("[FAIL] %s", got)
+ return syscall.ENOTRECOVERABLE
+ } else {
+ printf("[ OK ] %s", got)
+ return nil
+ }
+}
+
+func mustDecode(wantFilePath string, v any) {
+ if f, err := os.Open(wantFilePath); err != nil {
+ fatalf("cannot open %q: %v", wantFilePath, err)
+ } else if err = json.NewDecoder(f).Decode(v); err != nil {
+ fatalf("cannot decode %q: %v", wantFilePath, err)
+ } else if err = f.Close(); err != nil {
+ fatalf("cannot close %q: %v", wantFilePath, err)
+ }
+}
+
+func mustParseMountinfo(name string) *Mountinfo {
+ m := NewMountinfo(name)
+ if err := m.Parse(); err != nil {
+ fatalf("%v", err)
+ panic("unreachable")
+ }
+ return m
+}