From fae910a1ad0a107a2d3cd49011430c857838d2a8 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sun, 14 Dec 2025 10:22:48 +0900 Subject: [PATCH] container: sync stubbed wait4 loop after notify This ensures consistent state observed by wait4 loop when running against stub. Signed-off-by: Ophestra --- container/autoroot_test.go | 2 +- container/dispatcher_test.go | 20 ++++++++++++++++---- container/init_test.go | 30 +++++++++++++++--------------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/container/autoroot_test.go b/container/autoroot_test.go index bf846a2..e4a982f 100644 --- a/container/autoroot_test.go +++ b/container/autoroot_test.go @@ -202,7 +202,7 @@ func TestIsAutoRootBindable(t *testing.T) { t.Parallel() var msg message.Msg if tc.log { - msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{ + msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{ call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil), }})} } diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go index 834c700..26c4932 100644 --- a/container/dispatcher_test.go +++ b/container/dispatcher_test.go @@ -162,7 +162,8 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) { t.Parallel() wait4signal := make(chan struct{}) - k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)} + lockNotify := make(chan struct{}) + k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)} defer stub.HandleExit(t) if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) { t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr) @@ -200,8 +201,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { t.Helper() t.Parallel() - k := &kstub{nil, stub.New(t, - func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} }, + k := &kstub{nil, nil, stub.New(t, + func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} }, stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)}, )} state := &setupState{Params: tc.params, Msg: k} @@ -322,12 +323,19 @@ const ( type kstub struct { wait4signal chan struct{} + lockNotify chan struct{} *stub.Stub[syscallDispatcher] } func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } -func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") } +func (k *kstub) lockOSThread() { + k.Helper() + expect := k.Expects("lockOSThread") + if k.lockNotify != nil && expect.Ret == magicWait4Signal { + <-k.lockNotify + } +} func (k *kstub) setPtracer(pid uintptr) error { k.Helper() @@ -472,6 +480,10 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) { k.FailNow() } + if k.lockNotify != nil && expect.Ret == magicWait4Signal { + defer close(k.lockNotify) + } + // export channel for external instrumentation if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil { chanf(c) diff --git a/container/init_test.go b/container/init_test.go index e8daf6f..6177ff3 100644 --- a/container/init_test.go +++ b/container/init_test.go @@ -1992,7 +1992,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), // this terminates the goroutine at the call, preventing it from leaking while preserving behaviour call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD), @@ -2075,7 +2075,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), - call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbose", stub.ExpectArgs{[]any{"forwarding context cancellation"}}, nil, nil), // magicWait4Signal as ret causes wait4 stub to unblock call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", os.Interrupt}, magicWait4Signal, stub.UniqueError(9)), @@ -2090,7 +2090,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), // magicWait4Signal as args[4] causes this to block until simulated signal is delivered call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil), @@ -2175,7 +2175,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), - call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- syscall.SIGQUIT }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbosef", stub.ExpectArgs{"got %s, forwarding to initial process", []any{"quit"}}, nil, nil), // magicWait4Signal as ret causes wait4 stub to unblock call("signal", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent", syscall.SIGQUIT}, magicWait4Signal, stub.UniqueError(0xfe)), @@ -2190,7 +2190,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), // magicWait4Signal as args[4] causes this to block until simulated signal is delivered call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil), @@ -2275,7 +2275,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), - call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil), call("beforeExit", stub.ExpectArgs{}, nil, nil), call("exit", stub.ExpectArgs{0}, nil, nil), @@ -2283,7 +2283,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), // this terminates the goroutine at the call, preventing it from leaking while preserving behaviour call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD), @@ -2366,7 +2366,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), - call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil), @@ -2377,7 +2377,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil), // this terminates the goroutine at the call, preventing it from leaking while preserving behaviour @@ -2461,7 +2461,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), - call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil), @@ -2471,7 +2471,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), @@ -2599,7 +2599,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/run/current-system/sw/bin/bash")}}, nil, nil), call("start", stub.ExpectArgs{"/run/current-system/sw/bin/bash", []string{"bash", "-c", "false"}, ([]string)(nil), "/.hakurei/nonexistent"}, &os.Process{Pid: 0xbad}, nil), - call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil), @@ -2609,7 +2609,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), @@ -2741,7 +2741,7 @@ func TestInitEntrypoint(t *testing.T) { call("fatalf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{check.MustAbs("/bin/zsh")}}, nil, nil), call("start", stub.ExpectArgs{"/bin/zsh", []string{"zsh", "-c", "exec vim"}, []string{"DISPLAY=:0"}, "/.hakurei"}, &os.Process{Pid: 0xcafe}, nil), - call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil), + call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, magicWait4Signal, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), call("verbose", stub.ExpectArgs{[]any{os.ErrInvalid.Error()}}, nil, nil), @@ -2752,7 +2752,7 @@ func TestInitEntrypoint(t *testing.T) { /* wait4 */ Tracks: []stub.Expect{{Calls: []stub.Call{ - call("lockOSThread", stub.ExpectArgs{}, nil, nil), + call("lockOSThread", stub.ExpectArgs{}, magicWait4Signal, nil), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR), call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),