diff options
Diffstat (limited to 'container/dispatcher_test.go')
| -rw-r--r-- | container/dispatcher_test.go | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go new file mode 100644 index 00000000..e2b553f4 --- /dev/null +++ b/container/dispatcher_test.go @@ -0,0 +1,595 @@ +package container + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "reflect" + "runtime" + "slices" + "syscall" + "testing" + "time" + + "hakurei.app/container/seccomp" +) + +var errUnique = errors.New("unique error injected by the test suite") + +type opValidTestCase struct { + name string + op Op + want bool +} + +func checkOpsValid(t *testing.T, testCases []opValidTestCase) { + t.Run("valid", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.op.Valid(); got != tc.want { + t.Errorf("Valid: %v, want %v", got, tc.want) + } + }) + } + }) +} + +type opsBuilderTestCase struct { + name string + ops *Ops + want Ops +} + +func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) { + t.Run("build", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { + t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want) + } + }) + } + }) +} + +type opIsTestCase struct { + name string + op, v Op + want bool +} + +func checkOpIs(t *testing.T, testCases []opIsTestCase) { + t.Run("is", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.op.Is(tc.v); got != tc.want { + t.Errorf("Is: %v, want %v", got, tc.want) + } + }) + } + }) +} + +type opMetaTestCase struct { + name string + op Op + + wantPrefix string + wantString string +} + +func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { + t.Run("meta", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("prefix", func(t *testing.T) { + if got := tc.op.prefix(); got != tc.wantPrefix { + t.Errorf("prefix: %q, want %q", got, tc.wantPrefix) + } + }) + + t.Run("string", func(t *testing.T) { + if got := tc.op.String(); got != tc.wantString { + t.Errorf("String: %s, want %s", got, tc.wantString) + } + }) + }) + } + }) +} + +type opBehaviourTestCase struct { + name string + params *Params + op Op + + early []kexpect + wantErrEarly error + + apply []kexpect + wantErrApply error +} + +func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { + t.Run("behaviour", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + state := &setupState{Params: tc.params} + k := &kstub{t: t, want: slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)} + errEarly := tc.op.early(state, k) + k.expect("\x00") + if !errors.Is(errEarly, tc.wantErrEarly) { + t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly) + } + if errEarly != nil { + goto out + } + + if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) { + t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply) + } + + out: + if len(k.want) != k.pos { + count := k.pos - 1 // separator + if count < len(tc.early) { + t.Errorf("early: %d calls, want %d", count, len(tc.early)) + } else { + t.Errorf("apply: %d calls, want %d", count-len(tc.early), len(tc.apply)) + } + } + }) + } + }) +} + +func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile { + f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr} + // check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results + f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name) + return f +} + +type checkedOsFile struct { + t *testing.T + name string + want string + closeErr error + cleanup runtime.Cleanup + bytes.Buffer +} + +func (f *checkedOsFile) Name() string { return f.name } +func (f *checkedOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") } +func (f *checkedOsFile) Close() error { + defer f.cleanup.Stop() + if f.String() != f.want { + f.t.Errorf("checkedOsFile:\n%s\nwant\n%s", f.String(), f.want) + return syscall.ENOTRECOVERABLE + } + return f.closeErr +} + +type writeErrOsFile struct{ err error } + +func (writeErrOsFile) Name() string { panic("unreachable") } +func (f writeErrOsFile) Write([]byte) (int, error) { return 0, f.err } +func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") } +func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") } +func (writeErrOsFile) Close() error { panic("unreachable") } + +type expectArgs = [5]any + +type isDirFi bool + +func (isDirFi) Name() string { panic("unreachable") } +func (isDirFi) Size() int64 { panic("unreachable") } +func (isDirFi) Mode() fs.FileMode { panic("unreachable") } +func (isDirFi) ModTime() time.Time { panic("unreachable") } +func (fi isDirFi) IsDir() bool { return bool(fi) } +func (isDirFi) Sys() any { panic("unreachable") } + +func stubDir(names ...string) []os.DirEntry { + d := make([]os.DirEntry, len(names)) + for i, name := range names { + d[i] = nameDentry(name) + } + return d +} + +type nameDentry string + +func (e nameDentry) Name() string { return string(e) } +func (nameDentry) IsDir() bool { panic("unreachable") } +func (nameDentry) Type() fs.FileMode { panic("unreachable") } +func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") } + +type kexpect struct { + name string + args expectArgs + ret any + err error +} + +func (k *kexpect) error(ok ...bool) error { + if !slices.Contains(ok, false) { + return k.err + } + return syscall.ENOTRECOVERABLE +} + +type kstub struct { + t *testing.T + want []kexpect + pos int +} + +// expect checks name and returns the current kexpect and advances pos. +func (k *kstub) expect(name string) (expect *kexpect) { + if len(k.want) == k.pos { + k.t.Fatal("expect: want too short") + } + expect = &k.want[k.pos] + if name != expect.name { + if expect.name == "\x00" { + k.t.Fatalf("expect: func = %s, separator overrun", name) + } + if name == "\x00" { + k.t.Fatalf("expect: separator, want %s", expect.name) + } + k.t.Fatalf("expect: func = %s, want %s", name, expect.name) + } + k.pos++ + return +} + +// checkArg checks an argument comparable with the == operator. Avoid using this with pointers. +func checkArg[T comparable](k *kstub, arg string, got T, n int) bool { + if k.pos == 0 { + panic("invalid call to checkArg") + } + expect := k.want[k.pos-1] + want, ok := expect.args[n].(T) + if !ok || got != want { + k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1) + return false + } + return true +} + +// checkArgReflect checks an argument of any type. +func checkArgReflect(k *kstub, arg string, got any, n int) bool { + if k.pos == 0 { + panic("invalid call to checkArgReflect") + } + expect := k.want[k.pos-1] + want := expect.args[n] + if !reflect.DeepEqual(got, want) { + k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1) + return false + } + return true +} + +func (k *kstub) setPtracer(pid uintptr) error { + return k.expect("setPtracer").error( + checkArg(k, "pid", pid, 0)) +} + +func (k *kstub) setDumpable(dumpable uintptr) error { + return k.expect("setDumpable").error( + checkArg(k, "dumpable", dumpable, 0)) +} + +func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err } +func (k *kstub) lastcap() uintptr { return k.expect("setNoNewPrivs").ret.(uintptr) } + +func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error { + return k.expect("capset").error( + checkArgReflect(k, "hdrp", hdrp, 0), + checkArgReflect(k, "datap", datap, 1)) +} + +func (k *kstub) capBoundingSetDrop(cap uintptr) error { + return k.expect("capBoundingSetDrop").error( + checkArg(k, "cap", cap, 0)) +} + +func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err } + +func (k *kstub) capAmbientRaise(cap uintptr) error { + return k.expect("capAmbientRaise").error( + checkArg(k, "cap", cap, 0)) +} + +func (k *kstub) isatty(fd int) bool { + expect := k.expect("isatty") + if !checkArg(k, "fd", fd, 0) { + k.t.FailNow() + } + return expect.ret.(bool) +} + +func (k *kstub) receive(key string, e any, v **os.File) (closeFunc func() error, err error) { + expect := k.expect("receive") + return expect.ret.(func() error), expect.error( + checkArg(k, "key", key, 0), + checkArgReflect(k, "e", e, 1), + checkArg(k, "v", v, 2)) +} + +func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error { + return k.expect("bindMount").error( + checkArg(k, "source", source, 0), + checkArg(k, "target", target, 1), + checkArg(k, "flags", flags, 2), + checkArg(k, "eq", eq, 3)) +} + +func (k *kstub) remount(target string, flags uintptr) error { + return k.expect("remount").error( + checkArg(k, "target", target, 0), + checkArg(k, "flags", flags, 1)) +} + +func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { + return k.expect("mountTmpfs").error( + checkArg(k, "fsname", fsname, 0), + checkArg(k, "target", target, 1), + checkArg(k, "flags", flags, 2), + checkArg(k, "size", size, 3), + checkArg(k, "perm", perm, 4)) +} + +func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error { + + return k.expect("ensureFile").error( + checkArg(k, "name", name, 0), + checkArg(k, "perm", perm, 1), + checkArg(k, "pperm", pperm, 2)) +} + +func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error { + return k.expect("seccompLoad").error( + checkArgReflect(k, "rules", rules, 0), + checkArg(k, "flags", flags, 1)) +} + +func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) { + expect := k.expect("notify") + if c == nil || expect.error( + checkArgReflect(k, "sig", sig, 1)) != nil { + k.t.FailNow() + } + + // export channel for external instrumentation + if chanp, ok := expect.args[0].(*chan<- os.Signal); ok && chanp != nil { + if *chanp != nil { + panic(fmt.Sprintf("attempting reuse of %p", chanp)) + } + *chanp = c + } +} + +func (k *kstub) start(c *exec.Cmd) error { + return k.expect("start").error( + checkArg(k, "c.Path", c.Path, 0), + checkArgReflect(k, "c.Args", c.Args, 1), + checkArgReflect(k, "c.Env", c.Env, 2), + checkArg(k, "c.Dir", c.Dir, 3)) +} + +func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error { + return k.expect("signal").error( + checkArg(k, "c.Path", c.Path, 0), + checkArgReflect(k, "c.Args", c.Args, 1), + checkArgReflect(k, "c.Env", c.Env, 2), + checkArg(k, "c.Dir", c.Dir, 3), + checkArg(k, "sig", sig, 4)) +} + +func (k *kstub) evalSymlinks(path string) (string, error) { + expect := k.expect("evalSymlinks") + return expect.ret.(string), expect.error( + checkArg(k, "path", path, 0)) +} + +func (k *kstub) exit(code int) { + k.expect("exit") + if !checkArg(k, "code", code, 0) { + k.t.FailNow() + } +} + +func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) } + +func (k *kstub) stat(name string) (os.FileInfo, error) { + expect := k.expect("stat") + return expect.ret.(os.FileInfo), expect.error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) mkdir(name string, perm os.FileMode) error { + return k.expect("mkdir").error( + checkArg(k, "name", name, 0), + checkArg(k, "perm", perm, 1)) +} + +func (k *kstub) mkdirTemp(dir, pattern string) (string, error) { + expect := k.expect("mkdirTemp") + return expect.ret.(string), expect.error( + checkArg(k, "dir", dir, 0), + checkArg(k, "pattern", pattern, 1)) +} + +func (k *kstub) mkdirAll(path string, perm os.FileMode) error { + return k.expect("mkdirAll").error( + checkArg(k, "path", path, 0), + checkArg(k, "perm", perm, 1)) +} + +func (k *kstub) readdir(name string) ([]os.DirEntry, error) { + expect := k.expect("readdir") + return expect.ret.([]os.DirEntry), expect.error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error { + return k.expect("writeFile").error( + checkArg(k, "name", name, 0), + checkArgReflect(k, "data", data, 1), + checkArg(k, "perm", perm, 2)) +} + +func (k *kstub) createTemp(dir, pattern string) (osFile, error) { + expect := k.expect("createTemp") + return expect.ret.(osFile), expect.error( + checkArg(k, "dir", dir, 0), + checkArg(k, "pattern", pattern, 1)) +} + +func (k *kstub) remove(name string) error { + return k.expect("remove").error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) newFile(fd uintptr, name string) *os.File { + expect := k.expect("newFile") + if expect.error( + checkArg(k, "fd", fd, 0), + checkArg(k, "name", name, 1)) != nil { + k.t.FailNow() + } + return expect.ret.(*os.File) +} + +func (k *kstub) symlink(oldname, newname string) error { + return k.expect("symlink").error( + checkArg(k, "oldname", oldname, 0), + checkArg(k, "newname", newname, 1)) +} + +func (k *kstub) readlink(name string) (string, error) { + expect := k.expect("readlink") + return expect.ret.(string), expect.error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) umask(mask int) (oldmask int) { + expect := k.expect("umask") + if !checkArg(k, "mask", mask, 0) { + k.t.FailNow() + } + return expect.ret.(int) +} + +func (k *kstub) sethostname(p []byte) (err error) { + return k.expect("sethostname").error( + checkArgReflect(k, "p", p, 0)) +} + +func (k *kstub) chdir(path string) (err error) { + return k.expect("chdir").error( + checkArg(k, "path", path, 0)) +} + +func (k *kstub) fchdir(fd int) (err error) { + return k.expect("fchdir").error( + checkArg(k, "fd", fd, 0)) +} + +func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) { + expect := k.expect("open") + return expect.ret.(int), expect.error( + checkArg(k, "path", path, 0), + checkArg(k, "mode", mode, 1), + checkArg(k, "perm", perm, 2)) +} + +func (k *kstub) close(fd int) (err error) { + return k.expect("close").error( + checkArg(k, "fd", fd, 0)) +} + +func (k *kstub) pivotRoot(newroot, putold string) (err error) { + return k.expect("pivotRoot").error( + checkArg(k, "newroot", newroot, 0), + checkArg(k, "putold", putold, 1)) +} + +func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) { + return k.expect("mount").error( + checkArg(k, "source", source, 0), + checkArg(k, "target", target, 1), + checkArg(k, "fstype", fstype, 2), + checkArg(k, "flags", flags, 3), + checkArg(k, "data", data, 4)) +} + +func (k *kstub) unmount(target string, flags int) (err error) { + return k.expect("unmount").error( + checkArg(k, "target", target, 0), + checkArg(k, "flags", flags, 1)) +} + +func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) { + expect := k.expect("wait4") + return expect.ret.(int), expect.error( + checkArg(k, "pid", pid, 0), + checkArg(k, "wstatus", wstatus, 1), + checkArg(k, "options", options, 2), + checkArg(k, "rusage", rusage, 3)) +} + +func (k *kstub) printf(format string, v ...any) { + if k.expect("printf").error( + checkArg(k, "format", format, 0), + checkArgReflect(k, "v", v, 1)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) fatal(v ...any) { + if k.expect("fatal").error( + checkArgReflect(k, "v", v, 0)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) fatalf(format string, v ...any) { + if k.expect("fatalf").error( + checkArg(k, "format", format, 0), + checkArgReflect(k, "v", v, 1)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) verbose(v ...any) { + if k.expect("verbose").error( + checkArgReflect(k, "v", v, 0)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) verbosef(format string, v ...any) { + if k.expect("verbosef").error( + checkArg(k, "format", format, 0), + checkArgReflect(k, "v", v, 1)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) suspend() { k.expect("suspend") } +func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) } +func (k *kstub) beforeExit() { k.expect("beforeExit") } + +func (k *kstub) printBaseErr(err error, fallback string) { + if k.expect("printBaseErr").error( + checkArgReflect(k, "err", err, 0), + checkArg(k, "fallback", fallback, 1)) != nil { + k.t.FailNow() + } +} |
