From 46c5ce493638ea2d034bbbdb9449e93b66d0330f Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 30 Oct 2025 05:20:49 +0900 Subject: [PATCH] internal/outcome/shim: check full behaviour This took significant effort to stub out, and achieves full coverage after c5aefe5e9d. Signed-off-by: Ophestra --- internal/outcome/dispatcher_test.go | 87 ++++++- internal/outcome/shim.go | 24 +- internal/outcome/shim_test.go | 344 +++++++++++++++++++++++++++- 3 files changed, 424 insertions(+), 31 deletions(-) diff --git a/internal/outcome/dispatcher_test.go b/internal/outcome/dispatcher_test.go index e01946c..96822fa 100644 --- a/internal/outcome/dispatcher_test.go +++ b/internal/outcome/dispatcher_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" "io/fs" "log" @@ -193,8 +194,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { } wantConfig := tc.newConfig() - k := &kstub{panicDispatcher{}, stub.New(t, - func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} }, + k := &kstub{nil, nil, panicDispatcher{}, stub.New(t, + func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, panicDispatcher{}, s} }, stub.Expect{Calls: wantCallsFull}, )} defer stub.HandleExit(t) @@ -297,7 +298,12 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) { t.Parallel() defer stub.HandleExit(t) - k := &kstub{panicDispatcher{}, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} }, tc.want)} + + uNotifyContext, uShimReader := make(chan struct{}), make(chan struct{}) + k := &kstub{uNotifyContext, uShimReader, panicDispatcher{}, + stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { + return &kstub{uNotifyContext, uShimReader, panicDispatcher{}, s} + }, tc.want)} if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) { t.Errorf("%s: error = %#v, want %#v", fname, err, tc.wantErr) } @@ -312,6 +318,11 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) { // kstub partially implements syscallDispatcher via [stub.Stub]. type kstub struct { + // notifyContext blocks on unblockNotifyContext if provided with a kstub.Stub track. + unblockNotifyContext chan struct{} + // stubTrackReader blocks on unblockShimReader if unblocking notifyContext. + unblockShimReader chan struct{} + panicDispatcher *stub.Stub[syscallDispatcher] } @@ -354,6 +365,19 @@ func (k *kstub) readdir(name string) ([]os.DirEntry, error) { stub.CheckArg(k.Stub, "name", name, 0)) } func (k *kstub) tempdir() string { k.Helper(); return k.Expects("tempdir").Ret.(string) } +func (k *kstub) exit(code int) { + k.Helper() + expect := k.Expects("exit") + + if errors.Is(expect.Err, unblockNotifyContext) { + close(k.unblockNotifyContext) + } + + if !stub.CheckArg(k.Stub, "code", code, 0) { + k.FailNow() + } + panic(expect.Ret.(int)) +} func (k *kstub) evalSymlinks(path string) (string, error) { k.Helper() expect := k.Expects("evalSymlinks") @@ -388,16 +412,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error func (k *kstub) expectCheckContainer(expect *stub.Call, z *container.Container) error { k.Helper() - err := expect.Error( - stub.CheckArgReflect(k.Stub, "params", &z.Params, 0)) - if err != nil { + if !stub.CheckArgReflect(k.Stub, "params", &z.Params, 0) { k.Errorf("params:\n%s\n%s", mustMarshal(&z.Params), mustMarshal(expect.Args[0])) } - return err + return expect.Err } func (k *kstub) containerStart(z *container.Container) error { k.Helper() + if k.unblockShimReader != nil { + close(k.unblockShimReader) + } return k.expectCheckContainer(k.Expects("containerStart"), z) } func (k *kstub) containerServe(z *container.Container) error { @@ -428,12 +453,25 @@ func (k *kstub) cmdOutput(cmd *exec.Cmd) ([]byte, error) { func (k *kstub) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) { k.Helper() - if k.Expects("notifyContext").Error( + expect := k.Expects("notifyContext") + + if expect.Error( stub.CheckArgReflect(k.Stub, "parent", parent, 0), stub.CheckArgReflect(k.Stub, "signals", signals, 1)) != nil { k.FailNow() } - return k.Context(), func() { k.Helper(); k.Expects("notifyContextStop") } + + if sub, ok := expect.Ret.(int); ok && sub >= 0 { + subVal := reflect.ValueOf(k.Stub).Elem().FieldByName("sub") + ks := &kstub{nil, nil, panicDispatcher{}, reflect. + NewAt(subVal.Type(), unsafe.Pointer(subVal.UnsafeAddr())).Elem(). + Interface().([]*stub.Stub[syscallDispatcher])[sub]} + + <-k.unblockNotifyContext + return k.Context(), func() { k.Helper(); ks.Expects("notifyContextStop") } + } + + return k.Context(), func() { panic("unexpected call to stop") } } func (k *kstub) mustHsuPath() *check.Absolute { @@ -457,26 +495,51 @@ type stubTrackReader struct { *kstub } +// unblockNotifyContext is passed via call and must be handled by stubTrackReader.Read +var unblockNotifyContext = errors.New("this error unblocks notifyContext and must not be returned") + func (r *stubTrackReader) Read(p []byte) (n int, err error) { r.subOnce.Do(func() { subVal := reflect.ValueOf(r.kstub.Stub).Elem().FieldByName("sub") - r.kstub = &kstub{panicDispatcher{}, reflect. + r.kstub = &kstub{r.kstub.unblockNotifyContext, r.kstub.unblockShimReader, panicDispatcher{}, reflect. NewAt(subVal.Type(), unsafe.Pointer(subVal.UnsafeAddr())).Elem(). Interface().([]*stub.Stub[syscallDispatcher])[r.sub]} }) - return r.kstub.Read(p) + n, err = r.kstub.Read(p) + if errors.Is(err, unblockNotifyContext) { + err = nil + close(r.unblockNotifyContext) + <-r.unblockShimReader + } + return n, err } func (k *kstub) setupContSignal(pid int) (io.ReadCloser, func(), error) { k.Helper() expect := k.Expects("setupContSignal") - return &stubTrackReader{sub: expect.Ret.(int), kstub: k}, func() { k.Expects("wKeepAlive") }, expect.Error( + return &stubTrackReader{sub: expect.Ret.(int), kstub: k}, func() { k.Helper(); k.Expects("wKeepAlive") }, expect.Error( stub.CheckArg(k.Stub, "pid", pid, 0)) } func (k *kstub) getMsg() message.Msg { k.Helper(); k.Expects("getMsg"); return k } +func (k *kstub) fatal(v ...any) { + if k.Expects("fatal").Error( + stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { + k.FailNow() + } + panic(stub.PanicExit) +} +func (k *kstub) fatalf(format string, v ...any) { + if k.Expects("fatalf").Error( + stub.CheckArg(k.Stub, "format", format, 0), + stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil { + k.FailNow() + } + panic(stub.PanicExit) +} + func (k *kstub) Close() error { k.Helper(); return k.Expects("rcClose").Err } func (k *kstub) Read(p []byte) (n int, err error) { k.Helper() diff --git a/internal/outcome/shim.go b/internal/outcome/shim.go index a2dcb15..1a83f8e 100644 --- a/internal/outcome/shim.go +++ b/internal/outcome/shim.go @@ -93,7 +93,7 @@ func shimEntrypoint(k syscallDispatcher) { } if err := k.setDumpable(container.SUID_DUMP_DISABLE); err != nil { - k.fatalf("cannot set SUID_DUMP_DISABLE: %s", err) + k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err) } var ( @@ -114,11 +114,8 @@ func shimEntrypoint(k syscallDispatcher) { closeSetup = f if err = state.populateLocal(k, msg); err != nil { - if m, ok := message.GetMessage(err); ok { - k.fatal(m) - } else { - k.fatalf("cannot populate local state: %v", err) - } + printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) }, + "cannot populate local state:", err) } } @@ -138,7 +135,6 @@ func shimEntrypoint(k syscallDispatcher) { k.fatalf("cannot set up exit request: %v", err) return } - } else { defer wKeepAlive() signalPipe = r @@ -152,15 +148,12 @@ func shimEntrypoint(k syscallDispatcher) { stateParams := state.newParams() for _, op := range state.Shim.Ops { if err := op.toContainer(stateParams); err != nil { - if m, ok := message.GetMessage(err); ok { - k.fatal(m) - } else { - k.fatalf("cannot create container state: %v", err) - } + printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) }, + "cannot create container state:", err) } } - if stateParams.params.Ops == nil { // unreachable - k.fatal("invalid container params") + if stateParams.params.Ops == nil { // only reachable with corrupted outcomeState + k.fatal("invalid container state") } // shim exit outcomes @@ -226,7 +219,8 @@ func shimEntrypoint(k syscallDispatcher) { k.exit(hst.ExitFailure) } if err := k.containerServe(z); err != nil { - printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) }, "cannot configure container:", err) + printMessageError(func(v ...any) { k.fatal(fmt.Sprintln(v...)) }, + "cannot configure container:", err) } if err := k.seccompLoad( diff --git a/internal/outcome/shim_test.go b/internal/outcome/shim_test.go index 046c22b..9626b6c 100644 --- a/internal/outcome/shim_test.go +++ b/internal/outcome/shim_test.go @@ -106,8 +106,8 @@ func TestShimEntrypoint(t *testing.T) { Remount(fhs.AbsRoot, syscall.MS_RDONLY), } - templateState := outcomeState{ - Shim: &shimParams{PrivPID: 0xbad, WaitDelay: 0xf, Verbose: true, Ops: []outcomeOp{ + newShimParams := func() *shimParams { + return &shimParams{PrivPID: 0xbad, WaitDelay: 0xf, Verbose: true, Ops: []outcomeOp{ &spParamsOp{"xterm-256color", true}, &spRuntimeOp{sessionTypeWayland}, spTmpdirOp{}, @@ -116,8 +116,11 @@ func TestShimEntrypoint(t *testing.T) { &spPulseOp{(*[pulseCookieSizeMax]byte)(bytes.Repeat([]byte{0}, pulseCookieSizeMax)), pulseCookieSizeMax}, &spDBusOp{true}, &spFilesystemOp{}, - }}, + }} + } + templateState := outcomeState{ + Shim: newShimParams(), ID: &checkExpectInstanceId, Identity: hst.IdentityMax, UserID: 10, @@ -128,6 +131,333 @@ func TestShimEntrypoint(t *testing.T) { } checkSimple(t, "shimEntrypoint", []simpleTestCase{ + {"dumpable", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, new(log.Logger), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, stub.UniqueError(11)), + call("fatalf", stub.ExpectArgs{"cannot set SUID_DUMP_DISABLE: %v", []any{stub.UniqueError(11)}}, nil, nil), + }}, nil}, + + {"receive fd", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, syscall.EBADF), + call("fatal", stub.ExpectArgs{[]any{"invalid config descriptor"}}, nil, nil), + }}, nil}, + + {"receive env", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv), + call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil), + }}, nil}, + + {"receive strange", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, stub.UniqueError(10)), + call("fatalf", stub.ExpectArgs{"cannot receive shim setup params: %v", []any{stub.UniqueError(10)}}, nil, nil), + }}, nil}, + + {"invalid state", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", func() outcomeState { + state := templateState + state.Shim = newShimParams() + state.Shim.PrivPID = 0 + return state + }(), nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("fatal", stub.ExpectArgs{[]any{"impossible outcome state reached\n"}}, nil, nil), + }}, nil}, + + {"sigaction pipe", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, &os.SyscallError{Syscall: "pipe2", Err: stub.UniqueError(9)}), + call("fatal", stub.ExpectArgs{[]any{"pipe2: unique error 9 injected by the test suite"}}, nil, nil), + }}, nil}, + + {"sigaction cgo", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, syscall.ENOTRECOVERABLE), + call("fatalf", stub.ExpectArgs{"cannot install SIGCONT handler: %v", []any{syscall.ENOTRECOVERABLE}}, nil, nil), + }}, nil}, + + {"sigaction strange", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, stub.UniqueError(8)), + call("fatalf", stub.ExpectArgs{"cannot set up exit request: %v", []any{stub.UniqueError(8)}}, nil, nil), + }}, nil}, + + {"prctl", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, stub.UniqueError(7)), + call("fatalf", stub.ExpectArgs{"cannot set parent-death signal: %v", []any{stub.UniqueError(7)}}, nil, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }}, nil}, + + {"toContainer", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", func() outcomeState { + state := templateState + state.Shim = newShimParams() + state.Shim.Ops = []outcomeOp{errorOp(6)} + return state + }(), nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("fatal", stub.ExpectArgs{[]any{"cannot create container state: unique error 6 injected by the test suite\n"}}, nil, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }}, nil}, + + {"bad ops", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", func() outcomeState { + state := templateState + state.Shim = newShimParams() + state.Shim.Ops = nil + return state + }(), nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("fatal", stub.ExpectArgs{[]any{"invalid container state"}}, nil, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }}, nil}, + + {"start", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, stub.UniqueError(5)), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("verbose", stub.ExpectArgs{[]any{"cannot start container: unique error 5 injected by the test suite\n"}}, nil, nil), + call("exit", stub.ExpectArgs{hst.ExitFailure}, stub.PanicExit, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, nil, nil), // stub terminates this goroutine + }}}}, nil}, + + {"start logger signalread", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, stub.UniqueError(5)), + call("getLogger", stub.ExpectArgs{}, log.Default(), nil), + call("exit", stub.ExpectArgs{hst.ExitFailure}, stub.PanicExit, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, []byte{}, stub.UniqueError(4)), + call("fatalf", stub.ExpectArgs{"cannot read from signal pipe: %v", []any{stub.UniqueError(4)}}, nil, nil), + }}}}, nil}, + + {"serve", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), + call("containerServe", stub.ExpectArgs{templateParams}, nil, stub.UniqueError(3)), + call("fatal", stub.ExpectArgs{[]any{"cannot configure container: unique error 3 injected by the test suite\n"}}, nil, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, nil, nil), // stub terminates this goroutine + }}}}, nil}, + + {"seccomp", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), + call("containerServe", stub.ExpectArgs{templateParams}, nil, nil), + call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, stub.UniqueError(2)), + call("fatalf", stub.ExpectArgs{"cannot load syscall filter: %v", []any{stub.UniqueError(2)}}, nil, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, nil, nil), // stub terminates this goroutine + }}}}, nil}, + + {"exited closesetup earlyrequested", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, stub.UniqueError(1)), + call("verbosef", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, 0, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), + call("containerServe", stub.ExpectArgs{templateParams}, nil, nil), + call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil), + call("containerWait", stub.ExpectArgs{templateParams}, nil, makeExitError(1<<8)), + call("exit", stub.ExpectArgs{1}, stub.PanicExit, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, []byte{shimMsgExitRequested}, nil), + call("exit", stub.ExpectArgs{hst.ExitRequest}, stub.PanicExit, unblockNotifyContext), + }}}}, nil}, + + {"exited requested", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, 0, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), + call("containerServe", stub.ExpectArgs{templateParams}, nil, nil), + call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil), + call("containerWait", stub.ExpectArgs{templateParams}, nil, makeExitError(1<<8)), + call("exit", stub.ExpectArgs{1}, stub.PanicExit, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, []byte{shimMsgExitRequested}, unblockNotifyContext), + call("notifyContextStop", stub.ExpectArgs{}, nil, nil), + call("rcRead", stub.ExpectArgs{}, nil, nil), // stub terminates this goroutine + }}}}, nil}, + + {"canceled orphaned", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), + call("containerServe", stub.ExpectArgs{templateParams}, nil, nil), + call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil), + call("containerWait", stub.ExpectArgs{templateParams}, nil, context.Canceled), + call("exit", stub.ExpectArgs{hst.ExitCancel}, stub.PanicExit, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, []byte{shimMsgOrphaned}, nil), + call("exit", stub.ExpectArgs{hst.ExitOrphan}, stub.PanicExit, nil), + }}}}, nil}, + + {"strangewait invalidmsg", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ + call("getMsg", stub.ExpectArgs{}, nil, nil), + call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), + call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil), + call("receive", stub.ExpectArgs{"HAKUREI_SHIM", templateState, nil}, nil, nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), + call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil), + call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil), + call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), + call("New", stub.ExpectArgs{}, nil, nil), + call("closeReceive", stub.ExpectArgs{}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), + call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), + call("containerServe", stub.ExpectArgs{templateParams}, nil, nil), + call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil), + call("containerWait", stub.ExpectArgs{templateParams}, nil, stub.UniqueError(0)), + call("verbosef", stub.ExpectArgs{"cannot wait: %v", []any{stub.UniqueError(0)}}, nil, nil), + call("exit", stub.ExpectArgs{127}, stub.PanicExit, nil), + + // deferred + call("wKeepAlive", stub.ExpectArgs{}, nil, nil), + }, Tracks: []stub.Expect{{Calls: []stub.Call{ + call("rcRead", stub.ExpectArgs{}, []byte{0xff}, nil), + call("fatalf", stub.ExpectArgs{"got invalid message %d from signal handler", []any{byte(0xff)}}, nil, nil), + }}}}, nil}, + {"success", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{ call("getMsg", stub.ExpectArgs{}, nil, nil), call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil), @@ -139,7 +469,7 @@ func TestShimEntrypoint(t *testing.T) { call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil), call("New", stub.ExpectArgs{}, nil, nil), call("closeReceive", stub.ExpectArgs{}, nil, nil), - call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, nil, nil), + call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, -1, nil), call("containerStart", stub.ExpectArgs{templateParams}, nil, nil), call("containerServe", stub.ExpectArgs{templateParams}, nil, nil), call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil), @@ -156,3 +486,9 @@ func TestShimEntrypoint(t *testing.T) { }}}}, nil}, }) } + +// errorOp implements a noop outcomeOp that unconditionally returns [stub.UniqueError]. +type errorOp stub.UniqueError + +func (e errorOp) toSystem(*outcomeStateSys) error { return stub.UniqueError(e) } +func (e errorOp) toContainer(*outcomeStateParams) error { return stub.UniqueError(e) }