From 65fe09caf9bd8cbed9177022c765423885aed5be Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 28 Jul 2025 01:04:29 +0900 Subject: [PATCH] container: check cancel signal delivery This change also makes some parts of the test more robust. Signed-off-by: Ophestra --- container/container.go | 4 +- container/container_test.go | 84 +++++++++++++++++++++++++------------ container/init.go | 4 +- container/init_test.go | 28 ++++++++++++- 4 files changed, 90 insertions(+), 30 deletions(-) diff --git a/container/container.go b/container/container.go index 0b6891b..941b663 100644 --- a/container/container.go +++ b/container/container.go @@ -71,6 +71,7 @@ type ( Hostname string // Sequential container setup ops. *Ops + // Seccomp system call filter rules. SeccompRules []seccomp.NativeRule // Extra seccomp flags. @@ -79,6 +80,7 @@ type ( SeccompPresets seccomp.FilterPreset // Do not load seccomp program. SeccompDisable bool + // Permission bits of newly created parent directories. // The zero value is interpreted as 0755. ParentPerm os.FileMode @@ -121,7 +123,7 @@ func (p *Container) Start() error { } p.cmd = exec.CommandContext(ctx, MustExecutable()) - p.cmd.Args = []string{"init"} + p.cmd.Args = []string{initName} p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr p.cmd.WaitDelay = p.WaitDelay if p.Cancel != nil { diff --git a/container/container_test.go b/container/container_test.go index 81b7049..8e59e4e 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -12,7 +12,6 @@ import ( "strings" "syscall" "testing" - "time" "hakurei.app/command" "hakurei.app/container" @@ -21,7 +20,6 @@ import ( "hakurei.app/hst" "hakurei.app/internal" "hakurei.app/internal/hlog" - "hakurei.app/ldd" ) const ( @@ -92,18 +90,54 @@ func TestContainer(t *testing.T) { t.Cleanup(func() { container.SetOutput(oldOutput) }) } + t.Run("cancel", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) + + c := helperNewContainer(ctx, "block") + c.Stdout, c.Stderr = os.Stdout, os.Stderr + c.WaitDelay = helperDefaultTimeout + + ready := make(chan struct{}) + if r, w, err := os.Pipe(); err != nil { + t.Fatalf("cannot pipe: %v", err) + } else { + c.ExtraFiles = append(c.ExtraFiles, w) + go func() { + defer close(ready) + if _, err = r.Read(make([]byte, 1)); err != nil { + panic(err.Error()) + } + }() + } + + if err := c.Start(); err != nil { + hlog.PrintBaseError(err, "start:") + t.Fatalf("cannot start container: %v", err) + } else if err = c.Serve(); err != nil { + hlog.PrintBaseError(err, "serve:") + t.Errorf("cannot serve setup params: %v", err) + } + <-ready + cancel() + wantErr := context.Canceled + if err := c.Wait(); !errors.Is(err, wantErr) { + hlog.PrintBaseError(err, "wait:") + t.Fatalf("Wait: error = %v, want %v", err, wantErr) + } + }) + for i, tc := range containerTestCases { t.Run(tc.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout) defer cancel() - hostname := hostnameFromTestCase(tc.name) - c := container.New(ctx, os.Args[0], "container", strconv.Itoa(i)) - prepareHelper(c) + var libPaths []string + c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i)) c.Uid = tc.uid c.Gid = tc.gid - c.Hostname = hostname + c.Hostname = hostnameFromTestCase(tc.name) c.Stdout, c.Stderr = os.Stdout, os.Stderr + c.WaitDelay = helperDefaultTimeout *c.Ops = append(*c.Ops, *tc.ops...) c.SeccompRules = tc.rules c.SeccompFlags = tc.flags | seccomp.AllowMultiarch @@ -114,38 +148,27 @@ func TestContainer(t *testing.T) { c. Tmpfs("/tmp", 0, 0755). - Bind(os.Args[0], os.Args[0], 0). - Place("/etc/hostname", []byte(hostname)) - // in case test has cgo enabled - var libPaths []string - if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { - log.Fatalf("ldd: %v", err) - } else { - libPaths = ldd.Path(entries) - } - for _, name := range libPaths { - c.Bind(name, name, 0) - } + Place("/etc/hostname", []byte(c.Hostname)) // needs /proc to check mountinfo c.Proc("/proc") // mountinfo cannot be resolved directly by helper due to libPaths nondeterminism mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths)) - mnt = append(mnt, ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore)) - mnt = append(mnt, tc.mnt...) mnt = append(mnt, - // Tmpfs("/tmp", 0, 0755) - ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), - // Bind(os.Args[0], os.Args[0], 0) - ent(ignore, os.Args[0], "ro,nosuid,nodev,relatime", ignore, ignore, ignore), - // Place("/etc/hostname", []byte(hostname)) - ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), + ent("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), + // Bind(os.Args[0], helperInnerPath, 0) + ent(ignore, helperInnerPath, "ro,nosuid,nodev,relatime", ignore, ignore, ignore), ) for _, name := range libPaths { // Bind(name, name, 0) mnt = append(mnt, ent(ignore, name, "ro,nosuid,nodev,relatime", ignore, ignore, ignore)) } + mnt = append(mnt, tc.mnt...) mnt = append(mnt, + // Tmpfs("/tmp", 0, 0755) + ent("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore), + // Place("/etc/hostname", []byte(hostname)) + ent(ignore, "/etc/hostname", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore), // Proc("/proc") ent("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), // Place(pathWantMnt, want.Bytes()) @@ -206,6 +229,13 @@ func TestContainerString(t *testing.T) { func init() { helperCommands = append(helperCommands, func(c command.Command) { + c.Command("block", command.UsageInternal, func(args []string) error { + if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil { + return fmt.Errorf("write to sync pipe: %v", err) + } + select {} + }) + c.Command("container", command.UsageInternal, func(args []string) error { if len(args) != 1 { return syscall.EINVAL diff --git a/container/init.go b/container/init.go index 8853bd8..757af58 100644 --- a/container/init.go +++ b/container/init.go @@ -367,9 +367,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { } } +const initName = "init" + // TryArgv0 calls [Init] if the last element of argv0 is "init". func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) { - if len(os.Args) > 0 && path.Base(os.Args[0]) == "init" { + if len(os.Args) > 0 && path.Base(os.Args[0]) == initName { msg = v Init(prepare, setVerbose) msg.BeforeExit() diff --git a/container/init_test.go b/container/init_test.go index ad35b4c..6e99cc0 100644 --- a/container/init_test.go +++ b/container/init_test.go @@ -1,18 +1,24 @@ package container_test import ( + "context" "log" "os" "testing" + "time" "hakurei.app/command" "hakurei.app/container" "hakurei.app/internal" "hakurei.app/internal/hlog" + "hakurei.app/ldd" ) const ( envDoCheck = "HAKUREI_TEST_DO_CHECK" + + helperDefaultTimeout = 5 * time.Second + helperInnerPath = "/usr/bin/helper" ) var helperCommands []func(c command.Command) @@ -40,4 +46,24 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func prepareHelper(c *container.Container) { c.Env = append(c.Env, envDoCheck+"=1") } +func helperNewContainerLibPaths(ctx context.Context, libPaths *[]string, args ...string) (c *container.Container) { + c = container.New(ctx, helperInnerPath, args...) + c.Env = append(c.Env, envDoCheck+"=1") + c.Bind(os.Args[0], helperInnerPath, 0) + + // in case test has cgo enabled + if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { + log.Fatalf("ldd: %v", err) + } else { + *libPaths = ldd.Path(entries) + } + for _, name := range *libPaths { + c.Bind(name, name, 0) + } + + return +} + +func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) { + return helperNewContainerLibPaths(ctx, new([]string), args...) +}