hakurei/container/dispatcher_test.go
Ophestra 2baa2d7063
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m12s
Test / Hakurei (push) Successful in 3m17s
Test / Hpkg (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 4m33s
Test / Hakurei (race detector) (push) Successful in 5m8s
Test / Flake checks (push) Successful in 1m25s
container/init: measure init behaviour
This used to be entirely done via integration tests, with almost no hope of error injection and coverage profile. These tests significantly increase confidence of future work in this area.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-24 04:52:32 +09:00

749 lines
19 KiB
Go

package container
import (
"bytes"
"errors"
"io"
"io/fs"
"os"
"os/exec"
"reflect"
"runtime"
"slices"
"strings"
"sync"
"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 simpleTestCase struct {
name string
f func(k syscallDispatcher) error
want [][]kexpect
wantErr error
}
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer handleExitStub(t, "check")
k := &kstub{t: t, want: tc.want, wg: new(sync.WaitGroup)}
if err := tc.f(k); !errors.Is(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
}
k.handleIncomplete(func(k *kstub) {
t.Errorf("%s: %d calls, want %d (track %d)", fname, k.pos, len(k.want[k.track]), k.track)
})
})
}
}
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: [][]kexpect{slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)}, wg: new(sync.WaitGroup)}
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:
k.handleIncomplete(func(k *kstub) {
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 sliceAddr[S any](s []S) *[]S { return &s }
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
}
func newConstFile(s string) osFile { return &readerOsFile{Reader: strings.NewReader(s)} }
type readerOsFile struct {
closed bool
io.Reader
}
func (*readerOsFile) Name() string { panic("unreachable") }
func (*readerOsFile) Write([]byte) (int, error) { panic("unreachable") }
func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (r *readerOsFile) Close() error {
if r.closed {
return os.ErrClosed
}
r.closed = true
return nil
}
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
}
func handleExitStub(t *testing.T, prefix string) {
r := recover()
if r == 0xdeadbeef {
t.Log(prefix + " terminated on an exit stub")
return
}
if r != nil {
panic(r)
}
}
type kstub struct {
t *testing.T
want [][]kexpect
// pos is the current position in want[track].
pos int
// track is the current active want.
track int
// sub stores addresses of kstub created by new.
sub []*kstub
// wg waits for all descendants to complete.
wg *sync.WaitGroup
}
// handleIncomplete calls f on an incomplete k and all its descendants.
func (k *kstub) handleIncomplete(f func(k *kstub)) {
k.wg.Wait()
if k.want != nil && len(k.want[k.track]) != k.pos {
f(k)
}
for _, sk := range k.sub {
sk.handleIncomplete(f)
}
}
// expect checks name and returns the current kexpect and advances pos.
func (k *kstub) expect(name string) (expect *kexpect) {
if len(k.want[k.track]) == k.pos {
k.t.Fatal("expect: want too short")
}
expect = &k.want[k.track][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.track][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.track][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) new(f func(k syscallDispatcher)) {
k.expect("new")
if len(k.want) <= k.track+1 {
k.t.Fatalf("new: track overrun")
}
sk := &kstub{t: k.t, want: k.want, track: len(k.sub) + 1, wg: k.wg}
k.sub = append(k.sub, sk)
k.wg.Add(1)
go func() {
defer k.wg.Done()
defer handleExitStub(k.t, "goroutine")
f(sk)
}()
}
func (k *kstub) lockOSThread() { k.expect("lockOSThread") }
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("lastcap").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, fdp *uintptr) (closeFunc func() error, err error) {
expect := k.expect("receive")
var closed bool
closeFunc = func() error {
if closed {
k.t.Error("closeFunc called more than once")
return os.ErrClosed
}
closed = true
if expect.ret != nil {
// use return stored in kexpect for closeFunc instead
return expect.ret.(error)
}
return nil
}
err = expect.error(
checkArg(k, "key", key, 0),
checkArgReflect(k, "e", e, 1),
checkArgReflect(k, "fdp", fdp, 2))
// 3 is unused so stores params
if expect.args[3] != nil {
if v, ok := expect.args[3].(*initParams); ok && v != nil {
if p, ok0 := e.(*initParams); ok0 && p != nil {
*p = *v
}
}
}
// 4 is unused so stores fd
if expect.args[4] != nil {
if v, ok := expect.args[4].(uintptr); ok && v >= 3 {
if fdp != nil {
*fdp = v
}
}
}
return
}
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 chanf, ok := expect.args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c)
}
}
func (k *kstub) start(c *exec.Cmd) error {
expect := k.expect("start")
err := expect.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))
if process, ok := expect.ret.(*os.Process); ok && process != nil {
c.Process = process
}
return err
}
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()
}
panic(0xdeadbeef)
}
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) openNew(name string) (osFile, error) {
expect := k.expect("openNew")
return expect.ret.(osFile), 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")
// special case to prevent leaking the wait4 goroutine when testing initEntrypoint
if v, ok := expect.args[4].(int); ok && v == 0xdeadbeef {
k.t.Log("terminating current goroutine as requested by kexpect")
panic(0xdeadbeef)
}
wpid = expect.ret.(int)
err = expect.error(
checkArg(k, "pid", pid, 0),
checkArg(k, "options", options, 2))
if wstatusV, ok := expect.args[1].(syscall.WaitStatus); wstatus != nil && ok {
*wstatus = wstatusV
}
if rusageV, ok := expect.args[3].(syscall.Rusage); rusage != nil && ok {
*rusage = rusageV
}
return
}
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()
}
panic(0xdeadbeef)
}
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()
}
panic(0xdeadbeef)
}
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()
}
}