2 Commits

Author SHA1 Message Date
0fd357e7f6 container/init: do not suspend output
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Sandbox (push) Successful in 39s
Test / Sandbox (race detector) (push) Successful in 39s
Test / Hakurei (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Hpkg (push) Successful in 41s
Test / Flake checks (push) Successful in 1m20s
Init is not very talkative after process start even when verbose. Suspending output here is pointless and does more harm than good.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 08:11:00 +09:00
57231d4acf container/init: improve signal handling
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 3m57s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m30s
The SIGTERM signal is delivered in many other cases and can lead to strange behaviour. The unconditional resume of the logger also causes strange behaviour in the cancellation forwarding path. This change also passes through additional signals.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-23 08:02:03 +09:00
3 changed files with 117 additions and 31 deletions

View File

@@ -24,7 +24,7 @@ import (
const (
// CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGTERM
CancelSignal = SIGUSR2
)
type (

View File

@@ -341,7 +341,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
if err := k.start(cmd); err != nil {
k.fatalf(msg, "%v", err)
}
msg.Suspend()
if err := closeSetup(); err != nil {
k.printf(msg, "cannot close setup pipe: %v", err)
@@ -390,7 +389,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
// handle signals to dump withheld messages
sig := make(chan os.Signal, 2)
k.notify(sig, os.Interrupt, CancelSignal)
k.notify(sig, CancelSignal,
os.Interrupt, SIGTERM, SIGQUIT)
// closed after residualProcessTimeout has elapsed after initial process death
timeout := make(chan struct{})
@@ -399,11 +399,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
for {
select {
case s := <-sig:
if msg.Resume() {
msg.Verbosef("%s after process start", s.String())
} else {
msg.Verbosef("got %s", s.String())
}
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
msg.Verbose("forwarding context cancellation")
if err := k.signal(cmd, os.Interrupt); err != nil {
@@ -411,6 +406,16 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
continue
}
if s == SIGTERM || s == SIGQUIT {
msg.Verbosef("got %s, forwarding to initial process", s.String())
if err := k.signal(cmd, s); err != nil {
k.printf(msg, "cannot forward signal: %v", err)
}
continue
}
msg.Verbosef("got %s", s.String())
msg.BeforeExit()
k.exit(0)
@@ -422,9 +427,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
}
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
msg.Resume()
switch {
case w.wstatus.Exited():
r = w.wstatus.ExitStatus()

View File

@@ -2062,17 +2062,13 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, 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("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(10)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
call("resume", stub.ExpectArgs{}, true, nil),
call("verbosef", stub.ExpectArgs{"%s after process start", []any{"terminated"}}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- CancelSignal }, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, 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)),
call("printf", stub.ExpectArgs{"cannot forward cancellation: %v", []any{stub.UniqueError(9)}}, nil, nil),
call("resume", stub.ExpectArgs{}, false, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
call("printf", stub.ExpectArgs{"timeout exceeded waiting for lingering processes", ([]any)(nil)}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
@@ -2090,6 +2086,104 @@ func TestInitEntrypoint(t *testing.T) {
}}},
}, nil},
{"lowlastcap signaled cancel passthrough", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("getpid", stub.ExpectArgs{}, 1, nil),
call("setPtracer", stub.ExpectArgs{uintptr(0)}, 0, stub.UniqueError(8)),
call("verbosef", stub.ExpectArgs{"cannot enable ptrace protection via Yama LSM: %v", []any{stub.UniqueError(8)}}, nil, nil),
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr), &initParams{Params{
Dir: check.MustAbs("/.hakurei/nonexistent"),
Path: check.MustAbs("/run/current-system/sw/bin/bash"),
Args: []string{"bash", "-c", "false"},
ForwardCancel: true,
AdoptWaitDelay: 5 * time.Nanosecond,
Uid: 1 << 24,
Gid: 1 << 47,
Hostname: "hakurei-check",
Ops: new(Ops).Bind(check.MustAbs("/"), check.MustAbs("/"), comp.BindDevice).Proc(check.MustAbs("/proc/")),
SeccompRules: make([]seccomp.NativeRule, 0),
SeccompDisable: true,
ParentPerm: 0750,
}, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(7), nil),
call("swapVerbose", stub.ExpectArgs{false}, false, nil),
call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/uid_map", []byte("16777216 1971 1\n"), os.FileMode(0)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/setgroups", []byte("deny\n"), os.FileMode(0)}, nil, nil),
call("writeFile", stub.ExpectArgs{"/proc/self/gid_map", []byte("140737488355328 127 1\n"), os.FileMode(0)}, nil, nil),
call("setDumpable", stub.ExpectArgs{uintptr(0)}, nil, nil),
call("umask", stub.ExpectArgs{0}, 022, nil),
call("sethostname", stub.ExpectArgs{[]byte("hakurei-check")}, nil, nil),
call("lastcap", stub.ExpectArgs{}, uintptr(4), nil),
call("mount", stub.ExpectArgs{"", "/", "", uintptr(0x8c000), ""}, nil, nil),
/* begin early */
call("evalSymlinks", stub.ExpectArgs{"/"}, "/", nil),
/* end early */
call("mount", stub.ExpectArgs{"rootfs", "/proc/self/fd", "tmpfs", uintptr(6), ""}, nil, nil),
call("chdir", stub.ExpectArgs{"/proc/self/fd"}, nil, nil),
call("mkdir", stub.ExpectArgs{"sysroot", os.FileMode(0755)}, nil, nil),
call("mount", stub.ExpectArgs{"sysroot", "sysroot", "", uintptr(0xd000), ""}, nil, nil),
call("mkdir", stub.ExpectArgs{"host", os.FileMode(0755)}, nil, nil),
call("pivotRoot", stub.ExpectArgs{"/proc/self/fd", "host"}, nil, nil),
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
/* begin apply */
call("stat", stub.ExpectArgs{"/host"}, isDirFi(true), nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0700)}, nil, nil),
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot", uintptr(0x4001)}}, nil, nil),
call("bindMount", stub.ExpectArgs{"/host", "/sysroot", uintptr(0x4001), false}, nil, nil),
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &MountProcOp{Target: check.MustAbs("/proc/")}}}, nil, nil),
call("mkdirAll", stub.ExpectArgs{"/sysroot/proc", os.FileMode(0750)}, nil, nil),
call("mount", stub.ExpectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil),
/* end apply */
call("mount", stub.ExpectArgs{"host", "host", "", uintptr(0x4c000), ""}, nil, nil),
call("unmount", stub.ExpectArgs{"host", 2}, nil, nil),
call("open", stub.ExpectArgs{"/", syscall.O_DIRECTORY | syscall.O_RDONLY, uint32(0)}, 1<<35, syscall.EINTR),
call("open", stub.ExpectArgs{"/", syscall.O_DIRECTORY | syscall.O_RDONLY, uint32(0)}, 1<<35, nil),
call("chdir", stub.ExpectArgs{"/sysroot"}, nil, nil),
call("pivotRoot", stub.ExpectArgs{".", "."}, nil, nil),
call("fchdir", stub.ExpectArgs{1 << 35}, nil, nil),
call("unmount", stub.ExpectArgs{".", 2}, nil, nil),
call("chdir", stub.ExpectArgs{"/"}, nil, nil),
call("close", stub.ExpectArgs{1 << 35}, nil, nil),
call("capAmbientClearAll", stub.ExpectArgs{}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x0)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x1)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x2)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x3)}, nil, nil),
call("capBoundingSetDrop", stub.ExpectArgs{uintptr(0x4)}, nil, nil),
call("capset", stub.ExpectArgs{&capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, new([2]capData)}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"syscall filter not configured"}}, nil, nil),
call("newFile", stub.ExpectArgs{uintptr(0x3a), "extra file 0"}, (*os.File)(nil), nil),
call("newFile", stub.ExpectArgs{uintptr(0x3b), "extra file 1"}, (*os.File)(nil), nil),
call("umask", stub.ExpectArgs{022}, 0, 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("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, 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("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)),
call("printf", stub.ExpectArgs{"cannot forward signal: %v", []any{stub.UniqueError(0xfe)}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
call("printf", stub.ExpectArgs{"timeout exceeded waiting for lingering processes", []any(nil)}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{206}, nil, nil),
},
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, 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),
// 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),
}}},
}, nil},
{"lowlastcap signaled cancel", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{
/* entrypoint */
Calls: []stub.Call{
@@ -2164,11 +2258,9 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, 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("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(7)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{func(c chan<- os.Signal) { c <- os.Interrupt }, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
call("resume", stub.ExpectArgs{}, false, 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("verbosef", stub.ExpectArgs{"got %s", []any{"interrupt"}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{0}, nil, nil),
@@ -2257,11 +2349,9 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, 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("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(5)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
call("resume", stub.ExpectArgs{}, true, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
call("printf", stub.ExpectArgs{"timeout exceeded waiting for lingering processes", ([]any)(nil)}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
@@ -2352,11 +2442,9 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, 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("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(3)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
call("resume", stub.ExpectArgs{}, true, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with signal %s", []any{syscall.Signal(0x4e)}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{0xce}, nil, nil),
@@ -2490,11 +2578,9 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, 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("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(1)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
call("resume", stub.ExpectArgs{}, true, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with code %d", []any{1}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{1}, nil, nil),
@@ -2632,11 +2718,9 @@ func TestInitEntrypoint(t *testing.T) {
call("umask", stub.ExpectArgs{022}, 0, 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("suspend", stub.ExpectArgs{}, true, nil),
call("printf", stub.ExpectArgs{"cannot close setup pipe: %v", []any{stub.UniqueError(0)}}, nil, nil),
call("New", stub.ExpectArgs{}, nil, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{syscall.SIGINT, syscall.SIGTERM}}, nil, nil),
call("resume", stub.ExpectArgs{}, true, nil),
call("notify", stub.ExpectArgs{nil, []os.Signal{CancelSignal, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT}}, nil, nil),
call("verbosef", stub.ExpectArgs{"initial process exited with status %#x", []any{syscall.WaitStatus(0xfade007f)}}, nil, nil),
call("beforeExit", stub.ExpectArgs{}, nil, nil),
call("exit", stub.ExpectArgs{0xff}, nil, nil),