diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 1671902..58e8ace 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -17,17 +17,30 @@ import ( "hakurei.app/internal" "hakurei.app/internal/app" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" "hakurei.app/system" "hakurei.app/system/dbus" ) -func buildCommand(ctx context.Context, out io.Writer) command.Command { +func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { var ( flagVerbose bool flagJSON bool ) - c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }). + c := command.New(out, log.Printf, "hakurei", func([]string) error { + msg.SwapVerbose(flagVerbose) + + if early.yamaLSM != nil { + msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM) + // not fatal + } + + if early.dumpable != nil { + log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable) + // not fatal + } + + return nil + }). Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity"). Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable") @@ -39,10 +52,10 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { } // config extraArgs... - config := tryPath(args[0]) + config := tryPath(msg, args[0]) config.Args = append(config.Args, args[1:]...) - app.Main(ctx, config) + app.Main(ctx, msg, config) panic("unreachable") }) @@ -78,9 +91,9 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { passwd *user.User passwdOnce sync.Once passwdFunc = func() { - us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity)) + us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustIDMsg(msg), flagIdentity)) if u, err := user.LookupId(us); err != nil { - hlog.Verbosef("cannot look up uid %s", us) + msg.Verbosef("cannot look up uid %s", us) passwd = &user.User{ Uid: us, Gid: us, @@ -162,7 +175,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { } } - app.Main(ctx, config) + app.Main(ctx, msg, config) panic("unreachable") }). Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), @@ -198,13 +211,13 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { c.NewCommand("show", "Show live or local app configuration", func(args []string) error { switch len(args) { case 0: // system - printShowSystem(os.Stdout, flagShort, flagJSON) + printShowSystem(msg, os.Stdout, flagShort, flagJSON) case 1: // instance name := args[0] - config, entry := tryShort(name) + config, entry := tryShort(msg, name) if config == nil { - config = tryPath(name) + config = tryPath(msg, name) } printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) @@ -219,8 +232,8 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { var flagShort bool c.NewCommand("ps", "List active instances", func(args []string) error { var sc hst.Paths - app.CopyPaths(&sc, new(app.Hsu).MustID()) - printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON) + app.CopyPaths(msg, &sc, new(app.Hsu).MustID()) + printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON) return errSuccess }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") } diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go index 8ce9a23..dd16b25 100644 --- a/cmd/hakurei/command_test.go +++ b/cmd/hakurei/command_test.go @@ -7,6 +7,7 @@ import ( "testing" "hakurei.app/command" + "hakurei.app/container" ) func TestHelp(t *testing.T) { @@ -68,7 +69,7 @@ Flags: for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { out := new(bytes.Buffer) - c := buildCommand(t.Context(), out) + c := buildCommand(t.Context(), container.NewMsg(nil), new(earlyHardeningErrs), out) if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) { t.Errorf("Parse: error = %v; want %v", err, command.ErrHelp) diff --git a/cmd/hakurei/main.go b/cmd/hakurei/main.go index 2b9193b..5cc8224 100644 --- a/cmd/hakurei/main.go +++ b/cmd/hakurei/main.go @@ -13,8 +13,6 @@ import ( "syscall" "hakurei.app/container" - "hakurei.app/internal" - "hakurei.app/internal/hlog" ) var ( @@ -24,20 +22,20 @@ var ( license string ) -func init() { hlog.Prepare("hakurei") } +// earlyHardeningErrs are errors collected while setting up early hardening feature. +type earlyHardeningErrs struct{ yamaLSM, dumpable error } func main() { // early init path, skips root check and duplicate PR_SET_DUMPABLE - container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) + container.TryArgv0(nil) - if err := container.SetPtracer(0); err != nil { - hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err) - // not fatal: this program runs as the privileged user - } + log.SetPrefix("hakurei: ") + log.SetFlags(0) + msg := container.NewMsg(log.Default()) - if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil { - log.Printf("cannot set SUID_DUMP_DISABLE: %s", err) - // not fatal: this program runs as the privileged user + early := earlyHardeningErrs{ + yamaLSM: container.SetPtracer(0), + dumpable: container.SetDumpable(container.SUID_DUMP_DISABLE), } if os.Geteuid() == 0 { @@ -48,10 +46,10 @@ func main() { syscall.SIGINT, syscall.SIGTERM) defer stop() // unreachable - buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) { - hlog.Verbosef("command returned %v", err) + buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) { + msg.Verbosef("command returned %v", err) if errors.Is(err, errSuccess) { - hlog.BeforeExit() + msg.BeforeExit() os.Exit(0) } // this catches faulty command handlers that fail to return before this point diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index 897382e..3825ca1 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -10,20 +10,20 @@ import ( "strings" "syscall" + "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" ) -func tryPath(name string) (config *hst.Config) { +func tryPath(msg container.Msg, name string) (config *hst.Config) { var r io.Reader config = new(hst.Config) if name != "-" { - r = tryFd(name) + r = tryFd(msg, name) if r == nil { - hlog.Verbose("load configuration from file") + msg.Verbose("load configuration from file") if f, err := os.Open(name); err != nil { log.Fatalf("cannot access configuration file %q: %s", name, err) @@ -49,14 +49,14 @@ func tryPath(name string) (config *hst.Config) { return } -func tryFd(name string) io.ReadCloser { +func tryFd(msg container.Msg, name string) io.ReadCloser { if v, err := strconv.Atoi(name); err != nil { if !errors.Is(err, strconv.ErrSyntax) { - hlog.Verbosef("name cannot be interpreted as int64: %v", err) + msg.Verbosef("name cannot be interpreted as int64: %v", err) } return nil } else { - hlog.Verbosef("trying config stream from %d", v) + msg.Verbosef("trying config stream from %d", v) fd := uintptr(v) if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 { if errors.Is(errno, syscall.EBADF) { @@ -68,7 +68,7 @@ func tryFd(name string) io.ReadCloser { } } -func tryShort(name string) (config *hst.Config, entry *state.State) { +func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.State) { likePrefix := false if len(name) <= 32 { likePrefix = true @@ -86,11 +86,11 @@ func tryShort(name string) (config *hst.Config, entry *state.State) { // try to match from state store if likePrefix && len(name) >= 8 { - hlog.Verbose("argument looks like prefix") + msg.Verbose("argument looks like prefix") var sc hst.Paths - app.CopyPaths(&sc, new(app.Hsu).MustID()) - s := state.NewMulti(sc.RunDirPath.String()) + app.CopyPaths(msg, &sc, new(app.Hsu).MustID()) + s := state.NewMulti(msg, sc.RunDirPath.String()) if entries, err := state.Join(s); err != nil { log.Printf("cannot join store: %v", err) // drop to fetch from file @@ -104,7 +104,7 @@ func tryShort(name string) (config *hst.Config, entry *state.State) { break } - hlog.Verbosef("instance %s skipped", v) + msg.Verbosef("instance %s skipped", v) } } } diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 938517b..2c57538 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -11,18 +11,19 @@ import ( "text/tabwriter" "time" + "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" "hakurei.app/system/dbus" ) -func printShowSystem(output io.Writer, short, flagJSON bool) { +func printShowSystem(msg container.Msg, output io.Writer, short, flagJSON bool) { t := newPrinter(output) defer t.MustFlush() info := &hst.Info{User: new(app.Hsu).MustID()} - app.CopyPaths(&info.Paths, info.User) + app.CopyPaths(msg, &info.Paths, info.User) if flagJSON { printJSON(output, short, info) diff --git a/cmd/hpkg/main.go b/cmd/hpkg/main.go index 2ae646c..03247aa 100644 --- a/cmd/hpkg/main.go +++ b/cmd/hpkg/main.go @@ -13,22 +13,21 @@ import ( "hakurei.app/command" "hakurei.app/container" "hakurei.app/hst" - "hakurei.app/internal" - "hakurei.app/internal/hlog" ) var ( errSuccess = errors.New("success") ) -func init() { - hlog.Prepare("hpkg") +func main() { + log.SetPrefix("hpkg: ") + log.SetFlags(0) + msg := container.NewMsg(log.Default()) + if err := os.Setenv("SHELL", pathShell.String()); err != nil { log.Fatalf("cannot set $SHELL: %v", err) } -} -func main() { if os.Geteuid() == 0 { log.Fatal("this program must not run as root") } @@ -41,7 +40,7 @@ func main() { flagVerbose bool flagDropShell bool ) - c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }). + c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { msg.SwapVerbose(flagVerbose); return nil }). Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console"). Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action") @@ -90,12 +89,12 @@ func main() { } cleanup := func() { // should be faster than a native implementation - mustRun(chmod, "-R", "+w", workDir.String()) - mustRun(rm, "-rf", workDir.String()) + mustRun(msg, chmod, "-R", "+w", workDir.String()) + mustRun(msg, rm, "-rf", workDir.String()) } beforeRunFail.Store(&cleanup) - mustRun(tar, "-C", workDir.String(), "-xf", pkgPath) + mustRun(msg, tar, "-C", workDir.String(), "-xf", pkgPath) /* Parse bundle and app metadata, do pre-install checks. @@ -148,10 +147,10 @@ func main() { } // sec: should compare version string - hlog.Verbosef("installing application %q version %q over local %q", + msg.Verbosef("installing application %q version %q over local %q", bundle.ID, bundle.Version, a.Version) } else { - hlog.Verbosef("application %q clean installation", bundle.ID) + msg.Verbosef("application %q clean installation", bundle.ID) // sec: should install credentials } @@ -159,7 +158,7 @@ func main() { Setup steps for files owned by the target user. */ - withCacheDir(ctx, "install", []string{ + withCacheDir(ctx, msg, "install", []string{ // export inner bundle path in the environment "export BUNDLE=" + hst.Tmp + "/bundle", // replace inner /etc @@ -181,7 +180,7 @@ func main() { }, workDir, bundle, pathSet, flagDropShell, cleanup) if bundle.GPU { - withCacheDir(ctx, "mesa-wrappers", []string{ + withCacheDir(ctx, msg, "mesa-wrappers", []string{ // link nixGL mesa wrappers "mkdir -p nix/.nixGL", "ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL", @@ -193,7 +192,7 @@ func main() { Activate home-manager generation. */ - withNixDaemon(ctx, "activate", []string{ + withNixDaemon(ctx, msg, "activate", []string{ // clean up broken links "mkdir -p .local/state/{nix,home-manager}", "chmod -R +w .local/state/{nix,home-manager}", @@ -261,7 +260,7 @@ func main() { */ if a.GPU && flagAutoDrivers { - withNixDaemon(ctx, "nix-gl", []string{ + withNixDaemon(ctx, msg, "nix-gl", []string{ "mkdir -p /nix/.nixGL/auto", "rm -rf /nix/.nixGL/auto", "export NIXPKGS_ALLOW_UNFREE=1", @@ -316,7 +315,7 @@ func main() { Spawn app. */ - mustRunApp(ctx, config, func() {}) + mustRunApp(ctx, msg, config, func() {}) return errSuccess }). Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build"). @@ -324,9 +323,9 @@ func main() { } c.MustParse(os.Args[1:], func(err error) { - hlog.Verbosef("command returned %v", err) + msg.Verbosef("command returned %v", err) if errors.Is(err, errSuccess) { - hlog.BeforeExit() + msg.BeforeExit() os.Exit(0) } }) diff --git a/cmd/hpkg/paths.go b/cmd/hpkg/paths.go index 0964a55..ee4b7e7 100644 --- a/cmd/hpkg/paths.go +++ b/cmd/hpkg/paths.go @@ -9,7 +9,6 @@ import ( "hakurei.app/container" "hakurei.app/hst" - "hakurei.app/internal/hlog" ) const bash = "bash" @@ -51,8 +50,8 @@ func lookPath(file string) string { var beforeRunFail = new(atomic.Pointer[func()]) -func mustRun(name string, arg ...string) { - hlog.Verbosef("spawning process: %q %q", name, arg) +func mustRun(msg container.Msg, name string, arg ...string) { + msg.Verbosef("spawning process: %q %q", name, arg) cmd := exec.Command(name, arg...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr if err := cmd.Run(); err != nil { diff --git a/cmd/hpkg/proc.go b/cmd/hpkg/proc.go index d374e13..1bbf5b4 100644 --- a/cmd/hpkg/proc.go +++ b/cmd/hpkg/proc.go @@ -9,14 +9,14 @@ import ( "os" "os/exec" + "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal" - "hakurei.app/internal/hlog" ) var hakureiPath = internal.MustHakureiPath() -func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) { +func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, beforeFail func()) { var ( cmd *exec.Cmd st io.WriteCloser @@ -26,10 +26,10 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) { beforeFail() log.Fatalf("cannot pipe: %v", err) } else { - if hlog.Load() { - cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3") + if msg.IsVerbose() { + cmd = exec.CommandContext(ctx, hakureiPath.String(), "-v", "app", "3") } else { - cmd = exec.CommandContext(ctx, hakureiPath, "app", "3") + cmd = exec.CommandContext(ctx, hakureiPath.String(), "app", "3") } cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.ExtraFiles = []*os.File{r} @@ -51,7 +51,8 @@ func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) { var exitError *exec.ExitError if errors.As(err, &exitError) { beforeFail() - internal.Exit(exitError.ExitCode()) + msg.BeforeExit() + os.Exit(exitError.ExitCode()) } else { beforeFail() log.Fatalf("cannot wait: %v", err) diff --git a/cmd/hpkg/with.go b/cmd/hpkg/with.go index 1895a06..578d525 100644 --- a/cmd/hpkg/with.go +++ b/cmd/hpkg/with.go @@ -2,20 +2,21 @@ package main import ( "context" + "os" "strings" "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/hst" - "hakurei.app/internal" ) func withNixDaemon( ctx context.Context, + msg container.Msg, action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config, app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), ) { - mustRunAppDropShell(ctx, updateConfig(&hst.Config{ + mustRunAppDropShell(ctx, msg, updateConfig(&hst.Config{ ID: app.ID, Path: pathShell, @@ -61,9 +62,10 @@ func withNixDaemon( func withCacheDir( ctx context.Context, + msg container.Msg, action string, command []string, workDir *container.Absolute, app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { - mustRunAppDropShell(ctx, &hst.Config{ + mustRunAppDropShell(ctx, msg, &hst.Config{ ID: app.ID, Path: pathShell, @@ -97,12 +99,13 @@ func withCacheDir( }, dropShell, beforeFail) } -func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) { +func mustRunAppDropShell(ctx context.Context, msg container.Msg, config *hst.Config, dropShell bool, beforeFail func()) { if dropShell { config.Args = []string{bash, "-l"} - mustRunApp(ctx, config, beforeFail) + mustRunApp(ctx, msg, config, beforeFail) beforeFail() - internal.Exit(0) + msg.BeforeExit() + os.Exit(0) } - mustRunApp(ctx, config, beforeFail) + mustRunApp(ctx, msg, config, beforeFail) } diff --git a/container/autoroot.go b/container/autoroot.go index c37e49b..ee8bcbd 100644 --- a/container/autoroot.go +++ b/container/autoroot.go @@ -34,7 +34,7 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error { r.resolved = make([]*BindMountOp, 0, len(d)) for _, ent := range d { name := ent.Name() - if IsAutoRootBindable(name) { + if IsAutoRootBindable(state, name) { // careful: the Valid method is skipped, make sure this is always valid op := &BindMountOp{ Source: r.Host.Append(name), @@ -78,7 +78,7 @@ func (r *AutoRootOp) String() string { } // IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot. -func IsAutoRootBindable(name string) bool { +func IsAutoRootBindable(msg Msg, name string) bool { switch name { case "proc", "dev", "tmp", "mnt", "etc": diff --git a/container/autoroot_test.go b/container/autoroot_test.go index 3c90431..fe66222 100644 --- a/container/autoroot_test.go +++ b/container/autoroot_test.go @@ -180,19 +180,26 @@ func TestIsAutoRootBindable(t *testing.T) { testCases := []struct { name string want bool + log bool }{ - {"proc", false}, - {"dev", false}, - {"tmp", false}, - {"mnt", false}, - {"etc", false}, - {"", false}, + {"proc", false, false}, + {"dev", false, false}, + {"tmp", false, false}, + {"mnt", false, false}, + {"etc", false, false}, + {"", false, true}, - {"var", true}, + {"var", true, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - if got := IsAutoRootBindable(tc.name); got != tc.want { + var msg Msg + if tc.log { + msg = &kstub{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), + }})} + } + if got := IsAutoRootBindable(msg, tc.name); got != tc.want { t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want) } }) diff --git a/container/container.go b/container/container.go index f49faa8..c19c2e7 100644 --- a/container/container.go +++ b/container/container.go @@ -49,6 +49,7 @@ type ( cmd *exec.Cmd ctx context.Context + msg Msg Params } @@ -162,10 +163,10 @@ func (p *Container) Start() error { // map to overflow id to work around ownership checks if p.Uid < 1 { - p.Uid = OverflowUid() + p.Uid = OverflowUid(p.msg) } if p.Gid < 1 { - p.Gid = OverflowGid() + p.Gid = OverflowGid(p.msg) } if !p.RetainSession { @@ -263,19 +264,19 @@ func (p *Container) Start() error { } return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false} } else { - msg.Verbosef("landlock abi version %d", abi) + p.msg.Verbosef("landlock abi version %d", abi) } if rulesetFd, err := rulesetAttr.Create(0); err != nil { return &StartError{true, "create landlock ruleset", err, false, false} } else { - msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) + p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr) if err = LandlockRestrictSelf(rulesetFd, 0); err != nil { _ = Close(rulesetFd) return &StartError{true, "enforce landlock ruleset", err, false, false} } if err = Close(rulesetFd); err != nil { - msg.Verbosef("cannot close landlock ruleset: %v", err) + p.msg.Verbosef("cannot close landlock ruleset: %v", err) // not fatal } } @@ -283,7 +284,7 @@ func (p *Container) Start() error { landlockOut: } - msg.Verbose("starting container init") + p.msg.Verbose("starting container init") if err := p.cmd.Start(); err != nil { return &StartError{false, "start container init", err, false, true} } @@ -325,7 +326,7 @@ func (p *Container) Serve() error { Getuid(), Getgid(), len(p.ExtraFiles), - msg.IsVerbose(), + p.msg.IsVerbose(), }, ) if err != nil { @@ -392,17 +393,21 @@ func (p *Container) ProcessState() *os.ProcessState { } // New returns the address to a new instance of [Container] that requires further initialisation before use. -func New(ctx context.Context) *Container { - p := &Container{ctx: ctx, Params: Params{Ops: new(Ops)}} +func New(ctx context.Context, msg Msg) *Container { + if msg == nil { + msg = NewMsg(nil) + } + + p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}} c, cancel := context.WithCancel(ctx) p.cancel = cancel - p.cmd = exec.CommandContext(c, MustExecutable()) + p.cmd = exec.CommandContext(c, MustExecutable(msg)) return p } // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. -func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container { - z := New(ctx) +func NewCommand(ctx context.Context, msg Msg, pathname *Absolute, name string, args ...string) *Container { + z := New(ctx, msg) z.Path = pathname z.Args = append([]string{name}, args...) return z diff --git a/container/container_test.go b/container/container_test.go index 0b6cdf5..2235648 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -23,8 +23,6 @@ import ( "hakurei.app/container/seccomp" "hakurei.app/container/vfs" "hakurei.app/hst" - "hakurei.app/internal" - "hakurei.app/internal/hlog" "hakurei.app/ldd" ) @@ -351,8 +349,6 @@ var containerTestCases = []struct { } func TestContainer(t *testing.T) { - replaceOutput(t) - t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { wantErr := context.Canceled wantExitCode := 0 @@ -547,7 +543,8 @@ func testContainerCancel( } func TestContainerString(t *testing.T) { - c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env") + msg := container.NewMsg(nil) + c := container.NewCommand(t.Context(), msg, container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env") c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompRules = seccomp.Preset( seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY, @@ -689,7 +686,7 @@ var ( var helperCommands []func(c command.Command) func TestMain(m *testing.M) { - container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) + container.TryArgv0(nil) if os.Getenv(envDoCheck) == "1" { c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error { @@ -712,12 +709,13 @@ func TestMain(m *testing.M) { } func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) { - c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...) + msg := container.NewMsg(nil) + c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...) c.Env = append(c.Env, envDoCheck+"=1") c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0) // in case test has cgo enabled - if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil { + if entries, err := ldd.Exec(ctx, msg, os.Args[0]); err != nil { log.Fatalf("ldd: %v", err) } else { *libPaths = ldd.Path(entries) diff --git a/container/dispatcher.go b/container/dispatcher.go index 929421b..65881cc 100644 --- a/container/dispatcher.go +++ b/container/dispatcher.go @@ -3,7 +3,6 @@ package container import ( "io" "io/fs" - "log" "os" "os/exec" "os/signal" @@ -38,7 +37,7 @@ type syscallDispatcher interface { setNoNewPrivs() error // lastcap provides [LastCap]. - lastcap() uintptr + lastcap(msg Msg) uintptr // capset provides capset. capset(hdrp *capHeader, datap *[2]capData) error // capBoundingSetDrop provides capBoundingSetDrop. @@ -53,9 +52,9 @@ type syscallDispatcher interface { receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) // bindMount provides procPaths.bindMount. - bindMount(source, target string, flags uintptr) error + bindMount(msg Msg, source, target string, flags uintptr) error // remount provides procPaths.remount. - remount(target string, flags uintptr) error + remount(msg Msg, target string, flags uintptr) error // mountTmpfs provides mountTmpfs. mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error // ensureFile provides ensureFile. @@ -122,22 +121,12 @@ type syscallDispatcher interface { // wait4 provides syscall.Wait4 wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) - // printf provides [log.Printf]. - printf(format string, v ...any) - // fatal provides [log.Fatal] - fatal(v ...any) - // fatalf provides [log.Fatalf] - fatalf(format string, v ...any) - // verbose provides [Msg.Verbose]. - verbose(v ...any) - // verbosef provides [Msg.Verbosef]. - verbosef(format string, v ...any) - // suspend provides [Msg.Suspend]. - suspend() - // resume provides [Msg.Resume]. - resume() bool - // beforeExit provides [Msg.BeforeExit]. - beforeExit() + // printf provides the Printf method of [log.Logger]. + printf(msg Msg, format string, v ...any) + // fatal provides the Fatal method of [log.Logger] + fatal(msg Msg, v ...any) + // fatalf provides the Fatalf method of [log.Logger] + fatalf(msg Msg, format string, v ...any) } // direct implements syscallDispatcher on the current kernel. @@ -151,7 +140,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) } func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) } func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } -func (direct) lastcap() uintptr { return LastCap() } +func (direct) lastcap(msg Msg) uintptr { return LastCap(msg) } func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) } func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) } func (direct) capAmbientClearAll() error { return capAmbientClearAll() } @@ -161,11 +150,11 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) { return Receive(key, e, fdp) } -func (direct) bindMount(source, target string, flags uintptr) error { - return hostProc.bindMount(source, target, flags) +func (direct) bindMount(msg Msg, source, target string, flags uintptr) error { + return hostProc.bindMount(msg, source, target, flags) } -func (direct) remount(target string, flags uintptr) error { - return hostProc.remount(target, flags) +func (direct) remount(msg Msg, target string, flags uintptr) error { + return hostProc.remount(msg, target, flags) } func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { return mountTmpfs(k, fsname, target, flags, size, perm) @@ -232,11 +221,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s return syscall.Wait4(pid, wstatus, options, rusage) } -func (direct) printf(format string, v ...any) { log.Printf(format, v...) } -func (direct) fatal(v ...any) { log.Fatal(v...) } -func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } -func (direct) verbose(v ...any) { msg.Verbose(v...) } -func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) } -func (direct) suspend() { msg.Suspend() } -func (direct) resume() bool { return msg.Resume() } -func (direct) beforeExit() { msg.BeforeExit() } +func (direct) printf(msg Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) } +func (direct) fatal(msg Msg, v ...any) { msg.GetLogger().Fatal(v...) } +func (direct) fatalf(msg Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) } diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go index 7e14130..2a2685f 100644 --- a/container/dispatcher_test.go +++ b/container/dispatcher_test.go @@ -2,8 +2,10 @@ package container import ( "bytes" + "fmt" "io" "io/fs" + "log" "os" "os/exec" "reflect" @@ -136,7 +138,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call { type simpleTestCase struct { name string - f func(k syscallDispatcher) error + f func(k *kstub) error want stub.Expect wantErr error } @@ -185,11 +187,11 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { t.Run(tc.name, func(t *testing.T) { t.Helper() - state := &setupState{Params: tc.params} k := &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} }, stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)}, )} + state := &setupState{Params: tc.params, Msg: k} defer stub.HandleExit(t) errEarly := tc.op.early(state, k) k.Expects(stub.CallSeparator) @@ -327,7 +329,11 @@ func (k *kstub) setDumpable(dumpable uintptr) error { } func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err } -func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) } +func (k *kstub) lastcap(msg Msg) uintptr { + k.Helper() + k.checkMsg(msg) + return k.Expects("lastcap").Ret.(uintptr) +} func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error { k.Helper() @@ -403,16 +409,18 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error return } -func (k *kstub) bindMount(source, target string, flags uintptr) error { +func (k *kstub) bindMount(msg Msg, source, target string, flags uintptr) error { k.Helper() + k.checkMsg(msg) return k.Expects("bindMount").Error( stub.CheckArg(k.Stub, "source", source, 0), stub.CheckArg(k.Stub, "target", target, 1), stub.CheckArg(k.Stub, "flags", flags, 2)) } -func (k *kstub) remount(target string, flags uintptr) error { +func (k *kstub) remount(msg Msg, target string, flags uintptr) error { k.Helper() + k.checkMsg(msg) return k.Expects("remount").Error( stub.CheckArg(k.Stub, "target", target, 0), stub.CheckArg(k.Stub, "flags", flags, 1)) @@ -694,7 +702,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage return } -func (k *kstub) printf(format string, v ...any) { +func (k *kstub) printf(_ Msg, format string, v ...any) { k.Helper() if k.Expects("printf").Error( stub.CheckArg(k.Stub, "format", format, 0), @@ -703,7 +711,7 @@ func (k *kstub) printf(format string, v ...any) { } } -func (k *kstub) fatal(v ...any) { +func (k *kstub) fatal(_ Msg, v ...any) { k.Helper() if k.Expects("fatal").Error( stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { @@ -712,7 +720,7 @@ func (k *kstub) fatal(v ...any) { panic(stub.PanicExit) } -func (k *kstub) fatalf(format string, v ...any) { +func (k *kstub) fatalf(_ Msg, format string, v ...any) { k.Helper() if k.Expects("fatalf").Error( stub.CheckArg(k.Stub, "format", format, 0), @@ -722,7 +730,35 @@ func (k *kstub) fatalf(format string, v ...any) { panic(stub.PanicExit) } -func (k *kstub) verbose(v ...any) { +func (k *kstub) checkMsg(msg Msg) { + k.Helper() + var target *kstub + + if state, ok := msg.(*setupState); ok { + target = state.Msg.(*kstub) + } else { + target = msg.(*kstub) + } + + if k != target { + panic(fmt.Sprintf("unexpected Msg: %#v", msg)) + } +} + +func (k *kstub) GetLogger() *log.Logger { panic("unreachable") } +func (k *kstub) IsVerbose() bool { panic("unreachable") } + +func (k *kstub) SwapVerbose(verbose bool) bool { + k.Helper() + expect := k.Expects("swapVerbose") + if expect.Error( + stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil { + k.FailNow() + } + return expect.Ret.(bool) +} + +func (k *kstub) Verbose(v ...any) { k.Helper() if k.Expects("verbose").Error( stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { @@ -730,7 +766,7 @@ func (k *kstub) verbose(v ...any) { } } -func (k *kstub) verbosef(format string, v ...any) { +func (k *kstub) Verbosef(format string, v ...any) { k.Helper() if k.Expects("verbosef").Error( stub.CheckArg(k.Stub, "format", format, 0), @@ -739,6 +775,6 @@ func (k *kstub) verbosef(format string, v ...any) { } } -func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") } -func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) } -func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") } +func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) } +func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) } +func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") } diff --git a/container/executable.go b/container/executable.go index d19d56f..9b6d4f6 100644 --- a/container/executable.go +++ b/container/executable.go @@ -1,7 +1,6 @@ package container import ( - "log" "os" "sync" ) @@ -11,16 +10,16 @@ var ( executableOnce sync.Once ) -func copyExecutable() { +func copyExecutable(msg Msg) { if name, err := os.Executable(); err != nil { msg.BeforeExit() - log.Fatalf("cannot read executable path: %v", err) + msg.GetLogger().Fatalf("cannot read executable path: %v", err) } else { executable = name } } -func MustExecutable() string { - executableOnce.Do(copyExecutable) +func MustExecutable(msg Msg) string { + executableOnce.Do(func() { copyExecutable(msg) }) return executable } diff --git a/container/executable_test.go b/container/executable_test.go index 2a44017..5608a8a 100644 --- a/container/executable_test.go +++ b/container/executable_test.go @@ -9,7 +9,7 @@ import ( func TestExecutable(t *testing.T) { for i := 0; i < 16; i++ { - if got := container.MustExecutable(); got != os.Args[0] { + if got := container.MustExecutable(container.NewMsg(nil)); got != os.Args[0] { t.Errorf("MustExecutable: %q, want %q", got, os.Args[0]) } diff --git a/container/init.go b/container/init.go index 73478b5..e65472e 100644 --- a/container/init.go +++ b/container/init.go @@ -3,6 +3,7 @@ package container import ( "errors" "fmt" + "log" "os" "os/exec" "path" @@ -59,6 +60,7 @@ type ( setupState struct { nonrepeatable uintptr *Params + Msg } ) @@ -91,20 +93,23 @@ type initParams struct { Verbose bool } -func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) { - initEntrypoint(direct{}, prepareLogger, setVerbose) +// Init is called by [TryArgv0] if the current process is the container init. +func Init(msg Msg) { + if msg == nil { + panic("attempting to call initEntrypoint with nil msg") + } + initEntrypoint(direct{}, msg) } -func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) { +func initEntrypoint(k syscallDispatcher, msg Msg) { k.lockOSThread() - prepareLogger("init") if k.getpid() != 1 { - k.fatal("this process must run as pid 1") + k.fatal(msg, "this process must run as pid 1") } if err := k.setPtracer(0); err != nil { - k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err) + msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err) // not fatal: this program has no additional privileges at initial program start } @@ -116,65 +121,65 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV ) if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil { if errors.Is(err, EBADF) { - k.fatal("invalid setup descriptor") + k.fatal(msg, "invalid setup descriptor") } if errors.Is(err, ErrReceiveEnv) { - k.fatal("HAKUREI_SETUP not set") + k.fatal(msg, "HAKUREI_SETUP not set") } - k.fatalf("cannot decode init setup payload: %v", err) + k.fatalf(msg, "cannot decode init setup payload: %v", err) } else { if params.Ops == nil { - k.fatal("invalid setup parameters") + k.fatal(msg, "invalid setup parameters") } if params.ParentPerm == 0 { params.ParentPerm = 0755 } - setVerbose(params.Verbose) - k.verbose("received setup parameters") + msg.SwapVerbose(params.Verbose) + msg.Verbose("received setup parameters") closeSetup = f offsetSetup = int(setupFd + 1) } // write uid/gid map here so parent does not need to set dumpable if err := k.setDumpable(SUID_DUMP_USER); err != nil { - k.fatalf("cannot set SUID_DUMP_USER: %v", err) + k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err) } if err := k.writeFile(FHSProc+"self/uid_map", append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), 0); err != nil { - k.fatalf("%v", err) + k.fatalf(msg, "%v", err) } if err := k.writeFile(FHSProc+"self/setgroups", []byte("deny\n"), 0); err != nil && !os.IsNotExist(err) { - k.fatalf("%v", err) + k.fatalf(msg, "%v", err) } if err := k.writeFile(FHSProc+"self/gid_map", append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), 0); err != nil { - k.fatalf("%v", err) + k.fatalf(msg, "%v", err) } if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil { - k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err) + k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err) } oldmask := k.umask(0) if params.Hostname != "" { if err := k.sethostname([]byte(params.Hostname)); err != nil { - k.fatalf("cannot set hostname: %v", err) + k.fatalf(msg, "cannot set hostname: %v", err) } } // cache sysctl before pivot_root - lastcap := k.lastcap() + lastcap := k.lastcap(msg) if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil { - k.fatalf("cannot make / rslave: %v", err) + k.fatalf(msg, "cannot make / rslave: %v", err) } - state := &setupState{Params: ¶ms.Params} + state := &setupState{Params: ¶ms.Params, Msg: msg} /* early is called right before pivot_root into intermediate root; this step is mostly for gathering information that would otherwise be difficult to obtain @@ -182,41 +187,41 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV the state of the mount namespace */ for i, op := range *params.Ops { if op == nil || !op.Valid() { - k.fatalf("invalid op at index %d", i) + k.fatalf(msg, "invalid op at index %d", i) } if err := op.early(state, k); err != nil { if m, ok := messageFromError(err); ok { - k.fatal(m) + k.fatal(msg, m) } else { - k.fatalf("cannot prepare op at index %d: %v", i, err) + k.fatalf(msg, "cannot prepare op at index %d: %v", i, err) } } } if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil { - k.fatalf("cannot mount intermediate root: %v", err) + k.fatalf(msg, "cannot mount intermediate root: %v", err) } if err := k.chdir(intermediateHostPath); err != nil { - k.fatalf("cannot enter intermediate host path: %v", err) + k.fatalf(msg, "cannot enter intermediate host path: %v", err) } if err := k.mkdir(sysrootDir, 0755); err != nil { - k.fatalf("%v", err) + k.fatalf(msg, "%v", err) } if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil { - k.fatalf("cannot bind sysroot: %v", err) + k.fatalf(msg, "cannot bind sysroot: %v", err) } if err := k.mkdir(hostDir, 0755); err != nil { - k.fatalf("%v", err) + k.fatalf(msg, "%v", err) } // pivot_root uncovers intermediateHostPath in hostDir if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil { - k.fatalf("cannot pivot into intermediate root: %v", err) + k.fatalf(msg, "cannot pivot into intermediate root: %v", err) } if err := k.chdir(FHSRoot); err != nil { - k.fatalf("cannot enter intermediate root: %v", err) + k.fatalf(msg, "cannot enter intermediate root: %v", err) } /* apply is called right after pivot_root and entering the new root; @@ -226,23 +231,23 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV for i, op := range *params.Ops { // ops already checked during early setup if prefix, ok := op.prefix(); ok { - k.verbosef("%s %s", prefix, op) + msg.Verbosef("%s %s", prefix, op) } if err := op.apply(state, k); err != nil { if m, ok := messageFromError(err); ok { - k.fatal(m) + k.fatal(msg, m) } else { - k.fatalf("cannot apply op at index %d: %v", i, err) + k.fatalf(msg, "cannot apply op at index %d: %v", i, err) } } } // setup requiring host root complete at this point if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil { - k.fatalf("cannot make host root rprivate: %v", err) + k.fatalf(msg, "cannot make host root rprivate: %v", err) } if err := k.unmount(hostDir, MNT_DETACH); err != nil { - k.fatalf("cannot unmount host root: %v", err) + k.fatalf(msg, "cannot unmount host root: %v", err) } { @@ -251,39 +256,39 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0) return }); err != nil { - k.fatalf("cannot open intermediate root: %v", err) + k.fatalf(msg, "cannot open intermediate root: %v", err) } if err := k.chdir(sysrootPath); err != nil { - k.fatalf("cannot enter sysroot: %v", err) + k.fatalf(msg, "cannot enter sysroot: %v", err) } if err := k.pivotRoot(".", "."); err != nil { - k.fatalf("cannot pivot into sysroot: %v", err) + k.fatalf(msg, "cannot pivot into sysroot: %v", err) } if err := k.fchdir(fd); err != nil { - k.fatalf("cannot re-enter intermediate root: %v", err) + k.fatalf(msg, "cannot re-enter intermediate root: %v", err) } if err := k.unmount(".", MNT_DETACH); err != nil { - k.fatalf("cannot unmount intermediate root: %v", err) + k.fatalf(msg, "cannot unmount intermediate root: %v", err) } if err := k.chdir(FHSRoot); err != nil { - k.fatalf("cannot enter root: %v", err) + k.fatalf(msg, "cannot enter root: %v", err) } if err := k.close(fd); err != nil { - k.fatalf("cannot close intermediate root: %v", err) + k.fatalf(msg, "cannot close intermediate root: %v", err) } } if err := k.capAmbientClearAll(); err != nil { - k.fatalf("cannot clear the ambient capability set: %v", err) + k.fatalf(msg, "cannot clear the ambient capability set: %v", err) } for i := uintptr(0); i <= lastcap; i++ { if params.Privileged && i == CAP_SYS_ADMIN { continue } if err := k.capBoundingSetDrop(i); err != nil { - k.fatalf("cannot drop capability from bounding set: %v", err) + k.fatalf(msg, "cannot drop capability from bounding set: %v", err) } } @@ -292,29 +297,29 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil { - k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err) + k.fatalf(msg, "cannot raise CAP_SYS_ADMIN: %v", err) } } if err := k.capset( &capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, ); err != nil { - k.fatalf("cannot capset: %v", err) + k.fatalf(msg, "cannot capset: %v", err) } if !params.SeccompDisable { rules := params.SeccompRules if len(rules) == 0 { // non-empty rules slice always overrides presets - k.verbosef("resolving presets %#x", params.SeccompPresets) + msg.Verbosef("resolving presets %#x", params.SeccompPresets) rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) } if err := k.seccompLoad(rules, params.SeccompFlags); err != nil { // this also indirectly asserts PR_SET_NO_NEW_PRIVS - k.fatalf("cannot load syscall filter: %v", err) + k.fatalf(msg, "cannot load syscall filter: %v", err) } - k.verbosef("%d filter rules loaded", len(rules)) + msg.Verbosef("%d filter rules loaded", len(rules)) } else { - k.verbose("syscall filter not configured") + msg.Verbose("syscall filter not configured") } extraFiles := make([]*os.File, params.Count) @@ -331,14 +336,14 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV cmd.ExtraFiles = extraFiles cmd.Dir = params.Dir.String() - k.verbosef("starting initial program %s", params.Path) + msg.Verbosef("starting initial program %s", params.Path) if err := k.start(cmd); err != nil { - k.fatalf("%v", err) + k.fatalf(msg, "%v", err) } - k.suspend() + msg.Suspend() if err := closeSetup(); err != nil { - k.printf("cannot close setup pipe: %v", err) + k.printf(msg, "cannot close setup pipe: %v", err) // not fatal } @@ -372,7 +377,7 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV } } if !errors.Is(err, ECHILD) { - k.printf("unexpected wait4 response: %v", err) + k.printf(msg, "unexpected wait4 response: %v", err) } close(done) @@ -389,50 +394,50 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV for { select { case s := <-sig: - if k.resume() { - k.verbosef("%s after process start", s.String()) + if msg.Resume() { + msg.Verbosef("%s after process start", s.String()) } else { - k.verbosef("got %s", s.String()) + msg.Verbosef("got %s", s.String()) } if s == CancelSignal && params.ForwardCancel && cmd.Process != nil { - k.verbose("forwarding context cancellation") + msg.Verbose("forwarding context cancellation") if err := k.signal(cmd, os.Interrupt); err != nil { - k.printf("cannot forward cancellation: %v", err) + k.printf(msg, "cannot forward cancellation: %v", err) } continue } - k.beforeExit() + msg.BeforeExit() k.exit(0) case w := <-info: if w.wpid == cmd.Process.Pid { // initial process exited, output is most likely available again - k.resume() + msg.Resume() switch { case w.wstatus.Exited(): r = w.wstatus.ExitStatus() - k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) + msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) case w.wstatus.Signaled(): r = 128 + int(w.wstatus.Signal()) - k.verbosef("initial process exited with signal %s", w.wstatus.Signal()) + msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal()) default: r = 255 - k.verbosef("initial process exited with status %#x", w.wstatus) + msg.Verbosef("initial process exited with status %#x", w.wstatus) } go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() } case <-done: - k.beforeExit() + msg.BeforeExit() k.exit(r) case <-timeout: - k.printf("timeout exceeded waiting for lingering processes") - k.beforeExit() + k.printf(msg, "timeout exceeded waiting for lingering processes") + msg.BeforeExit() k.exit(r) } } @@ -441,10 +446,16 @@ func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setV 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 a nil msg is passed, the system logger is used instead. +func TryArgv0(msg Msg) { + if msg == nil { + log.SetPrefix(initName + ": ") + log.SetFlags(0) + msg = NewMsg(log.Default()) + } + if len(os.Args) > 0 && path.Base(os.Args[0]) == initName { - msg = v - Init(prepare, setVerbose) + Init(msg) msg.BeforeExit() os.Exit(0) } diff --git a/container/init_test.go b/container/init_test.go index 7dd92c0..162afad 100644 --- a/container/init_test.go +++ b/container/init_test.go @@ -11,25 +11,9 @@ import ( ) func TestInitEntrypoint(t *testing.T) { - assertPrefix := func(prefix string) { - if prefix != "init" { - t.Fatalf("prepareLogger: prefix = %q", prefix) - } - } - - assertVerboseFalse := func(verbose bool) { - if verbose { - t.Fatal("setVerbose: verbose = true, want false") - } - } - assertVerboseTrue := func(verbose bool) { - if !verbose { - t.Fatal("setVerbose: verbose = false, want true") - } - } checkSimple(t, "initEntrypoint", []simpleTestCase{ - {"getpid", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"getpid", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1<<10, nil), @@ -37,7 +21,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"receive bad fd", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"receive bad fd", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -47,7 +31,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"receive not set", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"receive not set", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -57,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"receive payload decode", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"receive payload decode", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -67,7 +51,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"receive invalid params", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"receive invalid params", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -91,7 +75,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"setDumpable user", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"setDumpable user", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -112,13 +96,14 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(78), nil), + call("swapVerbose", stub.ExpectArgs{true}, false, nil), call("verbose", stub.ExpectArgs{[]any{"received setup parameters"}}, nil, nil), call("setDumpable", stub.ExpectArgs{uintptr(1)}, nil, stub.UniqueError(77)), call("fatalf", stub.ExpectArgs{"cannot set SUID_DUMP_USER: %v", []any{stub.UniqueError(77)}}, nil, nil), }, }, nil}, - {"writeFile uid_map", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"writeFile uid_map", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -139,6 +124,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(76), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, stub.UniqueError(75)), @@ -146,7 +132,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"writeFile setgroups", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"writeFile setgroups", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -167,6 +153,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(74), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -175,7 +162,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"writeFile gid_map", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"writeFile gid_map", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -196,6 +183,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(72), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -205,7 +193,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"setDumpable disable", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"setDumpable disable", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -226,6 +214,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(70), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -236,7 +225,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"sethostname", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"sethostname", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -257,6 +246,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(68), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -269,7 +259,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"mount rslave root", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"mount rslave root", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -290,6 +280,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(66), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -304,7 +295,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"nil op", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"nil op", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -325,6 +316,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(64), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -341,7 +333,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"invalid op", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"invalid op", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -362,6 +354,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(63), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -378,7 +371,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"early unhandled error", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"early unhandled error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -399,6 +392,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(62), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -416,7 +410,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"early", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"early", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -437,6 +431,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(60), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -454,7 +449,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"mount ih", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"mount ih", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -475,6 +470,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(59), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -493,7 +489,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"chdir ih", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"chdir ih", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -514,6 +510,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(57), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -533,7 +530,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"mkdir sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"mkdir sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -554,6 +551,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(55), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -574,7 +572,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"mount bind sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"mount bind sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -595,6 +593,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(53), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -616,7 +615,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"mkdir host", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"mkdir host", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -637,6 +636,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(51), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -659,7 +659,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"pivotRoot ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"pivotRoot ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -680,6 +680,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(49), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -703,7 +704,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"chdir ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"chdir ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -724,6 +725,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(47), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -748,7 +750,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"apply unhandled error", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"apply unhandled error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -769,6 +771,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(45), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -802,7 +805,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"apply", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"apply", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -823,6 +826,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(43), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -856,7 +860,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"mount rprivate host", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"mount rprivate host", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -877,6 +881,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(42), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -911,7 +916,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"unmount host", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"unmount host", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -932,6 +937,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(40), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -967,7 +973,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"open ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"open ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -988,6 +994,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(38), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1025,7 +1032,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"chdir sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"chdir sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1046,6 +1053,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(36), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1084,7 +1092,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"pivotRoot sysroot", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"pivotRoot sysroot", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1105,6 +1113,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(34), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1144,7 +1153,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"fchdir ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"fchdir ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1165,6 +1174,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(32), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1205,7 +1215,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"unmount ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"unmount ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1226,6 +1236,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(30), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1267,7 +1278,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"chdir ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"chdir ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1288,6 +1299,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(28), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1330,7 +1342,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"close ir", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"close ir", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1351,6 +1363,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(26), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1394,7 +1407,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"capAmbientClearAll", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"capAmbientClearAll", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1415,6 +1428,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(24), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1459,7 +1473,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"capBoundingSetDrop", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"capBoundingSetDrop", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1480,6 +1494,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(22), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1532,7 +1547,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"capAmbientRaise", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"capAmbientRaise", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1553,6 +1568,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(20), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1638,7 +1654,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"capset", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"capset", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1659,6 +1675,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(18), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1745,7 +1762,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"seccompLoad", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"seccompLoad", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1766,6 +1783,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(16), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -1854,7 +1872,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"start", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{ + {"start", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), call("getpid", stub.ExpectArgs{}, 1, nil), @@ -1874,6 +1892,7 @@ func TestInitEntrypoint(t *testing.T) { SeccompDisable: true, ParentPerm: 0750, }, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(13), 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), @@ -1966,7 +1985,7 @@ func TestInitEntrypoint(t *testing.T) { }, }, nil}, - {"lowlastcap signaled cancel forward error", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{ + {"lowlastcap signaled cancel forward error", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ /* entrypoint */ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), @@ -1987,6 +2006,7 @@ func TestInitEntrypoint(t *testing.T) { SeccompDisable: true, ParentPerm: 0750, }, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(10), 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), @@ -2039,7 +2059,7 @@ func TestInitEntrypoint(t *testing.T) { call("umask", stub.ExpectArgs{022}, 0, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{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{}, nil, 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), @@ -2065,7 +2085,7 @@ func TestInitEntrypoint(t *testing.T) { }}}, }, nil}, - {"lowlastcap signaled cancel", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{ + {"lowlastcap signaled cancel", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ /* entrypoint */ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), @@ -2086,6 +2106,7 @@ func TestInitEntrypoint(t *testing.T) { 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), @@ -2138,7 +2159,7 @@ func TestInitEntrypoint(t *testing.T) { call("umask", stub.ExpectArgs{022}, 0, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{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{}, nil, 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), @@ -2155,7 +2176,7 @@ func TestInitEntrypoint(t *testing.T) { }}}, }, nil}, - {"lowlastcap signaled timeout", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{ + {"lowlastcap signaled timeout", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ /* entrypoint */ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), @@ -2176,6 +2197,7 @@ func TestInitEntrypoint(t *testing.T) { SeccompDisable: true, ParentPerm: 0750, }, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(5), 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), @@ -2228,7 +2250,7 @@ func TestInitEntrypoint(t *testing.T) { call("umask", stub.ExpectArgs{022}, 0, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{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{}, nil, 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), @@ -2247,7 +2269,7 @@ func TestInitEntrypoint(t *testing.T) { }}}, }, nil}, - {"lowlastcap signaled", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{ + {"lowlastcap signaled", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ /* entrypoint */ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), @@ -2268,6 +2290,7 @@ func TestInitEntrypoint(t *testing.T) { SeccompDisable: true, ParentPerm: 0750, }, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(3), 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), @@ -2320,7 +2343,7 @@ func TestInitEntrypoint(t *testing.T) { call("umask", stub.ExpectArgs{022}, 0, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{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{}, nil, 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), @@ -2346,7 +2369,7 @@ func TestInitEntrypoint(t *testing.T) { }}}, }, nil}, - {"strangewait nopriv notty noseccomp yamafault", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseFalse); return nil }, stub.Expect{ + {"strangewait nopriv notty noseccomp yamafault", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ /* entrypoint */ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), @@ -2367,6 +2390,7 @@ func TestInitEntrypoint(t *testing.T) { SeccompDisable: true, ParentPerm: 0750, }, 1971, 127, 2, false}, uintptr(0x39)}, stub.UniqueError(1), 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), @@ -2455,7 +2479,7 @@ func TestInitEntrypoint(t *testing.T) { call("umask", stub.ExpectArgs{022}, 0, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{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{}, nil, 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), @@ -2481,7 +2505,7 @@ func TestInitEntrypoint(t *testing.T) { }}}, }, nil}, - {"success", func(k syscallDispatcher) error { initEntrypoint(k, assertPrefix, assertVerboseTrue); return nil }, stub.Expect{ + {"success", func(k *kstub) error { initEntrypoint(k, k); return nil }, stub.Expect{ /* entrypoint */ Calls: []stub.Call{ call("lockOSThread", stub.ExpectArgs{}, nil, nil), @@ -2503,6 +2527,7 @@ func TestInitEntrypoint(t *testing.T) { RetainSession: true, Privileged: true, }, 1000, 100, 3, true}, uintptr(9)}, stub.UniqueError(0), nil), + call("swapVerbose", stub.ExpectArgs{true}, 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("4294967296 1000 1\n"), os.FileMode(0)}, nil, nil), @@ -2594,7 +2619,7 @@ func TestInitEntrypoint(t *testing.T) { call("umask", stub.ExpectArgs{022}, 0, nil), call("verbosef", stub.ExpectArgs{"starting initial program %s", []any{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{}, nil, 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), diff --git a/container/initbind.go b/container/initbind.go index ad8bfaf..43c8310 100644 --- a/container/initbind.go +++ b/container/initbind.go @@ -59,7 +59,7 @@ func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error { } } -func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error { +func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error { if b.sourceFinal == nil { if b.Flags&BindOptional == 0 { // unreachable @@ -92,11 +92,11 @@ func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error { } if b.sourceFinal.String() == b.Target.String() { - k.verbosef("mounting %q flags %#x", target, flags) + state.Verbosef("mounting %q flags %#x", target, flags) } else { - k.verbosef("mounting %q on %q flags %#x", source, target, flags) + state.Verbosef("mounting %q on %q flags %#x", source, target, flags) } - return k.bindMount(source, target, flags) + return k.bindMount(state, source, target, flags) } func (b *BindMountOp) Is(op Op) bool { diff --git a/container/initdev.go b/container/initdev.go index cfb4b8b..335cac0 100644 --- a/container/initdev.go +++ b/container/initdev.go @@ -46,6 +46,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error { return err } if err := k.bindMount( + state, toHost(FHSDev+name), targetPath, 0, @@ -93,6 +94,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error { if name, err := k.readlink(hostProc.stdout()); err != nil { return err } else if err = k.bindMount( + state, toHost(name), consolePath, 0, @@ -116,7 +118,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error { return nil } - if err := k.remount(target, MS_RDONLY); err != nil { + if err := k.remount(state, target, MS_RDONLY); err != nil { return err } return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777) diff --git a/container/initplace.go b/container/initplace.go index 5775ad7..5975bba 100644 --- a/container/initplace.go +++ b/container/initplace.go @@ -52,6 +52,7 @@ func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error { if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil { return err } else if err = k.bindMount( + state, tmpPath, target, syscall.MS_RDONLY|syscall.MS_NODEV, diff --git a/container/initremount.go b/container/initremount.go index 09f141d..b89ed54 100644 --- a/container/initremount.go +++ b/container/initremount.go @@ -21,8 +21,8 @@ type RemountOp struct { func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil } func (*RemountOp) early(*setupState, syscallDispatcher) error { return nil } -func (r *RemountOp) apply(_ *setupState, k syscallDispatcher) error { - return k.remount(toSysroot(r.Target.String()), r.Flags) +func (r *RemountOp) apply(state *setupState, k syscallDispatcher) error { + return k.remount(state, toSysroot(r.Target.String()), r.Flags) } func (r *RemountOp) Is(op Op) bool { diff --git a/container/mount.go b/container/mount.go index c5a82aa..dbd39cb 100644 --- a/container/mount.go +++ b/container/mount.go @@ -96,18 +96,17 @@ const ( ) // bindMount mounts source on target and recursively applies flags if MS_REC is set. -func (p *procPaths) bindMount(source, target string, flags uintptr) error { +func (p *procPaths) bindMount(msg Msg, source, target string, flags uintptr) error { // syscallDispatcher.bindMount and procPaths.remount must not be called from this function if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil { return err } - - return p.k.remount(target, flags) + return p.k.remount(msg, target, flags) } // remount applies flags on target, recursively if MS_REC is set. -func (p *procPaths) remount(target string, flags uintptr) error { +func (p *procPaths) remount(msg Msg, target string, flags uintptr) error { // syscallDispatcher methods bindMount, remount must not be called from this function var targetFinal string @@ -116,7 +115,7 @@ func (p *procPaths) remount(target string, flags uintptr) error { } else { targetFinal = v if targetFinal != target { - p.k.verbosef("target resolves to %q", targetFinal) + msg.Verbosef("target resolves to %q", targetFinal) } } @@ -146,7 +145,7 @@ func (p *procPaths) remount(target string, flags uintptr) error { return err } - if err = remountWithFlags(p.k, n, mf); err != nil { + if err = remountWithFlags(p.k, msg, n, mf); err != nil { return err } if flags&MS_REC == 0 { @@ -159,7 +158,7 @@ func (p *procPaths) remount(target string, flags uintptr) error { continue } - if err = remountWithFlags(p.k, cur, mf); err != nil && !errors.Is(err, EACCES) { + if err = remountWithFlags(p.k, msg, cur, mf); err != nil && !errors.Is(err, EACCES) { return err } } @@ -169,12 +168,12 @@ func (p *procPaths) remount(target string, flags uintptr) error { } // remountWithFlags remounts mount point described by [vfs.MountInfoNode]. -func remountWithFlags(k syscallDispatcher, n *vfs.MountInfoNode, mf uintptr) error { +func remountWithFlags(k syscallDispatcher, msg Msg, n *vfs.MountInfoNode, mf uintptr) error { // syscallDispatcher methods bindMount, remount must not be called from this function kf, unmatched := n.Flags() if len(unmatched) != 0 { - k.verbosef("unmatched vfs options: %q", unmatched) + msg.Verbosef("unmatched vfs options: %q", unmatched) } if kf&mf != mf { diff --git a/container/mount_test.go b/container/mount_test.go index 5d274e2..5391623 100644 --- a/container/mount_test.go +++ b/container/mount_test.go @@ -11,21 +11,21 @@ import ( func TestBindMount(t *testing.T) { checkSimple(t, "bindMount", []simpleTestCase{ - {"mount", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) + {"mount", func(k *kstub) error { + return newProcPaths(k, hostPath).bindMount(nil, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY) }, stub.Expect{Calls: []stub.Call{ call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, stub.UniqueError(0xbad)), }}, stub.UniqueError(0xbad)}, - {"success ne", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY) + {"success ne", func(k *kstub) error { + return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/.host-nix", syscall.MS_RDONLY) }, stub.Expect{Calls: []stub.Call{ call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/.host-nix", "", uintptr(0x9000), ""}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/.host-nix", uintptr(1)}, nil, nil), }}, nil}, - {"success", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).bindMount("/host/nix", "/sysroot/nix", syscall.MS_RDONLY) + {"success", func(k *kstub) error { + return newProcPaths(k, hostPath).bindMount(k, "/host/nix", "/sysroot/nix", syscall.MS_RDONLY) }, stub.Expect{Calls: []stub.Call{ call("mount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", "", uintptr(0x9000), ""}, nil, nil), call("remount", stub.ExpectArgs{"/sysroot/nix", uintptr(1)}, nil, nil), @@ -77,29 +77,29 @@ func TestRemount(t *testing.T) { 416 415 0:30 / /sysroot/nix/store ro,relatime master:5 - overlay overlay rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work` checkSimple(t, "remount", []simpleTestCase{ - {"evalSymlinks", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"evalSymlinks", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", stub.UniqueError(6)), }}, stub.UniqueError(6)}, - {"open", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"open", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, stub.UniqueError(5)), }}, &os.PathError{Op: "open", Path: "/sysroot/nix", Err: stub.UniqueError(5)}}, - {"readlink", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"readlink", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), call("readlink", stub.ExpectArgs{"/host/proc/self/fd/3735928559"}, "/sysroot/nix", stub.UniqueError(4)), }}, stub.UniqueError(4)}, - {"close", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"close", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -107,8 +107,8 @@ func TestRemount(t *testing.T) { call("close", stub.ExpectArgs{0xdeadbeef}, nil, stub.UniqueError(3)), }}, &os.PathError{Op: "close", Path: "/sysroot/nix", Err: stub.UniqueError(3)}}, - {"mountinfo no match", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"mountinfo no match", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(k, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/.hakurei", nil), call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/.hakurei"}}, nil, nil), @@ -118,8 +118,8 @@ func TestRemount(t *testing.T) { call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile(sampleMountinfoNix), nil), }}, &vfs.DecoderError{Op: "unfold", Line: -1, Err: vfs.UnfoldTargetError("/sysroot/.hakurei")}}, - {"mountinfo", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"mountinfo", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -128,8 +128,8 @@ func TestRemount(t *testing.T) { call("openNew", stub.ExpectArgs{"/host/proc/self/mountinfo"}, newConstFile("\x00"), nil), }}, &vfs.DecoderError{Op: "parse", Line: 0, Err: vfs.ErrMountInfoFields}}, - {"mount", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"mount", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -139,8 +139,8 @@ func TestRemount(t *testing.T) { call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, stub.UniqueError(2)), }}, stub.UniqueError(2)}, - {"mount propagate", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"mount propagate", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -151,8 +151,8 @@ func TestRemount(t *testing.T) { call("mount", stub.ExpectArgs{"none", "/sysroot/nix/.ro-store", "", uintptr(0x209027), ""}, nil, stub.UniqueError(1)), }}, stub.UniqueError(1)}, - {"success toplevel", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"success toplevel", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/bin", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/bin"}, "/sysroot/bin", nil), call("open", stub.ExpectArgs{"/sysroot/bin", 0x280000, uint32(0)}, 0xbabe, nil), @@ -162,8 +162,8 @@ func TestRemount(t *testing.T) { call("mount", stub.ExpectArgs{"none", "/sysroot/bin", "", uintptr(0x209027), ""}, nil, nil), }}, nil}, - {"success EACCES", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"success EACCES", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -175,8 +175,8 @@ func TestRemount(t *testing.T) { call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), }}, nil}, - {"success no propagate", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) + {"success no propagate", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -186,8 +186,8 @@ func TestRemount(t *testing.T) { call("mount", stub.ExpectArgs{"none", "/sysroot/nix", "", uintptr(0x209027), ""}, nil, nil), }}, nil}, - {"success case sensitive", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"success case sensitive", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(nil, "/sysroot/nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/nix"}, "/sysroot/nix", nil), call("open", stub.ExpectArgs{"/sysroot/nix", 0x280000, uint32(0)}, 0xdeadbeef, nil), @@ -199,8 +199,8 @@ func TestRemount(t *testing.T) { call("mount", stub.ExpectArgs{"none", "/sysroot/nix/store", "", uintptr(0x209027), ""}, nil, nil), }}, nil}, - {"success", func(k syscallDispatcher) error { - return newProcPaths(k, hostPath).remount("/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) + {"success", func(k *kstub) error { + return newProcPaths(k, hostPath).remount(k, "/sysroot/.nix", syscall.MS_REC|syscall.MS_RDONLY|syscall.MS_NODEV) }, stub.Expect{Calls: []stub.Call{ call("evalSymlinks", stub.ExpectArgs{"/sysroot/.nix"}, "/sysroot/NIX", nil), call("verbosef", stub.ExpectArgs{"target resolves to %q", []any{"/sysroot/NIX"}}, nil, nil), @@ -217,18 +217,18 @@ func TestRemount(t *testing.T) { func TestRemountWithFlags(t *testing.T) { checkSimple(t, "remountWithFlags", []simpleTestCase{ - {"noop unmatched", func(k syscallDispatcher) error { - return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) + {"noop unmatched", func(k *kstub) error { + return remountWithFlags(k, k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime,cat"}}, 0) }, stub.Expect{Calls: []stub.Call{ call("verbosef", stub.ExpectArgs{"unmatched vfs options: %q", []any{[]string{"cat"}}}, nil, nil), }}, nil}, - {"noop", func(k syscallDispatcher) error { - return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) + {"noop", func(k *kstub) error { + return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, 0) }, stub.Expect{}, nil}, - {"success", func(k syscallDispatcher) error { - return remountWithFlags(k, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) + {"success", func(k *kstub) error { + return remountWithFlags(k, nil, &vfs.MountInfoNode{MountInfoEntry: &vfs.MountInfoEntry{VfsOptstr: "rw,relatime"}}, syscall.MS_RDONLY) }, stub.Expect{Calls: []stub.Call{ call("mount", stub.ExpectArgs{"none", "", "", uintptr(0x209021), ""}, nil, nil), }}, nil}, @@ -237,20 +237,20 @@ func TestRemountWithFlags(t *testing.T) { func TestMountTmpfs(t *testing.T) { checkSimple(t, "mountTmpfs", []simpleTestCase{ - {"mkdirAll", func(k syscallDispatcher) error { + {"mkdirAll", func(k *kstub) error { return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) }, stub.Expect{Calls: []stub.Call{ call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, stub.UniqueError(0)), }}, stub.UniqueError(0)}, - {"success no size", func(k syscallDispatcher) error { + {"success no size", func(k *kstub) error { return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 0, 0710) }, stub.Expect{Calls: []stub.Call{ call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0750)}, nil, nil), call("mount", stub.ExpectArgs{"ephemeral", "/sysroot/run/user/1000", "tmpfs", uintptr(0), "mode=0710"}, nil, nil), }}, nil}, - {"success", func(k syscallDispatcher) error { + {"success", func(k *kstub) error { return mountTmpfs(k, "ephemeral", "/sysroot/run/user/1000", 0, 1<<10, 0700) }, stub.Expect{Calls: []stub.Call{ call("mkdirAll", stub.ExpectArgs{"/sysroot/run/user/1000", os.FileMode(0700)}, nil, nil), diff --git a/container/msg.go b/container/msg.go index 1512a58..38c38ec 100644 --- a/container/msg.go +++ b/container/msg.go @@ -23,45 +23,88 @@ func GetErrorMessage(err error) (string, bool) { return e.Message(), true } +// Msg is used for package-wide verbose logging. type Msg interface { + // GetLogger returns the address of the underlying [log.Logger]. + GetLogger() *log.Logger + + // IsVerbose atomically loads and returns whether [Msg] has verbose logging enabled. IsVerbose() bool + // SwapVerbose atomically stores a new verbose state and returns the previous value held by [Msg]. + SwapVerbose(verbose bool) bool + // Verbose passes its argument to the Println method of the underlying [log.Logger] if IsVerbose returns true. Verbose(v ...any) + // Verbosef passes its argument to the Printf method of the underlying [log.Logger] if IsVerbose returns true. Verbosef(format string, v ...any) - Suspend() + // Suspend causes the embedded [Suspendable] to withhold writes to its downstream [io.Writer]. + // Suspend returns false and is a noop if called between calls to Suspend and Resume. + Suspend() bool + + // Resume dumps the entire buffer held by the embedded [Suspendable] and stops withholding future writes. + // Resume returns false and is a noop if a call to Suspend does not precede it. Resume() bool + + // BeforeExit runs implementation-specific cleanup code, and optionally prints warnings. + // BeforeExit is called before [os.Exit]. BeforeExit() } -type DefaultMsg struct{ inactive atomic.Bool } +// defaultMsg is the default implementation of the [Msg] interface. +// The zero value is not safe for use. Callers should use the [NewMsg] function instead. +type defaultMsg struct { + verbose atomic.Bool -func (msg *DefaultMsg) IsVerbose() bool { return true } -func (msg *DefaultMsg) Verbose(v ...any) { - if !msg.inactive.Load() { - log.Println(v...) + logger *log.Logger + Suspendable +} + +// NewMsg initialises a downstream [log.Logger] for a new [Msg]. +// The [log.Logger] should no longer be configured after NewMsg returns. +// If downstream is nil, a new logger is initialised in its place. +func NewMsg(downstream *log.Logger) Msg { + if downstream == nil { + downstream = log.New(log.Writer(), "container: ", 0) + } + + m := defaultMsg{logger: downstream} + m.Suspendable.Downstream = downstream.Writer() + downstream.SetOutput(&m.Suspendable) + return &m +} + +func (msg *defaultMsg) GetLogger() *log.Logger { return msg.logger } + +func (msg *defaultMsg) IsVerbose() bool { return msg.verbose.Load() } +func (msg *defaultMsg) SwapVerbose(verbose bool) bool { return msg.verbose.Swap(verbose) } +func (msg *defaultMsg) Verbose(v ...any) { + if msg.verbose.Load() { + msg.logger.Println(v...) } } -func (msg *DefaultMsg) Verbosef(format string, v ...any) { - if !msg.inactive.Load() { - log.Printf(format, v...) +func (msg *defaultMsg) Verbosef(format string, v ...any) { + if msg.verbose.Load() { + msg.logger.Printf(format, v...) } } -func (msg *DefaultMsg) Suspend() { msg.inactive.Store(true) } -func (msg *DefaultMsg) Resume() bool { return msg.inactive.CompareAndSwap(true, false) } -func (msg *DefaultMsg) BeforeExit() {} +// Resume calls [Suspendable.Resume] and prints a message if buffer was filled +// between calls to [Suspendable.Suspend] and Resume. +func (msg *defaultMsg) Resume() bool { + resumed, dropped, _, err := msg.Suspendable.Resume() + if err != nil { + // probably going to result in an error as well, so this message is as good as unreachable + msg.logger.Printf("cannot dump buffer on resume: %v", err) + } + if resumed && dropped > 0 { + msg.logger.Printf("dropped %d bytes while output is suspended", dropped) + } + return resumed +} -// msg is the [Msg] implemented used by all exported [container] functions. -var msg Msg = new(DefaultMsg) - -// GetOutput returns the current active [Msg] implementation. -func GetOutput() Msg { return msg } - -// SetOutput replaces the current active [Msg] implementation. -func SetOutput(v Msg) { - if v == nil { - msg = new(DefaultMsg) - } else { - msg = v +// BeforeExit prints a message if called between calls to [Suspendable.Suspend] and Resume. +func (msg *defaultMsg) BeforeExit() { + if msg.Resume() { + msg.logger.Printf("beforeExit reached on suspended output") } } diff --git a/container/msg_test.go b/container/msg_test.go index 1b36a1d..ed5c0c5 100644 --- a/container/msg_test.go +++ b/container/msg_test.go @@ -1,14 +1,16 @@ package container_test import ( + "bytes" "errors" + "io" "log" "strings" - "sync/atomic" "syscall" "testing" "hakurei.app/container" + "hakurei.app/container/stub" ) func TestMessageError(t *testing.T) { @@ -39,146 +41,137 @@ func TestMessageError(t *testing.T) { } func TestDefaultMsg(t *testing.T) { - { - w := log.Writer() - f := log.Flags() - t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) }) - } - msg := new(container.DefaultMsg) + // copied from output.go + const suspendBufMax = 1 << 24 - t.Run("is verbose", func(t *testing.T) { - if !msg.IsVerbose() { - t.Error("IsVerbose unexpected outcome") - } - }) + t.Run("logger", func(t *testing.T) { + t.Run("nil", func(t *testing.T) { + got := container.NewMsg(nil).GetLogger() - t.Run("verbose", func(t *testing.T) { - log.SetOutput(panicWriter{}) - msg.Suspend() - msg.Verbose() - msg.Verbosef("\x00") - msg.Resume() - - buf := new(strings.Builder) - log.SetOutput(buf) - log.SetFlags(0) - msg.Verbose() - msg.Verbosef("\x00") - - want := "\n\x00\n" - if buf.String() != want { - t.Errorf("Verbose: %q, want %q", buf.String(), want) - } - }) - - t.Run("inactive", func(t *testing.T) { - { - inactive := msg.Resume() - if inactive { - t.Cleanup(func() { msg.Suspend() }) + if out := got.Writer().(*container.Suspendable).Downstream; out != log.Writer() { + t.Errorf("GetLogger: Downstream = %#v", out) } - } - if msg.Resume() { - t.Error("Resume unexpected outcome") - } + if prefix := got.Prefix(); prefix != "container: " { + t.Errorf("GetLogger: prefix = %q", prefix) + } + }) - msg.Suspend() - if !msg.Resume() { - t.Error("Resume unexpected outcome") - } + t.Run("takeover", func(t *testing.T) { + l := log.New(io.Discard, "\x00", 0xdeadbeef) + got := container.NewMsg(l) + + if logger := got.GetLogger(); logger != l { + t.Errorf("GetLogger: %#v, want %#v", logger, l) + } + + if ds := l.Writer().(*container.Suspendable).Downstream; ds != io.Discard { + t.Errorf("GetLogger: Downstream = %#v", ds) + } + }) }) - // the function is a noop - t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() }) -} + dw := expectWriter{t: t} -type panicWriter struct{} + steps := []struct { + name string + pt, next []byte + err error -func (panicWriter) Write([]byte) (int, error) { panic("unreachable") } + f func(t *testing.T, msg container.Msg) + }{ + {"zero verbose", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if msg.IsVerbose() { + t.Error("IsVerbose unexpected true") + } + }}, -func saveRestoreOutput(t *testing.T) { - out := container.GetOutput() - t.Cleanup(func() { container.SetOutput(out) }) -} + {"swap false", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if msg.SwapVerbose(false) { + t.Error("SwapVerbose unexpected true") + } + }}, + {"write discard", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.Verbose("\x00") + msg.Verbosef("\x00") + }}, + {"verbose false", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if msg.IsVerbose() { + t.Error("IsVerbose unexpected true") + } + }}, -func replaceOutput(t *testing.T) { - saveRestoreOutput(t) - container.SetOutput(&testOutput{t: t}) -} + {"swap true", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if msg.SwapVerbose(true) { + t.Error("SwapVerbose unexpected true") + } + }}, + {"write verbose", []byte("test: \x00\n"), nil, nil, func(_ *testing.T, msg container.Msg) { + msg.Verbose("\x00") + }}, + {"write verbosef", []byte(`test: "\x00"` + "\n"), nil, nil, func(_ *testing.T, msg container.Msg) { + msg.Verbosef("%q", "\x00") + }}, + {"verbose true", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if !msg.IsVerbose() { + t.Error("IsVerbose unexpected false") + } + }}, -type testOutput struct { - t *testing.T - suspended atomic.Bool -} + {"resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if msg.Resume() { + t.Error("Resume unexpected success") + } + }}, + {"beforeExit noop", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.BeforeExit() + }}, -func (out *testOutput) IsVerbose() bool { return testing.Verbose() } + {"beforeExit suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.Suspend() + }}, + {"beforeExit message", []byte("test: beforeExit reached on suspended output\n"), nil, nil, func(_ *testing.T, msg container.Msg) { + msg.BeforeExit() + }}, + {"post beforeExit resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) { + if msg.Resume() { + t.Error("Resume unexpected success") + } + }}, -func (out *testOutput) Verbose(v ...any) { - if !out.IsVerbose() { - return - } - out.t.Log(v...) -} + {"suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.Suspend() + }}, + {"suspend write", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.GetLogger().Print("\x00") + }}, + {"resume error", []byte("test: \x00\n"), []byte("test: cannot dump buffer on resume: unique error 0 injected by the test suite\n"), stub.UniqueError(0), func(t *testing.T, msg container.Msg) { + if !msg.Resume() { + t.Error("Resume unexpected failure") + } + }}, -func (out *testOutput) Verbosef(format string, v ...any) { - if !out.IsVerbose() { - return - } - out.t.Logf(format, v...) -} - -func (out *testOutput) Suspend() { - if out.suspended.CompareAndSwap(false, true) { - out.Verbose("suspend called") - return - } - out.Verbose("suspend called on suspended output") -} - -func (out *testOutput) Resume() bool { - if out.suspended.CompareAndSwap(true, false) { - out.Verbose("resume called") - return true - } - out.Verbose("resume called on unsuspended output") - return false -} - -func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") } - -func TestGetSetOutput(t *testing.T) { - { - out := container.GetOutput() - t.Cleanup(func() { container.SetOutput(out) }) + {"suspend drop", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.Suspend() + }}, + {"suspend write fill", nil, nil, nil, func(_ *testing.T, msg container.Msg) { + msg.GetLogger().Print(strings.Repeat("\x00", suspendBufMax)) + }}, + {"resume dropped", append([]byte("test: "), bytes.Repeat([]byte{0}, suspendBufMax-6)...), []byte("test: dropped 7 bytes while output is suspended\n"), nil, func(t *testing.T, msg container.Msg) { + if !msg.Resume() { + t.Error("Resume unexpected failure") + } + }}, } - t.Run("default", func(t *testing.T) { - container.SetOutput(new(stubOutput)) - if v, ok := container.GetOutput().(*container.DefaultMsg); ok { - t.Fatalf("SetOutput: got unexpected output %#v", v) + msg := container.NewMsg(log.New(&dw, "test: ", 0)) + for _, step := range steps { + // these share the same writer, so cannot be subtests + t.Logf("running step %q", step.name) + dw.expect, dw.next, dw.err = step.pt, step.next, step.err + step.f(t, msg) + if dw.expect != nil { + t.Errorf("expect: %q", string(dw.expect)) } - container.SetOutput(nil) - if _, ok := container.GetOutput().(*container.DefaultMsg); !ok { - t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput()) - } - }) - - t.Run("stub", func(t *testing.T) { - container.SetOutput(new(stubOutput)) - if _, ok := container.GetOutput().(*stubOutput); !ok { - t.Fatalf("SetOutput: got unexpected output %#v", container.GetOutput()) - } - }) + } } - -type stubOutput struct { - wrapF func(error, ...any) error -} - -func (*stubOutput) IsVerbose() bool { panic("unreachable") } -func (*stubOutput) Verbose(...any) { panic("unreachable") } -func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") } -func (*stubOutput) Suspend() { panic("unreachable") } -func (*stubOutput) Resume() bool { panic("unreachable") } -func (*stubOutput) BeforeExit() { panic("unreachable") } diff --git a/container/output_test.go b/container/output_test.go index cfa8e0d..b6dedf1 100644 --- a/container/output_test.go +++ b/container/output_test.go @@ -28,7 +28,7 @@ func TestSuspendable(t *testing.T) { ) // shares the same writer - testCases := []struct { + steps := []struct { name string w, pt []byte err error @@ -75,25 +75,25 @@ func TestSuspendable(t *testing.T) { var dw expectWriter w := container.Suspendable{Downstream: &dw} - for _, tc := range testCases { + for _, step := range steps { // these share the same writer, so cannot be subtests - t.Logf("writing step %q", tc.name) - dw.expect, dw.err = tc.pt, tc.err + t.Logf("writing step %q", step.name) + dw.expect, dw.err = step.pt, step.err var ( gotN int gotErr error ) - wantN := tc.n + wantN := step.n switch wantN { case nSpecialPtEquiv: - wantN = len(tc.pt) - gotN, gotErr = w.Write(tc.w) + wantN = len(step.pt) + gotN, gotErr = w.Write(step.w) case nSpecialWEquiv: - wantN = len(tc.w) - gotN, gotErr = w.Write(tc.w) + wantN = len(step.w) + gotN, gotErr = w.Write(step.w) case nSpecialSuspend: s := w.IsSuspended() @@ -101,8 +101,8 @@ func TestSuspendable(t *testing.T) { t.Fatal("Suspend: unexpected success") } - wantN = len(tc.w) - gotN, gotErr = w.Write(tc.w) + wantN = len(step.w) + gotN, gotErr = w.Write(step.w) default: if wantN <= nSpecialDump { @@ -118,10 +118,10 @@ func TestSuspendable(t *testing.T) { t.Errorf("Resume: dropped = %d, want %d", dropped, wantDropped) } - wantN = len(tc.pt) + wantN = len(step.pt) gotN, gotErr = int(n), err } else { - gotN, gotErr = w.Write(tc.w) + gotN, gotErr = w.Write(step.w) } } @@ -129,9 +129,13 @@ func TestSuspendable(t *testing.T) { t.Errorf("Write: n = %d, want %d", gotN, wantN) } - if !reflect.DeepEqual(gotErr, tc.wantErr) { + if !reflect.DeepEqual(gotErr, step.wantErr) { t.Errorf("Write: %v", gotErr) } + + if dw.expect != nil { + t.Errorf("expect: %q", string(dw.expect)) + } } } @@ -139,17 +143,31 @@ func TestSuspendable(t *testing.T) { type expectWriter struct { expect []byte err error + + // optional consecutive write + next []byte + + // optional, calls Error on failure if not nil + t *testing.T } func (w *expectWriter) Write(p []byte) (n int, err error) { - defer func() { w.expect = nil }() + defer func() { w.expect = w.next; w.next = nil }() n, err = len(p), w.err if w.expect == nil { - return 0, errors.New("unexpected call to Write: " + strconv.Quote(string(p))) + n, err = 0, errors.New("unexpected call to Write: "+strconv.Quote(string(p))) + if w.t != nil { + w.t.Error(err.Error()) + } + return } if string(p) != string(w.expect) { - return 0, errors.New("p = " + strconv.Quote(string(p)) + ", want " + strconv.Quote(string(w.expect))) + n, err = 0, errors.New("p = "+strconv.Quote(string(p))+", want "+strconv.Quote(string(w.expect))) + if w.t != nil { + w.t.Error(err.Error()) + } + return } return } diff --git a/container/sysctl.go b/container/sysctl.go index bfcf278..327872b 100644 --- a/container/sysctl.go +++ b/container/sysctl.go @@ -2,7 +2,6 @@ package container import ( "bytes" - "log" "os" "strconv" "sync" @@ -22,26 +21,28 @@ const ( kernelCapLastCapPath = FHSProcSys + "kernel/cap_last_cap" ) -func mustReadSysctl() { - if v, err := os.ReadFile(kernelOverflowuidPath); err != nil { - log.Fatalf("cannot read %q: %v", kernelOverflowuidPath, err) - } else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - log.Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err) - } +func mustReadSysctl(msg Msg) { + sysctlOnce.Do(func() { + if v, err := os.ReadFile(kernelOverflowuidPath); err != nil { + msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err) + } else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { + msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err) + } - if v, err := os.ReadFile(kernelOverflowgidPath); err != nil { - log.Fatalf("cannot read %q: %v", kernelOverflowgidPath, err) - } else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - log.Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err) - } + if v, err := os.ReadFile(kernelOverflowgidPath); err != nil { + msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowgidPath, err) + } else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { + msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err) + } - if v, err := os.ReadFile(kernelCapLastCapPath); err != nil { - log.Fatalf("cannot read %q: %v", kernelCapLastCapPath, err) - } else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - log.Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err) - } + if v, err := os.ReadFile(kernelCapLastCapPath); err != nil { + msg.GetLogger().Fatalf("cannot read %q: %v", kernelCapLastCapPath, err) + } else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { + msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err) + } + }) } -func OverflowUid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowuid } -func OverflowGid() int { sysctlOnce.Do(mustReadSysctl); return kernelOverflowgid } -func LastCap() uintptr { sysctlOnce.Do(mustReadSysctl); return uintptr(kernelCapLastCap) } +func OverflowUid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowuid } +func OverflowGid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowgid } +func LastCap(msg Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) } diff --git a/helper/container.go b/helper/container.go index a3b2257..9bab0df 100644 --- a/helper/container.go +++ b/helper/container.go @@ -16,6 +16,7 @@ import ( // New initialises a Helper instance with wt as the null-terminated argument writer. func New( ctx context.Context, + msg container.Msg, pathname *container.Absolute, name string, wt io.WriterTo, stat bool, @@ -26,7 +27,7 @@ func New( var args []string h := new(helperContainer) h.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles) - h.Container = container.NewCommand(ctx, pathname, name, args...) + h.Container = container.NewCommand(ctx, msg, pathname, name, args...) h.WaitDelay = WaitDelay if cmdF != nil { cmdF(h.Container) diff --git a/helper/container_test.go b/helper/container_test.go index 9aaeec8..a3f0a3f 100644 --- a/helper/container_test.go +++ b/helper/container_test.go @@ -12,7 +12,7 @@ import ( func TestContainer(t *testing.T) { t.Run("start invalid container", func(t *testing.T) { - h := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil) + h := helper.New(t.Context(), nil, container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil) wantErr := "container: starting an invalid container" if err := h.Start(); err == nil || err.Error() != wantErr { @@ -22,7 +22,7 @@ func TestContainer(t *testing.T) { }) t.Run("valid new helper nil check", func(t *testing.T) { - if got := helper.New(t.Context(), container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil { + if got := helper.New(t.Context(), nil, container.MustAbs(container.Nonexistent), "hakurei", argsWt, false, argF, nil, nil); got == nil { t.Errorf("New(%q, %q) got nil", argsWt, "hakurei") return @@ -31,7 +31,7 @@ func TestContainer(t *testing.T) { t.Run("implementation compliance", func(t *testing.T) { testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper { - return helper.New(ctx, container.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) { + return helper.New(ctx, nil, container.MustAbs(os.Args[0]), "helper", argsWt, stat, argF, func(z *container.Container) { setOutput(&z.Stdout, &z.Stderr) z. Bind(container.AbsFHSRoot, container.AbsFHSRoot, 0). diff --git a/helper/stub_test.go b/helper/stub_test.go index dc057ac..0679a73 100644 --- a/helper/stub_test.go +++ b/helper/stub_test.go @@ -6,12 +6,10 @@ import ( "hakurei.app/container" "hakurei.app/helper" - "hakurei.app/internal" - "hakurei.app/internal/hlog" ) func TestMain(m *testing.M) { - container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) + container.TryArgv0(nil) helper.InternalHelperStub() os.Exit(m.Run()) } diff --git a/internal/app/app.go b/internal/app/app.go index b533156..c5a3e0f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,23 +6,24 @@ import ( "log" "os" + "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app/state" ) // Main runs an app according to [hst.Config] and terminates. Main does not return. -func Main(ctx context.Context, config *hst.Config) { +func Main(ctx context.Context, msg container.Msg, config *hst.Config) { var id state.ID if err := state.NewAppID(&id); err != nil { log.Fatal(err) } seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}} - if err := seal.finalise(ctx, config); err != nil { + if err := seal.finalise(ctx, msg, config); err != nil { printMessageError("cannot seal app:", err) os.Exit(1) } - seal.main() + seal.main(msg) panic("unreachable") } diff --git a/internal/app/app_stub_test.go b/internal/app/app_stub_test.go index ff21849..8d33cd1 100644 --- a/internal/app/app_stub_test.go +++ b/internal/app/app_stub_test.go @@ -6,6 +6,8 @@ import ( "log" "os/exec" "os/user" + + "hakurei.app/container" ) type stubNixOS struct { @@ -187,10 +189,10 @@ func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) { } } -func (k *stubNixOS) overflowUid() int { return 65534 } -func (k *stubNixOS) overflowGid() int { return 65534 } +func (k *stubNixOS) overflowUid(container.Msg) int { return 65534 } +func (k *stubNixOS) overflowGid(container.Msg) int { return 65534 } -func (k *stubNixOS) mustHsuPath() string { return "/proc/nonexistent/hsu" } +func (k *stubNixOS) mustHsuPath() *container.Absolute { return m("/proc/nonexistent/hsu") } func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) } diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 005b721..fa85c46 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -37,7 +37,7 @@ func TestApp(t *testing.T) { 0xbd, 0x01, 0x78, 0x0e, 0xb9, 0xa6, 0x07, 0xac, }, - system.New(context.TODO(), 1000000). + system.New(context.TODO(), container.NewMsg(nil), 1000000). Ensure("/tmp/hakurei.0", 0711). Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute). Ensure("/tmp/hakurei.0/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/0", acl.Read, acl.Write, acl.Execute). @@ -129,7 +129,7 @@ func TestApp(t *testing.T) { 0x82, 0xd4, 0x13, 0x36, 0x9b, 0x64, 0xce, 0x7c, }, - system.New(context.TODO(), 1000009). + system.New(context.TODO(), container.NewMsg(nil), 1000009). Ensure("/tmp/hakurei.0", 0711). Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute). Ensure("/tmp/hakurei.0/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/9", acl.Read, acl.Write, acl.Execute). @@ -280,7 +280,7 @@ func TestApp(t *testing.T) { 0x4c, 0xf0, 0x73, 0xbd, 0xb4, 0x6e, 0xb5, 0xc1, }, - system.New(context.TODO(), 1000001). + system.New(context.TODO(), container.NewMsg(nil), 1000001). Ensure("/tmp/hakurei.0", 0711). Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute). Ensure("/tmp/hakurei.0/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/1", acl.Read, acl.Write, acl.Execute). @@ -377,7 +377,7 @@ func TestApp(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Run("finalise", func(t *testing.T) { seal := outcome{syscallDispatcher: tc.k, id: &stringPair[state.ID]{tc.id, tc.id.String()}} - err := seal.finalise(t.Context(), tc.config) + err := seal.finalise(t.Context(), container.NewMsg(nil), tc.config) if err != nil { if s, ok := container.GetErrorMessage(err); !ok { t.Fatalf("Seal: error = %v", err) diff --git a/internal/app/container.go b/internal/app/container.go index d406d21..ae9dbf3 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -20,6 +20,7 @@ const preallocateOpsCount = 1 << 5 // newContainer initialises [container.Params] via [hst.ContainerConfig]. // Note that remaining container setup must be queued by the caller. func newContainer( + msg container.Msg, k syscallDispatcher, s *hst.ContainerConfig, prefix string, @@ -73,8 +74,8 @@ func newContainer( params.Gid = k.getgid() *gid = params.Gid } else { - *uid = k.overflowUid() - *gid = k.overflowGid() + *uid = k.overflowUid(msg) + *gid = k.overflowGid(msg) } filesystem := s.Filesystem @@ -126,11 +127,11 @@ func newContainer( // get parent dir of socket dir := path.Dir(pair[1]) if dir == "." || dir == container.FHSRoot { - k.verbosef("dbus socket %q is in an unusual location", pair[1]) + msg.Verbosef("dbus socket %q is in an unusual location", pair[1]) } hidePaths = append(hidePaths, dir) } else { - k.verbosef("dbus socket %q is not absolute", pair[1]) + msg.Verbosef("dbus socket %q is not absolute", pair[1]) } } } @@ -138,7 +139,7 @@ func newContainer( } hidePathMatch := make([]bool, len(hidePaths)) for i := range hidePaths { - if err := evalSymlinks(k, &hidePaths[i]); err != nil { + if err := evalSymlinks(msg, k, &hidePaths[i]); err != nil { return nil, nil, err } } @@ -178,7 +179,7 @@ func newContainer( if autoroot != nil { for _, ent := range autoRootEntries { name := ent.Name() - if container.IsAutoRootBindable(name) { + if container.IsAutoRootBindable(msg, name) { hidePathSource = append(hidePathSource, autoroot.Source.Append(name)) } } @@ -193,7 +194,7 @@ func newContainer( } hidePathSourceEval[i] = [2]string{a.String(), a.String()} - if err := evalSymlinks(k, &hidePathSourceEval[i][0]); err != nil { + if err := evalSymlinks(msg, k, &hidePathSourceEval[i][0]); err != nil { return nil, nil, err } } @@ -209,7 +210,7 @@ func newContainer( return nil, nil, err } else if ok { hidePathMatch[i] = true - k.verbosef("hiding path %q from %q", hidePaths[i], p[1]) + msg.Verbosef("hiding path %q from %q", hidePaths[i], p[1]) } } } @@ -241,12 +242,12 @@ func newContainer( } // evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist]. -func evalSymlinks(k syscallDispatcher, v *string) error { +func evalSymlinks(msg container.Msg, k syscallDispatcher, v *string) error { if p, err := k.evalSymlinks(*v); err != nil { if !errors.Is(err, fs.ErrNotExist) { return err } - k.verbosef("path %q does not yet exist", *v) + msg.Verbosef("path %q does not yet exist", *v) } else { *v = p } diff --git a/internal/app/dispatcher.go b/internal/app/dispatcher.go index deecfa9..8ab6073 100644 --- a/internal/app/dispatcher.go +++ b/internal/app/dispatcher.go @@ -9,7 +9,6 @@ import ( "hakurei.app/container" "hakurei.app/internal" - "hakurei.app/internal/hlog" ) // syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. @@ -45,19 +44,15 @@ type syscallDispatcher interface { cmdOutput(cmd *exec.Cmd) ([]byte, error) // overflowUid provides [container.OverflowUid]. - overflowUid() int + overflowUid(msg container.Msg) int // overflowGid provides [container.OverflowGid]. - overflowGid() int + overflowGid(msg container.Msg) int // mustHsuPath provides [internal.MustHsuPath]. - mustHsuPath() string + mustHsuPath() *container.Absolute // fatalf provides [log.Fatalf]. fatalf(format string, v ...any) - - isVerbose() bool - verbose(v ...any) - verbosef(format string, v ...any) } // direct implements syscallDispatcher on the current kernel. @@ -87,13 +82,9 @@ func (direct) lookupGroupId(name string) (gid string, err error) { func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() } -func (direct) overflowUid() int { return container.OverflowUid() } -func (direct) overflowGid() int { return container.OverflowGid() } +func (direct) overflowUid(msg container.Msg) int { return container.OverflowUid(msg) } +func (direct) overflowGid(msg container.Msg) int { return container.OverflowGid(msg) } -func (direct) mustHsuPath() string { return internal.MustHsuPath() } +func (direct) mustHsuPath() *container.Absolute { return internal.MustHsuPath() } func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } - -func (k direct) isVerbose() bool { return hlog.Load() } -func (direct) verbose(v ...any) { hlog.Verbose(v...) } -func (direct) verbosef(format string, v ...any) { hlog.Verbosef(format, v...) } diff --git a/internal/app/finalise.go b/internal/app/finalise.go index 2a0e5e6..d2c685f 100644 --- a/internal/app/finalise.go +++ b/internal/app/finalise.go @@ -20,7 +20,6 @@ import ( "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" "hakurei.app/system" "hakurei.app/system/acl" "hakurei.app/system/dbus" @@ -120,7 +119,7 @@ type hsuUser struct { username string } -func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { +func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.Config) error { const ( home = "HOME" shell = "SHELL" @@ -183,7 +182,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { } else if !isValidUsername(k.user.username) { return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username)) } - k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap())) + k.user.uid = newInt(HsuUid(hsu.MustIDMsg(msg), k.user.identity.unwrap())) k.user.supp = make([]string, len(config.Groups)) for i, name := range config.Groups { @@ -201,7 +200,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { // permissive defaults if config.Container == nil { - hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION") + msg.Verbose("container configuration not supplied, PROCEED WITH CAUTION") if config.Shell == nil { config.Shell = container.AbsFHSRoot.Append("bin", "sh") @@ -276,13 +275,14 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { // TODO(ophestra): revert this after params to shim share := &shareHost{seal: k} - copyPaths(k.syscallDispatcher, &share.sc, hsu.MustID()) + copyPaths(k.syscallDispatcher, msg, &share.sc, hsu.MustIDMsg(msg)) + msg.Verbosef("process share directory at %q, runtime directory at %q", share.sc.SharePath, share.sc.RunDirPath) var mapuid, mapgid *stringPair[int] { var uid, gid int var err error - k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid) + k.container, k.env, err = newContainer(msg, k, config.Container, k.id.String(), &share.sc, &uid, &gid) k.waitDelay = config.Container.WaitDelay if err != nil { return &hst.AppError{Step: "initialise container configuration", Err: err} @@ -307,7 +307,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { k.env[xdgSessionType] = "tty" k.runDirPath = share.sc.RunDirPath - k.sys = system.New(k.ctx, k.user.uid.unwrap()) + k.sys = system.New(k.ctx, msg, k.user.uid.unwrap()) k.sys.Ensure(share.sc.SharePath.String(), 0711) { @@ -357,7 +357,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { // outer wayland socket (usually `/run/user/%d/wayland-%d`) var socketPath *container.Absolute if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok { - hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) + msg.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) } else if a, err := container.NewAbs(name); err != nil { socketPath = share.sc.RuntimePath.Append(name) @@ -379,7 +379,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String()) k.container.Bind(outerPath, innerPath, 0) } else { // bind mount wayland socket (insecure) - hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") + msg.Verbose("direct wayland access, PROCEED WITH CAUTION") share.ensureRuntimeDir() k.container.Bind(socketPath, innerPath, 0) k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) @@ -520,7 +520,7 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { k.container.PlaceP(innerDst, &payload) k.sys.CopyFile(payload, paCookiePath.String(), 256, 256) } else { - hlog.Verbose("cannot locate PulseAudio cookie (tried " + + msg.Verbose("cannot locate PulseAudio cookie (tried " + "$PULSE_COOKIE, " + "$XDG_CONFIG_HOME/pulse/cookie, " + "$HOME/.pulse-cookie)") @@ -596,8 +596,8 @@ func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { } slices.Sort(k.container.Env) - if hlog.Load() { - hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d", + if msg.IsVerbose() { + msg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d", k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops)) } diff --git a/internal/app/hsu.go b/internal/app/hsu.go index 0078ef0..93b6bb8 100644 --- a/internal/app/hsu.go +++ b/internal/app/hsu.go @@ -11,7 +11,6 @@ import ( "hakurei.app/container" "hakurei.app/hst" - "hakurei.app/internal/hlog" ) // Hsu caches responses from cmd/hsu. @@ -40,7 +39,7 @@ func (h *Hsu) ID() (int, error) { h.ensureDispatcher() h.idOnce.Do(func() { h.id = -1 - hsuPath := h.k.mustHsuPath() + hsuPath := h.k.mustHsuPath().String() cmd := exec.Command(hsuPath) cmd.Path = hsuPath @@ -71,7 +70,10 @@ func (h *Hsu) ID() (int, error) { } // MustID calls [Hsu.ID] and terminates on error. -func (h *Hsu) MustID() int { +func (h *Hsu) MustID() int { return h.MustIDMsg(nil) } + +// MustIDMsg implements MustID with a custom [container.Msg]. +func (h *Hsu) MustIDMsg(msg container.Msg) int { id, err := h.ID() if err == nil { return id @@ -79,7 +81,9 @@ func (h *Hsu) MustID() int { const fallback = "cannot retrieve user id from setuid wrapper:" if errors.Is(err, ErrHsuAccess) { - hlog.Verbose("*"+fallback, err) + if msg != nil { + msg.Verbose("*"+fallback, err) + } os.Exit(1) return -0xdeadbeef } else if m, ok := container.GetErrorMessage(err); ok { diff --git a/internal/app/paths.go b/internal/app/paths.go index 3316a22..442317c 100644 --- a/internal/app/paths.go +++ b/internal/app/paths.go @@ -8,10 +8,10 @@ import ( ) // CopyPaths populates a [hst.Paths] struct. -func CopyPaths(v *hst.Paths, userid int) { copyPaths(direct{}, v, userid) } +func CopyPaths(msg container.Msg, v *hst.Paths, userid int) { copyPaths(direct{}, msg, v, userid) } // copyPaths populates a [hst.Paths] struct. -func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) { +func copyPaths(k syscallDispatcher, msg container.Msg, v *hst.Paths, userid int) { const xdgRuntimeDir = "XDG_RUNTIME_DIR" if tempDir, err := container.NewAbs(k.tempdir()); err != nil { @@ -21,7 +21,7 @@ func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) { } v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid)) - k.verbosef("process share directory at %q", v.SharePath) + msg.Verbosef("process share directory at %q", v.SharePath) r, _ := k.lookupEnv(xdgRuntimeDir) if a, err := container.NewAbs(r); err != nil { @@ -32,5 +32,5 @@ func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) { v.RuntimePath = a v.RunDirPath = v.RuntimePath.Append("hakurei") } - k.verbosef("runtime directory at %q", v.RunDirPath) + msg.Verbosef("runtime directory at %q", v.RunDirPath) } diff --git a/internal/app/process.go b/internal/app/process.go index 8ac0585..1b9c037 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -15,7 +15,6 @@ import ( "hakurei.app/container" "hakurei.app/internal" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" "hakurei.app/system" ) @@ -39,6 +38,7 @@ type mainState struct { cmdWait chan error k *outcome + container.Msg uintptr } @@ -55,7 +55,7 @@ func (ms mainState) beforeExit(isFault bool) { panic("attempting to call beforeExit twice") } ms.done = true - defer hlog.BeforeExit() + defer ms.BeforeExit() if isFault && ms.cancel != nil { ms.cancel() @@ -97,37 +97,37 @@ func (ms mainState) beforeExit(isFault bool) { } } - if hlog.Load() { + if ms.IsVerbose() { if !ok { if err != nil { - hlog.Verbosef("wait: %v", err) + ms.Verbosef("wait: %v", err) } } else { switch { case wstatus.Exited(): - hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus()) + ms.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus()) case wstatus.CoreDump(): - hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid) + ms.Verbosef("process %d dumped core", ms.cmd.Process.Pid) case wstatus.Signaled(): - hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal()) + ms.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal()) default: - hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus) + ms.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus) } } } case <-waitDone: - hlog.Resume() + ms.Resume() // this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed. // This is different from the container failing to terminate within its timeout period, as that is enforced // by the shim. This path is instead reached when there is a lockup in shim preventing it from completing. log.Printf("process %d did not terminate", ms.cmd.Process.Pid) } - hlog.Resume() + ms.Resume() if ms.k.sync != nil { if err := ms.k.sync.Close(); err != nil { perror(err, "close wayland security context") @@ -170,7 +170,7 @@ func (ms mainState) beforeExit(isFault bool) { if l := len(states); l == 0 { ec |= system.User } else { - hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l) + ms.Verbosef("found %d instances, cleaning up without user-scoped operations", l) } // accumulate enablements of remaining launchers @@ -183,9 +183,9 @@ func (ms mainState) beforeExit(isFault bool) { } ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse) - if hlog.Load() { + if ms.IsVerbose() { if ec > 0 { - hlog.Verbose("reverting operations scope", system.TypeString(ec)) + ms.Verbose("reverting operations scope", system.TypeString(ec)) } } @@ -219,7 +219,7 @@ func (ms mainState) fatal(fallback string, ferr error) { } // main carries out outcome and terminates. main does not return. -func (k *outcome) main() { +func (k *outcome) main(msg container.Msg) { if !k.active.CompareAndSwap(false, true) { panic("outcome: attempted to run twice") } @@ -228,19 +228,19 @@ func (k *outcome) main() { hsuPath := internal.MustHsuPath() // ms.beforeExit required beyond this point - ms := &mainState{k: k} + ms := &mainState{Msg: msg, k: k} if err := k.sys.Commit(); err != nil { ms.fatal("cannot commit system setup:", err) } ms.uintptr |= mainNeedsRevert - ms.store = state.NewMulti(k.runDirPath.String()) + ms.store = state.NewMulti(msg, k.runDirPath.String()) ctx, cancel := context.WithCancel(k.ctx) defer cancel() ms.cancel = cancel - ms.cmd = exec.CommandContext(ctx, hsuPath) + ms.cmd = exec.CommandContext(ctx, hsuPath.String()) ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr ms.cmd.Dir = container.FHSRoot // container init enters final working directory // shim runs in the same session as monitor; see shim.go for behaviour @@ -260,13 +260,13 @@ func (k *outcome) main() { } if len(k.user.supp) > 0 { - hlog.Verbosef("attaching supplementary group ids %s", k.user.supp) + msg.Verbosef("attaching supplementary group ids %s", k.user.supp) // interpreted by hsu ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.user.supp, " ")) } - hlog.Verbosef("setuid helper at %s", hsuPath) - hlog.Suspend() + msg.Verbosef("setuid helper at %s", hsuPath) + msg.Suspend() if err := ms.cmd.Start(); err != nil { ms.fatal("cannot start setuid wrapper:", err) } @@ -286,18 +286,18 @@ func (k *outcome) main() { os.Getpid(), k.waitDelay, k.container, - hlog.Load(), + msg.IsVerbose(), }) }() return }(): if err != nil { - hlog.Resume() + msg.Resume() ms.fatal("cannot transmit shim config:", err) } case <-ctx.Done(): - hlog.Resume() + msg.Resume() ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err())) } diff --git a/internal/app/shim.go b/internal/app/shim.go index e38104c..8c860e1 100644 --- a/internal/app/shim.go +++ b/internal/app/shim.go @@ -15,8 +15,6 @@ import ( "hakurei.app/container" "hakurei.app/container/seccomp" - "hakurei.app/internal" - "hakurei.app/internal/hlog" ) //#include "shim-signal.h" @@ -53,7 +51,9 @@ const ( // ShimMain is the main function of the shim process and runs as the unconstrained target user. func ShimMain() { - hlog.Prepare("shim") + log.SetPrefix("shim: ") + log.SetFlags(0) + msg := container.NewMsg(log.Default()) if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil { log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) @@ -73,7 +73,7 @@ func ShimMain() { log.Fatalf("cannot receive shim setup params: %v", err) } else { - internal.InstallOutput(params.Verbose) + msg.SwapVerbose(params.Verbose) closeSetup = f } @@ -111,12 +111,12 @@ func ShimMain() { } // setup has not completed, terminate immediately - hlog.Resume() + msg.Resume() os.Exit(ShimExitRequest) return case 1: // got SIGCONT after adoption: monitor died before delivering signal - hlog.BeforeExit() + msg.BeforeExit() os.Exit(ShimExitOrphan) return @@ -144,7 +144,7 @@ func ShimMain() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) cancelContainer.Store(&stop) - z := container.New(ctx) + z := container.New(ctx, msg) z.Params = *params.Container z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr diff --git a/internal/app/state/multi.go b/internal/app/state/multi.go index 9a400ac..822cddc 100644 --- a/internal/app/state/multi.go +++ b/internal/app/state/multi.go @@ -13,8 +13,8 @@ import ( "sync" "syscall" + "hakurei.app/container" "hakurei.app/hst" - "hakurei.app/internal/hlog" ) // fine-grained locking and access @@ -24,16 +24,17 @@ type multiStore struct { // initialised backends backends *sync.Map - lock sync.RWMutex + msg container.Msg + mu sync.RWMutex } func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { - s.lock.RLock() - defer s.lock.RUnlock() + s.mu.RLock() + defer s.mu.RUnlock() // load or initialise new backend b := new(multiBackend) - b.lock.Lock() + b.mu.Lock() if v, ok := s.backends.LoadOrStore(identity, b); ok { b = v.(*multiBackend) } else { @@ -52,7 +53,7 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { } else { b.lockfile = l } - b.lock.Unlock() + b.mu.Unlock() } // lock backend @@ -85,17 +86,17 @@ func (s *multiStore) List() ([]int, error) { for _, e := range entries { // skip non-directories if !e.IsDir() { - hlog.Verbosef("skipped non-directory entry %q", e.Name()) + s.msg.Verbosef("skipped non-directory entry %q", e.Name()) continue } // skip non-numerical names if v, err := strconv.Atoi(e.Name()); err != nil { - hlog.Verbosef("skipped non-aid entry %q", e.Name()) + s.msg.Verbosef("skipped non-aid entry %q", e.Name()) continue } else { if v < 0 || v > 9999 { - hlog.Verbosef("skipped out of bounds entry %q", e.Name()) + s.msg.Verbosef("skipped out of bounds entry %q", e.Name()) continue } @@ -107,8 +108,8 @@ func (s *multiStore) List() ([]int, error) { } func (s *multiStore) Close() error { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() var errs []error s.backends.Range(func(_, value any) bool { @@ -126,7 +127,7 @@ type multiBackend struct { // created/opened by prepare lockfile *os.File - lock sync.RWMutex + mu sync.RWMutex } func (b *multiBackend) filename(id *ID) string { @@ -169,8 +170,8 @@ func (b *multiBackend) unlockFile() error { // reads all launchers in simpleBackend // file contents are ignored if decode is false func (b *multiBackend) load(decode bool) (Entries, error) { - b.lock.RLock() - defer b.lock.RUnlock() + b.mu.RLock() + defer b.mu.RUnlock() // read directory contents, should only contain files named after ids var entries []os.DirEntry @@ -280,8 +281,8 @@ func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error { // Save writes process state to filesystem func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error { - b.lock.Lock() - defer b.lock.Unlock() + b.mu.Lock() + defer b.mu.Unlock() if configWriter == nil && state.Config == nil { return ErrNoConfig @@ -336,8 +337,8 @@ func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter } func (b *multiBackend) Destroy(id ID) error { - b.lock.Lock() - defer b.lock.Unlock() + b.mu.Lock() + defer b.mu.Unlock() return os.Remove(b.filename(&id)) } @@ -353,8 +354,8 @@ func (b *multiBackend) Len() (int, error) { } func (b *multiBackend) close() error { - b.lock.Lock() - defer b.lock.Unlock() + b.mu.Lock() + defer b.mu.Unlock() err := b.lockfile.Close() if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) { @@ -364,9 +365,10 @@ func (b *multiBackend) close() error { } // NewMulti returns an instance of the multi-file store. -func NewMulti(runDir string) Store { - b := new(multiStore) - b.base = path.Join(runDir, "state") - b.backends = new(sync.Map) - return b +func NewMulti(msg container.Msg, runDir string) Store { + return &multiStore{ + msg: msg, + base: path.Join(runDir, "state"), + backends: new(sync.Map), + } } diff --git a/internal/app/state/multi_test.go b/internal/app/state/multi_test.go index d493a38..b8e1db9 100644 --- a/internal/app/state/multi_test.go +++ b/internal/app/state/multi_test.go @@ -1,9 +1,13 @@ package state_test import ( + "log" "testing" + "hakurei.app/container" "hakurei.app/internal/app/state" ) -func TestMulti(t *testing.T) { testStore(t, state.NewMulti(t.TempDir())) } +func TestMulti(t *testing.T) { + testStore(t, state.NewMulti(container.NewMsg(log.New(log.Writer(), "multi: ", 0)), t.TempDir())) +} diff --git a/internal/app/state/state.go b/internal/app/state/state.go index 6a3a82d..59fbc8c 100644 --- a/internal/app/state/state.go +++ b/internal/app/state/state.go @@ -19,9 +19,9 @@ type Store interface { // Cursor provided to f becomes invalid as soon as f returns. Do(identity int, f func(c Cursor)) (ok bool, err error) - // List queries the store and returns a list of aids known to the store. - // Note that some or all returned aids might not have any active apps. - List() (aids []int, err error) + // List queries the store and returns a list of identities known to the store. + // Note that some or all returned identities might not have any active apps. + List() (identities []int, err error) // Close releases any resources held by Store. Close() error diff --git a/internal/app/state/state_test.go b/internal/app/state/state_test.go index 0025918..cc57865 100644 --- a/internal/app/state/state_test.go +++ b/internal/app/state/state_test.go @@ -16,10 +16,10 @@ import ( func testStore(t *testing.T, s state.Store) { t.Run("list empty store", func(t *testing.T) { - if aids, err := s.List(); err != nil { + if identities, err := s.List(); err != nil { t.Fatalf("List: error = %v", err) - } else if len(aids) != 0 { - t.Fatalf("List: aids = %#v", aids) + } else if len(identities) != 0 { + t.Fatalf("List: identities = %#v", identities) } }) @@ -39,22 +39,22 @@ func testStore(t *testing.T, s state.Store) { makeState(t, &tc[i].state, &tc[i].ct) } - do := func(aid int, f func(c state.Cursor)) { - if ok, err := s.Do(aid, f); err != nil { + do := func(identity int, f func(c state.Cursor)) { + if ok, err := s.Do(identity, f); err != nil { t.Fatalf("Do: ok = %v, error = %v", ok, err) } } - insert := func(i, aid int) { - do(aid, func(c state.Cursor) { + insert := func(i, identity int) { + do(identity, func(c state.Cursor) { if err := c.Save(&tc[i].state, &tc[i].ct); err != nil { t.Fatalf("Save(&tc[%v]): error = %v", i, err) } }) } - check := func(i, aid int) { - do(aid, func(c state.Cursor) { + check := func(i, identity int) { + do(identity, func(c state.Cursor) { if entries, err := c.Load(); err != nil { t.Fatalf("Load: error = %v", err) } else if got, ok := entries[tc[i].state.ID]; !ok { @@ -81,7 +81,7 @@ func testStore(t *testing.T, s state.Store) { insert(insertEntryNoCheck, 0) }) - t.Run("insert entry different aid", func(t *testing.T) { + t.Run("insert entry different identity", func(t *testing.T) { insert(insertEntryOtherApp, 1) check(insertEntryOtherApp, 1) }) @@ -90,14 +90,14 @@ func testStore(t *testing.T, s state.Store) { check(insertEntryNoCheck, 0) }) - t.Run("list aids", func(t *testing.T) { - if aids, err := s.List(); err != nil { + t.Run("list identities", func(t *testing.T) { + if identities, err := s.List(); err != nil { t.Fatalf("List: error = %v", err) } else { - slices.Sort(aids) + slices.Sort(identities) want := []int{0, 1} - if !slices.Equal(aids, want) { - t.Fatalf("List() = %#v, want %#v", aids, want) + if !slices.Equal(identities, want) { + t.Fatalf("List() = %#v, want %#v", identities, want) } } }) @@ -110,7 +110,7 @@ func testStore(t *testing.T, s state.Store) { } }) - t.Run("clear aid 1", func(t *testing.T) { + t.Run("clear identity 1", func(t *testing.T) { do(1, func(c state.Cursor) { if err := c.Destroy(tc[insertEntryOtherApp].state.ID); err != nil { t.Fatalf("Destroy: error = %v", err) diff --git a/internal/exit.go b/internal/exit.go deleted file mode 100644 index fe2a7ce..0000000 --- a/internal/exit.go +++ /dev/null @@ -1,9 +0,0 @@ -package internal - -import ( - "os" - - "hakurei.app/internal/hlog" -) - -func Exit(code int) { hlog.BeforeExit(); os.Exit(code) } diff --git a/internal/output.go b/internal/output.go deleted file mode 100644 index 1d2b0d8..0000000 --- a/internal/output.go +++ /dev/null @@ -1,13 +0,0 @@ -package internal - -import ( - "hakurei.app/container" - "hakurei.app/internal/hlog" - "hakurei.app/system" -) - -func InstallOutput(verbose bool) { - hlog.Store(verbose) - container.SetOutput(hlog.Output{}) - system.SetOutput(hlog.Output{}) -} diff --git a/internal/path.go b/internal/path.go index 81af5a2..fca0fdf 100644 --- a/internal/path.go +++ b/internal/path.go @@ -2,9 +2,8 @@ package internal import ( "log" - "path" - "hakurei.app/internal/hlog" + "hakurei.app/container" ) var ( @@ -12,22 +11,23 @@ var ( hsu = compPoison ) -func MustHakureiPath() string { - if name, ok := checkPath(hmain); ok { - return name - } - hlog.BeforeExit() - log.Fatal("invalid hakurei path, this program is compiled incorrectly") - return compPoison // unreachable -} +// MustHakureiPath returns the absolute path to hakurei, configured at compile time. +func MustHakureiPath() *container.Absolute { return mustCheckPath(log.Fatal, "hakurei", hmain) } -func MustHsuPath() string { - if name, ok := checkPath(hsu); ok { - return name - } - hlog.BeforeExit() - log.Fatal("invalid hsu path, this program is compiled incorrectly") - return compPoison // unreachable -} +// MustHsuPath returns the absolute path to hakurei, configured at compile time. +func MustHsuPath() *container.Absolute { return mustCheckPath(log.Fatal, "hsu", hsu) } -func checkPath(p string) (string, bool) { return p, p != compPoison && p != "" && path.IsAbs(p) } +// mustCheckPath checks a pathname against compPoison, then [container.NewAbs], calling fatal if either step fails. +func mustCheckPath(fatal func(v ...any), name, pathname string) *container.Absolute { + if pathname != compPoison && pathname != "" { + if a, err := container.NewAbs(pathname); err != nil { + fatal(err.Error()) + return nil // unreachable + } else { + return a + } + } else { + fatal("invalid " + name + " path, this program is compiled incorrectly") + return nil // unreachable + } +} diff --git a/internal/path_test.go b/internal/path_test.go new file mode 100644 index 0000000..97a8f92 --- /dev/null +++ b/internal/path_test.go @@ -0,0 +1,43 @@ +package internal + +import ( + "reflect" + "testing" + + "hakurei.app/container" +) + +func TestMustCheckPath(t *testing.T) { + testCases := []struct { + name string + pathname string + wantFatal string + }{ + {"poison", compPoison, "invalid test path, this program is compiled incorrectly"}, + {"zero", "", "invalid test path, this program is compiled incorrectly"}, + {"not absolute", "\x00", `path "\x00" is not absolute`}, + {"success", "/proc/nonexistent", ""}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fatal := func(v ...any) { t.Fatal(append([]any{"invalid call to fatal:"}, v...)...) } + if tc.wantFatal != "" { + fatal = func(v ...any) { + if len(v) != 1 { + t.Errorf("mustCheckPath: fatal %#v", v) + } else if gotFatal, ok := v[0].(string); !ok { + t.Errorf("mustCheckPath: fatal = %#v", v[0]) + } else if gotFatal != tc.wantFatal { + t.Errorf("mustCheckPath: fatal = %q, want %q", gotFatal, tc.wantFatal) + } + + // do not simulate exit + } + } + + if got := mustCheckPath(fatal, "test", tc.pathname); got != nil && !reflect.DeepEqual(got, container.MustAbs(tc.pathname)) { + t.Errorf("mustCheckPath: %q", got) + } + }) + } +} diff --git a/ldd/exec.go b/ldd/exec.go index 79083d4..bdd8fa9 100644 --- a/ldd/exec.go +++ b/ldd/exec.go @@ -22,7 +22,7 @@ var ( msgStaticGlibc = []byte("not a dynamic executable") ) -func Exec(ctx context.Context, p string) ([]*Entry, error) { +func Exec(ctx context.Context, msg container.Msg, p string) ([]*Entry, error) { c, cancel := context.WithTimeout(ctx, lddTimeout) defer cancel() @@ -33,7 +33,7 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) { return nil, err } - z := container.NewCommand(c, toolPath, lddName, p) + z := container.NewCommand(c, msg, toolPath, lddName, p) z.Hostname = "hakurei-" + lddName z.SeccompFlags |= seccomp.AllowMultiarch z.SeccompPresets |= seccomp.PresetStrict diff --git a/system/acl.go b/system/acl.go index b49b3df..f65ba91 100644 --- a/system/acl.go +++ b/system/acl.go @@ -31,22 +31,22 @@ type aclUpdateOp struct { func (a *aclUpdateOp) Type() Enablement { return a.et } func (a *aclUpdateOp) apply(sys *I) error { - sys.verbose("applying ACL", a) + sys.msg.Verbose("applying ACL", a) return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false) } func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error { if ec.hasType(a.Type()) { - sys.verbose("stripping ACL", a) + sys.msg.Verbose("stripping ACL", a) err := sys.aclUpdate(a.path, sys.uid) if errors.Is(err, os.ErrNotExist) { // the ACL is effectively stripped if the file no longer exists - sys.verbosef("target of ACL %s no longer exists", a) + sys.msg.Verbosef("target of ACL %s no longer exists", a) err = nil } return newOpError("acl", err, true) } else { - sys.verbose("skipping ACL", a) + sys.msg.Verbose("skipping ACL", a) return nil } } diff --git a/system/dbus.go b/system/dbus.go index de52e78..97c567a 100644 --- a/system/dbus.go +++ b/system/dbus.go @@ -54,14 +54,14 @@ func (sys *I) ProxyDBus(session, system *dbus.Config, sessionPath, systemPath st return nil, newOpErrorMessage("dbus", err, fmt.Sprintf("cannot finalise message bus proxy: %v", err), false) } else { - if sys.isVerbose() { - sys.verbose("session bus proxy:", session.Args(sessionBus)) + if sys.msg.IsVerbose() { + sys.msg.Verbose("session bus proxy:", session.Args(sessionBus)) if system != nil { - sys.verbose("system bus proxy:", system.Args(systemBus)) + sys.msg.Verbose("system bus proxy:", system.Args(systemBus)) } // this calls the argsWt String method - sys.verbose("message bus proxy final args:", final.WriterTo) + sys.msg.Verbose("message bus proxy final args:", final.WriterTo) } d.final = final @@ -84,28 +84,28 @@ type dbusProxyOp struct { func (d *dbusProxyOp) Type() Enablement { return Process } func (d *dbusProxyOp) apply(sys *I) error { - sys.verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0]) + sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0]) if d.system { - sys.verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0]) + sys.msg.Verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0]) } - d.proxy = dbus.New(sys.ctx, d.final, d.out) + d.proxy = dbus.New(sys.ctx, sys.msg, d.final, d.out) if err := sys.dbusProxyStart(d.proxy); err != nil { d.out.Dump() return newOpErrorMessage("dbus", err, fmt.Sprintf("cannot start message bus proxy: %v", err), false) } - sys.verbose("starting message bus proxy", d.proxy) + sys.msg.Verbose("starting message bus proxy", d.proxy) return nil } func (d *dbusProxyOp) revert(sys *I, _ *Criteria) error { // criteria ignored here since dbus is always process-scoped - sys.verbose("terminating message bus proxy") + sys.msg.Verbose("terminating message bus proxy") sys.dbusProxyClose(d.proxy) exitMessage := "message bus proxy exit" - defer func() { sys.verbose(exitMessage) }() + defer func() { sys.msg.Verbose(exitMessage) }() err := sys.dbusProxyWait(d.proxy) if errors.Is(err, context.Canceled) { diff --git a/system/dbus/dbus_test.go b/system/dbus/dbus_test.go index 9d969ba..2599ef8 100644 --- a/system/dbus/dbus_test.go +++ b/system/dbus/dbus_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "hakurei.app/container" "hakurei.app/helper" "hakurei.app/system/dbus" ) @@ -92,9 +93,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { t.Run("invalid start", func(t *testing.T) { if !useSandbox { - p = dbus.NewDirect(t.Context(), nil, nil) + p = dbus.NewDirect(t.Context(), container.NewMsg(nil), nil, nil) } else { - p = dbus.New(t.Context(), nil, nil) + p = dbus.New(t.Context(), container.NewMsg(nil), nil, nil) } if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) { @@ -127,9 +128,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { defer cancel() output := new(strings.Builder) if !useSandbox { - p = dbus.NewDirect(ctx, final, output) + p = dbus.NewDirect(ctx, container.NewMsg(nil), final, output) } else { - p = dbus.New(ctx, final, output) + p = dbus.New(ctx, container.NewMsg(nil), final, output) } t.Run("invalid wait", func(t *testing.T) { diff --git a/system/dbus/export_test.go b/system/dbus/export_test.go index 574a4a7..b1dc365 100644 --- a/system/dbus/export_test.go +++ b/system/dbus/export_test.go @@ -3,11 +3,13 @@ package dbus import ( "context" "io" + + "hakurei.app/container" ) // NewDirect returns a new instance of [Proxy] with its sandbox disabled. -func NewDirect(ctx context.Context, final *Final, output io.Writer) *Proxy { - p := New(ctx, final, output) +func NewDirect(ctx context.Context, msg container.Msg, final *Final, output io.Writer) *Proxy { + p := New(ctx, msg, final, output) p.useSandbox = false return p } diff --git a/system/dbus/proc.go b/system/dbus/proc.go index 7c98282..24f8c40 100644 --- a/system/dbus/proc.go +++ b/system/dbus/proc.go @@ -52,14 +52,14 @@ func (p *Proxy) Start() error { } var libPaths []*container.Absolute - if entries, err := ldd.Exec(ctx, toolPath.String()); err != nil { + if entries, err := ldd.Exec(ctx, p.msg, toolPath.String()); err != nil { return err } else { libPaths = ldd.Path(entries) } p.helper = helper.New( - ctx, toolPath, "xdg-dbus-proxy", + ctx, p.msg, toolPath, "xdg-dbus-proxy", p.final, true, argF, func(z *container.Container) { z.SeccompFlags |= seccomp.AllowMultiarch diff --git a/system/dbus/proc_test.go b/system/dbus/proc_test.go index 5e93ace..8b41c04 100644 --- a/system/dbus/proc_test.go +++ b/system/dbus/proc_test.go @@ -6,12 +6,10 @@ import ( "hakurei.app/container" "hakurei.app/helper" - "hakurei.app/internal" - "hakurei.app/internal/hlog" ) func TestMain(m *testing.M) { - container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) + container.TryArgv0(nil) helper.InternalHelperStub() os.Exit(m.Run()) } diff --git a/system/dbus/proxy.go b/system/dbus/proxy.go index 2f625f6..a83bc9c 100644 --- a/system/dbus/proxy.go +++ b/system/dbus/proxy.go @@ -7,6 +7,7 @@ import ( "sync" "syscall" + "hakurei.app/container" "hakurei.app/helper" ) @@ -27,6 +28,7 @@ func (e *BadInterfaceError) Error() string { type Proxy struct { helper helper.Helper ctx context.Context + msg container.Msg cancel context.CancelCauseFunc cause func() error @@ -107,6 +109,6 @@ func Finalise(sessionBus, systemBus ProxyPair, session, system *Config) (final * } // New returns a new instance of [Proxy]. -func New(ctx context.Context, final *Final, output io.Writer) *Proxy { - return &Proxy{name: ProxyName, ctx: ctx, final: final, output: output, useSandbox: true} +func New(ctx context.Context, msg container.Msg, final *Final, output io.Writer) *Proxy { + return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true} } diff --git a/system/dispatcher.go b/system/dispatcher.go index 36defd1..38aab88 100644 --- a/system/dispatcher.go +++ b/system/dispatcher.go @@ -57,10 +57,6 @@ type syscallDispatcher interface { dbusProxyClose(proxy *dbus.Proxy) // dbusProxyWait provides the Wait method of [dbus.Proxy]. dbusProxyWait(proxy *dbus.Proxy) error - - isVerbose() bool - verbose(v ...any) - verbosef(format string, v ...any) } // direct implements syscallDispatcher on the current kernel. @@ -96,7 +92,3 @@ func (k direct) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, syst func (k direct) dbusProxyStart(proxy *dbus.Proxy) error { return proxy.Start() } func (k direct) dbusProxyClose(proxy *dbus.Proxy) { proxy.Close() } func (k direct) dbusProxyWait(proxy *dbus.Proxy) error { return proxy.Wait() } - -func (k direct) isVerbose() bool { return msg.IsVerbose() } -func (direct) verbose(v ...any) { msg.Verbose(v...) } -func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) } diff --git a/system/dispatcher_test.go b/system/dispatcher_test.go index 9676929..d83b46b 100644 --- a/system/dispatcher_test.go +++ b/system/dispatcher_test.go @@ -3,6 +3,7 @@ package system import ( "io" "io/fs" + "log" "os" "reflect" "slices" @@ -214,10 +215,10 @@ func (r *readerOsFile) Close() error { // InternalNew initialises [I] with a stub syscallDispatcher. func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) { - k := stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want) - sys := New(t.Context(), uid) - sys.syscallDispatcher = &kstub{k} - return sys, k + k := &kstub{stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)} + sys := New(t.Context(), k, uid) + sys.syscallDispatcher = k + return sys, k.Stub } type kstub struct{ *stub.Stub[syscallDispatcher] } @@ -357,12 +358,23 @@ func (k *kstub) dbusProxySCW(expect *stub.Call, proxy *dbus.Proxy) error { return expect.Err } -func (k *kstub) isVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) } +func (k *kstub) GetLogger() *log.Logger { panic("unreachable") } + +func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) } +func (k *kstub) SwapVerbose(verbose bool) bool { + k.Helper() + expect := k.Expects("swapVerbose") + if expect.Error( + stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil { + k.FailNow() + } + return expect.Ret.(bool) +} // ignoreValue marks a value to be ignored by the test suite. type ignoreValue struct{} -func (k *kstub) verbose(v ...any) { +func (k *kstub) Verbose(v ...any) { k.Helper() expect := k.Expects("verbose") @@ -381,7 +393,7 @@ func (k *kstub) verbose(v ...any) { } } -func (k *kstub) verbosef(format string, v ...any) { +func (k *kstub) Verbosef(format string, v ...any) { k.Helper() if k.Expects("verbosef").Error( stub.CheckArg(k.Stub, "format", format, 0), @@ -389,3 +401,7 @@ func (k *kstub) verbosef(format string, v ...any) { k.FailNow() } } + +func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) } +func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) } +func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") } diff --git a/system/link.go b/system/link.go index 2eb658a..d463b62 100644 --- a/system/link.go +++ b/system/link.go @@ -22,16 +22,16 @@ type hardlinkOp struct { func (l *hardlinkOp) Type() Enablement { return l.et } func (l *hardlinkOp) apply(sys *I) error { - sys.verbose("linking", l) + sys.msg.Verbose("linking", l) return newOpError("hardlink", sys.link(l.src, l.dst), false) } func (l *hardlinkOp) revert(sys *I, ec *Criteria) error { if ec.hasType(l.Type()) { - sys.verbosef("removing hard link %q", l.dst) + sys.msg.Verbosef("removing hard link %q", l.dst) return newOpError("hardlink", sys.remove(l.dst), true) } else { - sys.verbosef("skipping hard link %q", l.dst) + sys.msg.Verbosef("skipping hard link %q", l.dst) return nil } } diff --git a/system/mkdir.go b/system/mkdir.go index 97673f0..c4e6163 100644 --- a/system/mkdir.go +++ b/system/mkdir.go @@ -29,7 +29,7 @@ type mkdirOp struct { func (m *mkdirOp) Type() Enablement { return m.et } func (m *mkdirOp) apply(sys *I) error { - sys.verbose("ensuring directory", m) + sys.msg.Verbose("ensuring directory", m) if err := sys.mkdir(m.path, m.perm); err != nil { if !errors.Is(err, os.ErrExist) { @@ -49,10 +49,10 @@ func (m *mkdirOp) revert(sys *I, ec *Criteria) error { } if ec.hasType(m.Type()) { - sys.verbose("destroying ephemeral directory", m) + sys.msg.Verbose("destroying ephemeral directory", m) return newOpError("mkdir", sys.remove(m.path), true) } else { - sys.verbose("skipping ephemeral directory", m) + sys.msg.Verbose("skipping ephemeral directory", m) return nil } } diff --git a/system/output.go b/system/output.go index f183ebf..022164f 100644 --- a/system/output.go +++ b/system/output.go @@ -8,16 +8,6 @@ import ( "hakurei.app/container" ) -var msg container.Msg = new(container.DefaultMsg) - -func SetOutput(v container.Msg) { - if v == nil { - msg = new(container.DefaultMsg) - } else { - msg = v - } -} - // OpError is returned by [I.Commit] and [I.Revert]. type OpError struct { Op string diff --git a/system/output_test.go b/system/output_test.go index 80abcf8..b144d79 100644 --- a/system/output_test.go +++ b/system/output_test.go @@ -9,7 +9,6 @@ import ( "testing" "hakurei.app/container" - "hakurei.app/internal/hlog" ) func TestOpError(t *testing.T) { @@ -87,33 +86,6 @@ func TestOpError(t *testing.T) { }) } -func TestSetOutput(t *testing.T) { - oldmsg := msg - t.Cleanup(func() { msg = oldmsg }) - msg = nil - - t.Run("nil", func(t *testing.T) { - SetOutput(nil) - if _, ok := msg.(*container.DefaultMsg); !ok { - t.Errorf("SetOutput: %#v", msg) - } - }) - - t.Run("hlog", func(t *testing.T) { - SetOutput(hlog.Output{}) - if _, ok := msg.(hlog.Output); !ok { - t.Errorf("SetOutput: %#v", msg) - } - }) - - t.Run("reset", func(t *testing.T) { - SetOutput(nil) - if _, ok := msg.(*container.DefaultMsg); !ok { - t.Errorf("SetOutput: %#v", msg) - } - }) -} - func TestPrintJoinedError(t *testing.T) { testCases := []struct { name string diff --git a/system/system.go b/system/system.go index ea6d9d5..0d16a14 100644 --- a/system/system.go +++ b/system/system.go @@ -5,6 +5,8 @@ import ( "context" "errors" "strings" + + "hakurei.app/container" ) const ( @@ -65,11 +67,11 @@ func TypeString(e Enablement) string { } // New returns the address of a new [I] targeting uid. -func New(ctx context.Context, uid int) (sys *I) { - if ctx == nil || uid < 0 { +func New(ctx context.Context, msg container.Msg, uid int) (sys *I) { + if ctx == nil || msg == nil || uid < 0 { panic("invalid call to New") } - return &I{ctx: ctx, uid: uid, syscallDispatcher: direct{}} + return &I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}} } // An I provides deferred operating system interaction. [I] must not be copied. @@ -86,6 +88,7 @@ type I struct { // the behaviour of Revert is only defined for up to one call reverted bool + msg container.Msg syscallDispatcher } @@ -114,14 +117,14 @@ func (sys *I) Commit() error { } sys.committed = true - sp := New(sys.ctx, sys.uid) + sp := New(sys.ctx, sys.msg, sys.uid) sp.syscallDispatcher = sys.syscallDispatcher sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits defer func() { // sp is set to nil when all ops are applied if sp != nil { // rollback partial commit - sys.verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) + sys.msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) if err := sp.Revert(nil); err != nil { printJoinedError(sys.println, "cannot revert partial commit:", err) } diff --git a/system/system_test.go b/system/system_test.go index 12eb6fe..73b70fc 100644 --- a/system/system_test.go +++ b/system/system_test.go @@ -8,6 +8,7 @@ import ( "strconv" "testing" + "hakurei.app/container" "hakurei.app/container/stub" "hakurei.app/system/internal/xcb" ) @@ -71,7 +72,17 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(nil, 0) + New(nil, container.NewMsg(nil), 0) + }) + + t.Run("msg", func(t *testing.T) { + defer func() { + want := "invalid call to New" + if r := recover(); r != want { + t.Errorf("recover: %v, want %v", r, want) + } + }() + New(t.Context(), nil, 0) }) t.Run("uid", func(t *testing.T) { @@ -81,11 +92,11 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(t.Context(), -1) + New(t.Context(), container.NewMsg(nil), -1) }) }) - sys := New(t.Context(), 0xdeadbeef) + sys := New(t.Context(), container.NewMsg(nil), 0xdeadbeef) if sys.ctx == nil { t.Error("New: ctx = nil") } @@ -102,51 +113,51 @@ func TestEqual(t *testing.T) { want bool }{ {"simple UID", - New(t.Context(), 150), - New(t.Context(), 150), + New(t.Context(), container.NewMsg(nil), 150), + New(t.Context(), container.NewMsg(nil), 150), true}, {"simple UID differ", - New(t.Context(), 150), - New(t.Context(), 151), + New(t.Context(), container.NewMsg(nil), 150), + New(t.Context(), container.NewMsg(nil), 151), false}, {"simple UID nil", - New(t.Context(), 150), + New(t.Context(), container.NewMsg(nil), 150), nil, false}, {"op length mismatch", - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"), - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure("/run", 0755), false}, {"op value mismatch", - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure("/run", 0644), - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure("/run", 0755), false}, {"op type mismatch", - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 0, 256), - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure("/run", 0755), false}, {"op equals", - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure("/run", 0755), - New(t.Context(), 150). + New(t.Context(), container.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure("/run", 0755), true}, diff --git a/system/tmpfiles.go b/system/tmpfiles.go index d2a6c93..845b43a 100644 --- a/system/tmpfiles.go +++ b/system/tmpfiles.go @@ -34,7 +34,7 @@ func (t *tmpfileOp) apply(sys *I) error { return errors.New("invalid payload") } - sys.verbose("copying", t) + sys.msg.Verbose("copying", t) if b, err := sys.stat(t.src); err != nil { return newOpError("tmpfile", err, false) @@ -58,7 +58,7 @@ func (t *tmpfileOp) apply(sys *I) error { _ = r.Close() return newOpError("tmpfile", err, false) } - sys.verbosef("copied %d bytes from %q", n, t.src) + sys.msg.Verbosef("copied %d bytes from %q", n, t.src) } if err := r.Close(); err != nil { return newOpError("tmpfile", err, false) diff --git a/system/wayland.go b/system/wayland.go index 2dd0d98..5a87ab9 100644 --- a/system/wayland.go +++ b/system/wayland.go @@ -43,14 +43,14 @@ func (w *waylandOp) apply(sys *I) error { if err := w.conn.Attach(w.src); err != nil { return newOpError("wayland", err, false) } else { - sys.verbosef("wayland attached on %q", w.src) + sys.msg.Verbosef("wayland attached on %q", w.src) } if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil { return newOpError("wayland", err, false) } else { *w.sync = sp - sys.verbosef("wayland listening on %q", w.dst) + sys.msg.Verbosef("wayland listening on %q", w.dst) if err = sys.chmod(w.dst, 0); err != nil { return newOpError("wayland", err, false) } @@ -59,12 +59,12 @@ func (w *waylandOp) apply(sys *I) error { } func (w *waylandOp) revert(sys *I, _ *Criteria) error { - sys.verbosef("removing wayland socket on %q", w.dst) + sys.msg.Verbosef("removing wayland socket on %q", w.dst) if err := sys.remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) { return newOpError("wayland", err, true) } - sys.verbosef("detaching from wayland on %q", w.src) + sys.msg.Verbosef("detaching from wayland on %q", w.src) return newOpError("wayland", w.conn.Close(), true) } diff --git a/system/xhost.go b/system/xhost.go index 4ee4993..e5ea258 100644 --- a/system/xhost.go +++ b/system/xhost.go @@ -16,18 +16,18 @@ type xhostOp string func (x xhostOp) Type() Enablement { return EX11 } func (x xhostOp) apply(sys *I) error { - sys.verbosef("inserting entry %s to X11", x) + sys.msg.Verbosef("inserting entry %s to X11", x) return newOpError("xhost", sys.xcbChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false) } func (x xhostOp) revert(sys *I, ec *Criteria) error { if ec.hasType(x.Type()) { - sys.verbosef("deleting entry %s from X11", x) + sys.msg.Verbosef("deleting entry %s from X11", x) return newOpError("xhost", sys.xcbChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), true) } else { - sys.verbosef("skipping entry %s in X11", x) + sys.msg.Verbosef("skipping entry %s in X11", x) return nil } }