From 09d284498109b847da0da1e2c5f883268a7f2d46 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 21 Aug 2025 21:59:07 +0900 Subject: [PATCH] container/init: wrap syscall helper functions This allows tests to stub all kernel behaviour, enabling measurement of all function call arguments and error injection. Signed-off-by: Ophestra --- container/autoetc.go | 24 +- container/autoetc_test.go | 247 +++++++++++- container/autoroot.go | 25 +- container/autoroot_test.go | 120 +++++- container/capability.go | 47 ++- container/dispatcher.go | 229 +++++++++++ container/dispatcher_test.go | 595 ++++++++++++++++++++++++++++ container/init.go | 239 ++++++------ container/initbind.go | 25 +- container/initbind_test.go | 128 +++++- container/initdev.go | 37 +- container/initdev_test.go | 717 +++++++++++++++++++++++++++++++++- container/initmkdir.go | 8 +- container/initmkdir_test.go | 14 +- container/initoverlay.go | 30 +- container/initoverlay_test.go | 231 ++++++++++- container/initplace.go | 19 +- container/initplace_test.go | 109 +++++- container/initproc.go | 11 +- container/initproc_test.go | 22 +- container/initremount.go | 8 +- container/initremount_test.go | 9 + container/initsymlink.go | 18 +- container/initsymlink_test.go | 48 ++- container/inittmpfs.go | 11 +- container/inittmpfs_test.go | 23 ++ container/mount_test.go | 83 ---- container/msg.go | 25 ++ container/msg_test.go | 23 ++ container/path.go | 3 +- container/path_test.go | 56 ++- container/syscall.go | 13 + 32 files changed, 2835 insertions(+), 362 deletions(-) create mode 100644 container/dispatcher.go create mode 100644 container/dispatcher_test.go diff --git a/container/autoetc.go b/container/autoetc.go index 0334ad1..ee8dfbe 100644 --- a/container/autoetc.go +++ b/container/autoetc.go @@ -3,8 +3,7 @@ package container import ( "encoding/gob" "fmt" - "os" - "syscall" + "io/fs" ) func init() { gob.Register(new(AutoEtcOp)) } @@ -21,38 +20,35 @@ func (f *Ops) Etc(host *Absolute, prefix string) *Ops { type AutoEtcOp struct{ Prefix string } -func (e *AutoEtcOp) Valid() bool { return e != nil } -func (e *AutoEtcOp) early(*setupState) error { return nil } -func (e *AutoEtcOp) apply(state *setupState) error { +func (e *AutoEtcOp) Valid() bool { return e != nil } +func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil } +func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error { if state.nonrepeatable&nrAutoEtc != 0 { - return msg.WrapErr(syscall.EINVAL, "autoetc is not repeatable") + return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable") } state.nonrepeatable |= nrAutoEtc const target = sysrootPath + FHSEtc rel := e.hostRel() + "/" - if err := os.MkdirAll(target, 0755); err != nil { + if err := k.mkdirAll(target, 0755); err != nil { return wrapErrSelf(err) } - if d, err := os.ReadDir(toSysroot(e.hostPath().String())); err != nil { + if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil { return wrapErrSelf(err) } else { for _, ent := range d { n := ent.Name() switch n { - case ".host": - - case "passwd": - case "group": + case ".host", "passwd", "group": case "mtab": - if err = os.Symlink(FHSProc+"mounts", target+n); err != nil { + if err = k.symlink(FHSProc+"mounts", target+n); err != nil { return wrapErrSelf(err) } default: - if err = os.Symlink(rel+n, target+n); err != nil { + if err = k.symlink(rel+n, target+n); err != nil { return wrapErrSelf(err) } } diff --git a/container/autoetc_test.go b/container/autoetc_test.go index efb4b3b..8d942f4 100644 --- a/container/autoetc_test.go +++ b/container/autoetc_test.go @@ -1,8 +1,253 @@ package container -import "testing" +import ( + "errors" + "io/fs" + "os" + "testing" +) func TestAutoEtcOp(t *testing.T) { + t.Run("nonrepeatable", func(t *testing.T) { + wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable") + if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) { + t.Errorf("apply: error = %v, want %v", err, wantErr) + } + }) + + checkOpBehaviour(t, []opBehaviourTestCase{ + {"mkdirAll", new(Params), &AutoEtcOp{ + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"readdir", new(Params), &AutoEtcOp{ + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, + {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink", new(Params), &AutoEtcOp{ + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, + {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", + "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", + "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", + "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", + "modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf", + "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", + "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", + "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", + "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink mtab", new(Params), &AutoEtcOp{ + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, + {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", + "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", + "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", + "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", + "modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf", + "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", + "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", + "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", + "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil}, + {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"success nested", new(Params), &AutoEtcOp{ + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, + {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host", + "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", + "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", + "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", + "modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf", + "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", + "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", + "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", + "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil}, + {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil}, + }, nil}, + + {"success", new(Params), &AutoEtcOp{ + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil}, + {"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir( + "alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts", + "fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd", + "locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf", + "modprobe.d", "modules-load.d", "mtab", "nanorc", "netgroup", "nix", "nixos", "NIXOS", "nscd.conf", + "nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile", + "protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells", + "ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo", + "tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil}, + {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil}, + {"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*AutoEtcOp)(nil), false}, {"zero", new(AutoEtcOp), true}, diff --git a/container/autoroot.go b/container/autoroot.go index 0de6bce..e1c397c 100644 --- a/container/autoroot.go +++ b/container/autoroot.go @@ -3,8 +3,7 @@ package container import ( "encoding/gob" "fmt" - "os" - "syscall" + "io/fs" ) func init() { gob.Register(new(AutoRootOp)) } @@ -30,8 +29,8 @@ type AutoRootOp struct { func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil } -func (r *AutoRootOp) early(state *setupState) error { - if d, err := os.ReadDir(r.Host.String()); err != nil { +func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error { + if d, err := k.readdir(r.Host.String()); err != nil { return wrapErrSelf(err) } else { r.resolved = make([]Op, 0, len(d)) @@ -43,7 +42,7 @@ func (r *AutoRootOp) early(state *setupState) error { Target: AbsFHSRoot.Append(name), Flags: r.Flags, } - if err = op.early(state); err != nil { + if err = op.early(state, k); err != nil { return err } r.resolved = append(r.resolved, op) @@ -53,15 +52,15 @@ func (r *AutoRootOp) early(state *setupState) error { } } -func (r *AutoRootOp) apply(state *setupState) error { +func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error { if state.nonrepeatable&nrAutoRoot != 0 { - return msg.WrapErr(syscall.EINVAL, "autoroot is not repeatable") + return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable") } state.nonrepeatable |= nrAutoRoot for _, op := range r.resolved { - msg.Verbosef("%s %s", op.prefix(), op) - if err := op.apply(state); err != nil { + k.verbosef("%s %s", op.prefix(), op) + if err := op.apply(state, k); err != nil { return err } } @@ -83,13 +82,11 @@ func (r *AutoRootOp) String() string { // IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot. func IsAutoRootBindable(name string) bool { switch name { - case "proc": - case "dev": - case "tmp": - case "mnt": - case "etc": + case "proc", "dev", "tmp", "mnt", "etc": case "": // guard against accidentally binding / + // should be unreachable + msg.Verbose("got unexpected root entry") default: return true diff --git a/container/autoroot_test.go b/container/autoroot_test.go index 03df7c8..eb4d594 100644 --- a/container/autoroot_test.go +++ b/container/autoroot_test.go @@ -1,8 +1,126 @@ package container -import "testing" +import ( + "errors" + "io/fs" + "os" + "testing" +) func TestAutoRootOp(t *testing.T) { + t.Run("nonrepeatable", func(t *testing.T) { + wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable") + if err := (&AutoRootOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) { + t.Errorf("apply: error = %v, want %v", err, wantErr) + } + }) + + checkOpBehaviour(t, []opBehaviourTestCase{ + {"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{ + Host: MustAbs("/"), + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + Flags: BindWritable, + }, []kexpect{ + {"readdir", expectArgs{"/"}, stubDir(), errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"early", &Params{ParentPerm: 0750}, &AutoRootOp{ + Host: MustAbs("/"), + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + Flags: BindWritable, + }, []kexpect{ + {"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", + "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, + {"evalSymlinks", expectArgs{"/bin"}, "", errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"apply", &Params{ParentPerm: 0750}, &AutoRootOp{ + Host: MustAbs("/"), + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + Flags: BindWritable, + }, []kexpect{ + {"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", + "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, + {"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil}, + {"evalSymlinks", expectArgs{"/home"}, "/home", nil}, + {"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil}, + {"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil}, + {"evalSymlinks", expectArgs{"/nix"}, "/nix", nil}, + {"evalSymlinks", expectArgs{"/root"}, "/root", nil}, + {"evalSymlinks", expectArgs{"/run"}, "/run", nil}, + {"evalSymlinks", expectArgs{"/srv"}, "/srv", nil}, + {"evalSymlinks", expectArgs{"/sys"}, "/sys", nil}, + {"evalSymlinks", expectArgs{"/usr"}, "/usr", nil}, + {"evalSymlinks", expectArgs{"/var"}, "/var", nil}, + }, nil, []kexpect{ + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, + {"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique}, + }, wrapErrSelf(errUnique)}, + + {"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{ + Host: MustAbs("/"), + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + Flags: BindWritable, + }, []kexpect{ + {"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64", + "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, + {"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil}, + {"evalSymlinks", expectArgs{"/home"}, "/home", nil}, + {"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil}, + {"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil}, + {"evalSymlinks", expectArgs{"/nix"}, "/nix", nil}, + {"evalSymlinks", expectArgs{"/root"}, "/root", nil}, + {"evalSymlinks", expectArgs{"/run"}, "/run", nil}, + {"evalSymlinks", expectArgs{"/srv"}, "/srv", nil}, + {"evalSymlinks", expectArgs{"/sys"}, "/sys", nil}, + {"evalSymlinks", expectArgs{"/usr"}, "/usr", nil}, + {"evalSymlinks", expectArgs{"/var"}, "/var", nil}, + }, nil, []kexpect{ + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil}, + }, nil}, + + {"success", &Params{ParentPerm: 0750}, &AutoRootOp{ + Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"), + Prefix: "81ceabb30d37bbdb3868004629cb84e9", + }, []kexpect{ + {"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64", + "lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil}, + }, nil, []kexpect{ + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil}, + {"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*AutoRootOp)(nil), false}, {"zero", new(AutoRootOp), false}, diff --git a/container/capability.go b/container/capability.go index fda2fb2..f41bffa 100644 --- a/container/capability.go +++ b/container/capability.go @@ -37,9 +37,52 @@ func capToIndex(cap uintptr) uintptr { return cap >> 5 } func capToMask(cap uintptr) uint32 { return 1 << uint(cap&31) } func capset(hdrp *capHeader, datap *[2]capData) error { - if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, + r, _, errno := syscall.Syscall( + syscall.SYS_CAPSET, uintptr(unsafe.Pointer(hdrp)), - uintptr(unsafe.Pointer(&datap[0])), 0); errno != 0 { + uintptr(unsafe.Pointer(&datap[0])), 0, + ) + if r != 0 { + return errno + } + return nil +} + +// capBoundingSetDrop drops a capability from the calling thread's capability bounding set. +func capBoundingSetDrop(cap uintptr) error { + r, _, errno := syscall.Syscall( + syscall.SYS_PRCTL, + syscall.PR_CAPBSET_DROP, + cap, 0, + ) + if r != 0 { + return errno + } + return nil +} + +// capAmbientClearAll clears the ambient capability set of the calling thread. +func capAmbientClearAll() error { + r, _, errno := syscall.Syscall( + syscall.SYS_PRCTL, + PR_CAP_AMBIENT, + PR_CAP_AMBIENT_CLEAR_ALL, 0, + ) + if r != 0 { + return errno + } + return nil +} + +// capAmbientRaise adds to the ambient capability set of the calling thread. +func capAmbientRaise(cap uintptr) error { + r, _, errno := syscall.Syscall( + syscall.SYS_PRCTL, + PR_CAP_AMBIENT, + PR_CAP_AMBIENT_RAISE, + cap, + ) + if r != 0 { return errno } return nil diff --git a/container/dispatcher.go b/container/dispatcher.go new file mode 100644 index 0000000..38ed8f8 --- /dev/null +++ b/container/dispatcher.go @@ -0,0 +1,229 @@ +package container + +import ( + "io" + "io/fs" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + + "hakurei.app/container/seccomp" +) + +type osFile interface { + Name() string + io.Writer + fs.File +} + +// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. +type syscallDispatcher interface { + // setPtracer provides [SetPtracer]. + setPtracer(pid uintptr) error + // setDumpable provides [SetDumpable]. + setDumpable(dumpable uintptr) error + // setNoNewPrivs provides [SetNoNewPrivs]. + setNoNewPrivs() error + + // lastcap provides [LastCap]. + lastcap() uintptr + // capset provides capset. + capset(hdrp *capHeader, datap *[2]capData) error + // capBoundingSetDrop provides capBoundingSetDrop. + capBoundingSetDrop(cap uintptr) error + // capAmbientClearAll provides capAmbientClearAll. + capAmbientClearAll() error + // capAmbientRaise provides capAmbientRaise. + capAmbientRaise(cap uintptr) error + // isatty provides [Isatty]. + isatty(fd int) bool + // receive provides [Receive]. + receive(key string, e any, v **os.File) (closeFunc func() error, err error) + + // bindMount provides procPaths.bindMount. + bindMount(source, target string, flags uintptr, eq bool) error + // remount provides procPaths.remount. + remount(target string, flags uintptr) error + // mountTmpfs provides mountTmpfs. + mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error + // ensureFile provides ensureFile. + ensureFile(name string, perm, pperm os.FileMode) error + + // seccompLoad provides [seccomp.Load]. + seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error + // notify provides [signal.Notify]. + notify(c chan<- os.Signal, sig ...os.Signal) + // start starts [os/exec.Cmd]. + start(c *exec.Cmd) error + // signal signals the underlying process of [os/exec.Cmd]. + signal(c *exec.Cmd, sig os.Signal) error + // evalSymlinks provides [filepath.EvalSymlinks]. + evalSymlinks(path string) (string, error) + + // exit provides [os.Exit]. + exit(code int) + // getpid provides [os.Getpid]. + getpid() int + // stat provides [os.Stat]. + stat(name string) (os.FileInfo, error) + // mkdir provides [os.Mkdir]. + mkdir(name string, perm os.FileMode) error + // mkdirTemp provides [os.MkdirTemp]. + mkdirTemp(dir, pattern string) (string, error) + // mkdirAll provides [os.MkdirAll]. + mkdirAll(path string, perm os.FileMode) error + // readdir provides [os.ReadDir]. + readdir(name string) ([]os.DirEntry, error) + // writeFile provides [os.WriteFile]. + writeFile(name string, data []byte, perm os.FileMode) error + // createTemp provides [os.CreateTemp]. + createTemp(dir, pattern string) (osFile, error) + // remove provides os.Remove. + remove(name string) error + // newFile provides os.NewFile. + newFile(fd uintptr, name string) *os.File + // symlink provides os.Symlink. + symlink(oldname, newname string) error + // readlink provides [os.Readlink]. + readlink(name string) (string, error) + + // umask provides syscall.Umask. + umask(mask int) (oldmask int) + // sethostname provides syscall.Sethostname + sethostname(p []byte) (err error) + // chdir provides syscall.Chdir + chdir(path string) (err error) + // fchdir provides syscall.Fchdir + fchdir(fd int) (err error) + // open provides syscall.Open + open(path string, mode int, perm uint32) (fd int, err error) + // close provides syscall.Close + close(fd int) (err error) + // pivotRoot provides syscall.PivotRoot + pivotRoot(newroot, putold string) (err error) + // mount provides syscall.Mount + mount(source, target, fstype string, flags uintptr, data string) (err error) + // unmount provides syscall.Unmount + unmount(target string, flags int) (err error) + // 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() + // printBaseErr provides [Msg.PrintBaseErr]. + printBaseErr(err error, fallback string) +} + +// direct implements syscallDispatcher on the current kernel. +type direct struct{} + +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) 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() } +func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) } +func (direct) isatty(fd int) bool { return Isatty(fd) } +func (direct) receive(key string, e any, v **os.File) (func() error, error) { + return Receive(key, e, v) +} + +func (direct) bindMount(source, target string, flags uintptr, eq bool) error { + return hostProc.bindMount(source, target, flags, eq) +} +func (direct) remount(target string, flags uintptr) error { + return hostProc.remount(target, flags) +} +func (direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { + return mountTmpfs(fsname, target, flags, size, perm) +} +func (direct) ensureFile(name string, perm, pperm os.FileMode) error { + return ensureFile(name, perm, pperm) +} + +func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error { + return seccomp.Load(rules, flags) +} +func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) } +func (direct) start(c *exec.Cmd) error { return c.Start() } +func (direct) signal(c *exec.Cmd, sig os.Signal) error { return c.Process.Signal(sig) } +func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) } + +func (direct) exit(code int) { os.Exit(code) } +func (direct) getpid() int { return os.Getpid() } +func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) } +func (direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } +func (direct) mkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) } +func (direct) mkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } +func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } +func (direct) writeFile(name string, data []byte, perm os.FileMode) error { + return os.WriteFile(name, data, perm) +} +func (direct) createTemp(dir, pattern string) (osFile, error) { + return os.CreateTemp(dir, pattern) +} +func (direct) remove(name string) error { + return os.Remove(name) +} +func (direct) newFile(fd uintptr, name string) *os.File { + return os.NewFile(fd, name) +} +func (direct) symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} +func (direct) readlink(name string) (string, error) { + return os.Readlink(name) +} + +func (direct) umask(mask int) (oldmask int) { return syscall.Umask(mask) } +func (direct) sethostname(p []byte) (err error) { return syscall.Sethostname(p) } +func (direct) chdir(path string) (err error) { return syscall.Chdir(path) } +func (direct) fchdir(fd int) (err error) { return syscall.Fchdir(fd) } +func (direct) open(path string, mode int, perm uint32) (fd int, err error) { + return syscall.Open(path, mode, perm) +} +func (direct) close(fd int) (err error) { + return syscall.Close(fd) +} +func (direct) pivotRoot(newroot, putold string) (err error) { + return syscall.PivotRoot(newroot, putold) +} +func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) { + return syscall.Mount(source, target, fstype, flags, data) +} +func (direct) unmount(target string, flags int) (err error) { + return syscall.Unmount(target, flags) +} +func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) { + 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) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) } diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go new file mode 100644 index 0000000..e2b553f --- /dev/null +++ b/container/dispatcher_test.go @@ -0,0 +1,595 @@ +package container + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "reflect" + "runtime" + "slices" + "syscall" + "testing" + "time" + + "hakurei.app/container/seccomp" +) + +var errUnique = errors.New("unique error injected by the test suite") + +type opValidTestCase struct { + name string + op Op + want bool +} + +func checkOpsValid(t *testing.T, testCases []opValidTestCase) { + t.Run("valid", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.op.Valid(); got != tc.want { + t.Errorf("Valid: %v, want %v", got, tc.want) + } + }) + } + }) +} + +type opsBuilderTestCase struct { + name string + ops *Ops + want Ops +} + +func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) { + t.Run("build", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { + t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want) + } + }) + } + }) +} + +type opIsTestCase struct { + name string + op, v Op + want bool +} + +func checkOpIs(t *testing.T, testCases []opIsTestCase) { + t.Run("is", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := tc.op.Is(tc.v); got != tc.want { + t.Errorf("Is: %v, want %v", got, tc.want) + } + }) + } + }) +} + +type opMetaTestCase struct { + name string + op Op + + wantPrefix string + wantString string +} + +func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { + t.Run("meta", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("prefix", func(t *testing.T) { + if got := tc.op.prefix(); got != tc.wantPrefix { + t.Errorf("prefix: %q, want %q", got, tc.wantPrefix) + } + }) + + t.Run("string", func(t *testing.T) { + if got := tc.op.String(); got != tc.wantString { + t.Errorf("String: %s, want %s", got, tc.wantString) + } + }) + }) + } + }) +} + +type opBehaviourTestCase struct { + name string + params *Params + op Op + + early []kexpect + wantErrEarly error + + apply []kexpect + wantErrApply error +} + +func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { + t.Run("behaviour", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + state := &setupState{Params: tc.params} + k := &kstub{t: t, want: slices.Concat(tc.early, []kexpect{{name: "\x00"}}, tc.apply)} + errEarly := tc.op.early(state, k) + k.expect("\x00") + if !errors.Is(errEarly, tc.wantErrEarly) { + t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly) + } + if errEarly != nil { + goto out + } + + if err := tc.op.apply(state, k); !errors.Is(err, tc.wantErrApply) { + t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply) + } + + out: + if len(k.want) != k.pos { + count := k.pos - 1 // separator + if count < len(tc.early) { + t.Errorf("early: %d calls, want %d", count, len(tc.early)) + } else { + t.Errorf("apply: %d calls, want %d", count-len(tc.early), len(tc.apply)) + } + } + }) + } + }) +} + +func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile { + f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr} + // check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results + f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name) + return f +} + +type checkedOsFile struct { + t *testing.T + name string + want string + closeErr error + cleanup runtime.Cleanup + bytes.Buffer +} + +func (f *checkedOsFile) Name() string { return f.name } +func (f *checkedOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") } +func (f *checkedOsFile) Close() error { + defer f.cleanup.Stop() + if f.String() != f.want { + f.t.Errorf("checkedOsFile:\n%s\nwant\n%s", f.String(), f.want) + return syscall.ENOTRECOVERABLE + } + return f.closeErr +} + +type writeErrOsFile struct{ err error } + +func (writeErrOsFile) Name() string { panic("unreachable") } +func (f writeErrOsFile) Write([]byte) (int, error) { return 0, f.err } +func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") } +func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") } +func (writeErrOsFile) Close() error { panic("unreachable") } + +type expectArgs = [5]any + +type isDirFi bool + +func (isDirFi) Name() string { panic("unreachable") } +func (isDirFi) Size() int64 { panic("unreachable") } +func (isDirFi) Mode() fs.FileMode { panic("unreachable") } +func (isDirFi) ModTime() time.Time { panic("unreachable") } +func (fi isDirFi) IsDir() bool { return bool(fi) } +func (isDirFi) Sys() any { panic("unreachable") } + +func stubDir(names ...string) []os.DirEntry { + d := make([]os.DirEntry, len(names)) + for i, name := range names { + d[i] = nameDentry(name) + } + return d +} + +type nameDentry string + +func (e nameDentry) Name() string { return string(e) } +func (nameDentry) IsDir() bool { panic("unreachable") } +func (nameDentry) Type() fs.FileMode { panic("unreachable") } +func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") } + +type kexpect struct { + name string + args expectArgs + ret any + err error +} + +func (k *kexpect) error(ok ...bool) error { + if !slices.Contains(ok, false) { + return k.err + } + return syscall.ENOTRECOVERABLE +} + +type kstub struct { + t *testing.T + want []kexpect + pos int +} + +// expect checks name and returns the current kexpect and advances pos. +func (k *kstub) expect(name string) (expect *kexpect) { + if len(k.want) == k.pos { + k.t.Fatal("expect: want too short") + } + expect = &k.want[k.pos] + if name != expect.name { + if expect.name == "\x00" { + k.t.Fatalf("expect: func = %s, separator overrun", name) + } + if name == "\x00" { + k.t.Fatalf("expect: separator, want %s", expect.name) + } + k.t.Fatalf("expect: func = %s, want %s", name, expect.name) + } + k.pos++ + return +} + +// checkArg checks an argument comparable with the == operator. Avoid using this with pointers. +func checkArg[T comparable](k *kstub, arg string, got T, n int) bool { + if k.pos == 0 { + panic("invalid call to checkArg") + } + expect := k.want[k.pos-1] + want, ok := expect.args[n].(T) + if !ok || got != want { + k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1) + return false + } + return true +} + +// checkArgReflect checks an argument of any type. +func checkArgReflect(k *kstub, arg string, got any, n int) bool { + if k.pos == 0 { + panic("invalid call to checkArgReflect") + } + expect := k.want[k.pos-1] + want := expect.args[n] + if !reflect.DeepEqual(got, want) { + k.t.Errorf("%s: %s = %#v, want %#v (%d)", expect.name, arg, got, want, k.pos-1) + return false + } + return true +} + +func (k *kstub) setPtracer(pid uintptr) error { + return k.expect("setPtracer").error( + checkArg(k, "pid", pid, 0)) +} + +func (k *kstub) setDumpable(dumpable uintptr) error { + return k.expect("setDumpable").error( + checkArg(k, "dumpable", dumpable, 0)) +} + +func (k *kstub) setNoNewPrivs() error { return k.expect("setNoNewPrivs").err } +func (k *kstub) lastcap() uintptr { return k.expect("setNoNewPrivs").ret.(uintptr) } + +func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error { + return k.expect("capset").error( + checkArgReflect(k, "hdrp", hdrp, 0), + checkArgReflect(k, "datap", datap, 1)) +} + +func (k *kstub) capBoundingSetDrop(cap uintptr) error { + return k.expect("capBoundingSetDrop").error( + checkArg(k, "cap", cap, 0)) +} + +func (k *kstub) capAmbientClearAll() error { return k.expect("capAmbientClearAll").err } + +func (k *kstub) capAmbientRaise(cap uintptr) error { + return k.expect("capAmbientRaise").error( + checkArg(k, "cap", cap, 0)) +} + +func (k *kstub) isatty(fd int) bool { + expect := k.expect("isatty") + if !checkArg(k, "fd", fd, 0) { + k.t.FailNow() + } + return expect.ret.(bool) +} + +func (k *kstub) receive(key string, e any, v **os.File) (closeFunc func() error, err error) { + expect := k.expect("receive") + return expect.ret.(func() error), expect.error( + checkArg(k, "key", key, 0), + checkArgReflect(k, "e", e, 1), + checkArg(k, "v", v, 2)) +} + +func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error { + return k.expect("bindMount").error( + checkArg(k, "source", source, 0), + checkArg(k, "target", target, 1), + checkArg(k, "flags", flags, 2), + checkArg(k, "eq", eq, 3)) +} + +func (k *kstub) remount(target string, flags uintptr) error { + return k.expect("remount").error( + checkArg(k, "target", target, 0), + checkArg(k, "flags", flags, 1)) +} + +func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { + return k.expect("mountTmpfs").error( + checkArg(k, "fsname", fsname, 0), + checkArg(k, "target", target, 1), + checkArg(k, "flags", flags, 2), + checkArg(k, "size", size, 3), + checkArg(k, "perm", perm, 4)) +} + +func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error { + + return k.expect("ensureFile").error( + checkArg(k, "name", name, 0), + checkArg(k, "perm", perm, 1), + checkArg(k, "pperm", pperm, 2)) +} + +func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error { + return k.expect("seccompLoad").error( + checkArgReflect(k, "rules", rules, 0), + checkArg(k, "flags", flags, 1)) +} + +func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) { + expect := k.expect("notify") + if c == nil || expect.error( + checkArgReflect(k, "sig", sig, 1)) != nil { + k.t.FailNow() + } + + // export channel for external instrumentation + if chanp, ok := expect.args[0].(*chan<- os.Signal); ok && chanp != nil { + if *chanp != nil { + panic(fmt.Sprintf("attempting reuse of %p", chanp)) + } + *chanp = c + } +} + +func (k *kstub) start(c *exec.Cmd) error { + return k.expect("start").error( + checkArg(k, "c.Path", c.Path, 0), + checkArgReflect(k, "c.Args", c.Args, 1), + checkArgReflect(k, "c.Env", c.Env, 2), + checkArg(k, "c.Dir", c.Dir, 3)) +} + +func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error { + return k.expect("signal").error( + checkArg(k, "c.Path", c.Path, 0), + checkArgReflect(k, "c.Args", c.Args, 1), + checkArgReflect(k, "c.Env", c.Env, 2), + checkArg(k, "c.Dir", c.Dir, 3), + checkArg(k, "sig", sig, 4)) +} + +func (k *kstub) evalSymlinks(path string) (string, error) { + expect := k.expect("evalSymlinks") + return expect.ret.(string), expect.error( + checkArg(k, "path", path, 0)) +} + +func (k *kstub) exit(code int) { + k.expect("exit") + if !checkArg(k, "code", code, 0) { + k.t.FailNow() + } +} + +func (k *kstub) getpid() int { return k.expect("getpid").ret.(int) } + +func (k *kstub) stat(name string) (os.FileInfo, error) { + expect := k.expect("stat") + return expect.ret.(os.FileInfo), expect.error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) mkdir(name string, perm os.FileMode) error { + return k.expect("mkdir").error( + checkArg(k, "name", name, 0), + checkArg(k, "perm", perm, 1)) +} + +func (k *kstub) mkdirTemp(dir, pattern string) (string, error) { + expect := k.expect("mkdirTemp") + return expect.ret.(string), expect.error( + checkArg(k, "dir", dir, 0), + checkArg(k, "pattern", pattern, 1)) +} + +func (k *kstub) mkdirAll(path string, perm os.FileMode) error { + return k.expect("mkdirAll").error( + checkArg(k, "path", path, 0), + checkArg(k, "perm", perm, 1)) +} + +func (k *kstub) readdir(name string) ([]os.DirEntry, error) { + expect := k.expect("readdir") + return expect.ret.([]os.DirEntry), expect.error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error { + return k.expect("writeFile").error( + checkArg(k, "name", name, 0), + checkArgReflect(k, "data", data, 1), + checkArg(k, "perm", perm, 2)) +} + +func (k *kstub) createTemp(dir, pattern string) (osFile, error) { + expect := k.expect("createTemp") + return expect.ret.(osFile), expect.error( + checkArg(k, "dir", dir, 0), + checkArg(k, "pattern", pattern, 1)) +} + +func (k *kstub) remove(name string) error { + return k.expect("remove").error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) newFile(fd uintptr, name string) *os.File { + expect := k.expect("newFile") + if expect.error( + checkArg(k, "fd", fd, 0), + checkArg(k, "name", name, 1)) != nil { + k.t.FailNow() + } + return expect.ret.(*os.File) +} + +func (k *kstub) symlink(oldname, newname string) error { + return k.expect("symlink").error( + checkArg(k, "oldname", oldname, 0), + checkArg(k, "newname", newname, 1)) +} + +func (k *kstub) readlink(name string) (string, error) { + expect := k.expect("readlink") + return expect.ret.(string), expect.error( + checkArg(k, "name", name, 0)) +} + +func (k *kstub) umask(mask int) (oldmask int) { + expect := k.expect("umask") + if !checkArg(k, "mask", mask, 0) { + k.t.FailNow() + } + return expect.ret.(int) +} + +func (k *kstub) sethostname(p []byte) (err error) { + return k.expect("sethostname").error( + checkArgReflect(k, "p", p, 0)) +} + +func (k *kstub) chdir(path string) (err error) { + return k.expect("chdir").error( + checkArg(k, "path", path, 0)) +} + +func (k *kstub) fchdir(fd int) (err error) { + return k.expect("fchdir").error( + checkArg(k, "fd", fd, 0)) +} + +func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) { + expect := k.expect("open") + return expect.ret.(int), expect.error( + checkArg(k, "path", path, 0), + checkArg(k, "mode", mode, 1), + checkArg(k, "perm", perm, 2)) +} + +func (k *kstub) close(fd int) (err error) { + return k.expect("close").error( + checkArg(k, "fd", fd, 0)) +} + +func (k *kstub) pivotRoot(newroot, putold string) (err error) { + return k.expect("pivotRoot").error( + checkArg(k, "newroot", newroot, 0), + checkArg(k, "putold", putold, 1)) +} + +func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) { + return k.expect("mount").error( + checkArg(k, "source", source, 0), + checkArg(k, "target", target, 1), + checkArg(k, "fstype", fstype, 2), + checkArg(k, "flags", flags, 3), + checkArg(k, "data", data, 4)) +} + +func (k *kstub) unmount(target string, flags int) (err error) { + return k.expect("unmount").error( + checkArg(k, "target", target, 0), + checkArg(k, "flags", flags, 1)) +} + +func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) { + expect := k.expect("wait4") + return expect.ret.(int), expect.error( + checkArg(k, "pid", pid, 0), + checkArg(k, "wstatus", wstatus, 1), + checkArg(k, "options", options, 2), + checkArg(k, "rusage", rusage, 3)) +} + +func (k *kstub) printf(format string, v ...any) { + if k.expect("printf").error( + checkArg(k, "format", format, 0), + checkArgReflect(k, "v", v, 1)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) fatal(v ...any) { + if k.expect("fatal").error( + checkArgReflect(k, "v", v, 0)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) fatalf(format string, v ...any) { + if k.expect("fatalf").error( + checkArg(k, "format", format, 0), + checkArgReflect(k, "v", v, 1)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) verbose(v ...any) { + if k.expect("verbose").error( + checkArgReflect(k, "v", v, 0)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) verbosef(format string, v ...any) { + if k.expect("verbosef").error( + checkArg(k, "format", format, 0), + checkArgReflect(k, "v", v, 1)) != nil { + k.t.FailNow() + } +} + +func (k *kstub) suspend() { k.expect("suspend") } +func (k *kstub) resume() bool { return k.expect("resume").ret.(bool) } +func (k *kstub) beforeExit() { k.expect("beforeExit") } + +func (k *kstub) printBaseErr(err error, fallback string) { + if k.expect("printBaseErr").error( + checkArgReflect(k, "err", err, 0), + checkArg(k, "fallback", fallback, 1)) != nil { + k.t.FailNow() + } +} diff --git a/container/init.go b/container/init.go index 663bbc8..6688a34 100644 --- a/container/init.go +++ b/container/init.go @@ -3,10 +3,8 @@ package container import ( "errors" "fmt" - "log" "os" "os/exec" - "os/signal" "path" "runtime" "slices" @@ -46,9 +44,9 @@ type ( // Implementations of this interface are sent as a stream of gobs. Op interface { // early is called in host root. - early(state *setupState) error + early(state *setupState, k syscallDispatcher) error // apply is called in intermediate root. - apply(state *setupState) error + apply(state *setupState, k syscallDispatcher) error prefix() string Is(op Op) bool @@ -82,16 +80,20 @@ type initParams struct { Verbose bool } -func Init(prepare func(prefix string), setVerbose func(verbose bool)) { - runtime.LockOSThread() - prepare("init") +func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) { + initEntrypoint(prepareLogger, setVerbose, direct{}) +} - if os.Getpid() != 1 { - log.Fatal("this process must run as pid 1") +func initEntrypoint(prepareLogger func(prefix string), setVerbose func(verbose bool), k syscallDispatcher) { + runtime.LockOSThread() + prepareLogger("init") + + if k.getpid() != 1 { + k.fatal("this process must run as pid 1") } - if err := SetPtracer(0); err != nil { - msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err) + if err := k.setPtracer(0); err != nil { + k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err) // not fatal: this program has no additional privileges at initial program start } @@ -101,64 +103,64 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { setupFile *os.File offsetSetup int ) - if f, err := Receive(setupEnv, ¶ms, &setupFile); err != nil { + if f, err := k.receive(setupEnv, ¶ms, &setupFile); err != nil { if errors.Is(err, EBADF) { - log.Fatal("invalid setup descriptor") + k.fatal("invalid setup descriptor") } if errors.Is(err, ErrNotSet) { - log.Fatal("HAKUREI_SETUP not set") + k.fatal("HAKUREI_SETUP not set") } - log.Fatalf("cannot decode init setup payload: %v", err) + k.fatalf("cannot decode init setup payload: %v", err) } else { if params.Ops == nil { - log.Fatal("invalid setup parameters") + k.fatal("invalid setup parameters") } if params.ParentPerm == 0 { params.ParentPerm = 0755 } setVerbose(params.Verbose) - msg.Verbose("received setup parameters") + k.verbose("received setup parameters") closeSetup = f offsetSetup = int(setupFile.Fd() + 1) } // write uid/gid map here so parent does not need to set dumpable - if err := SetDumpable(SUID_DUMP_USER); err != nil { - log.Fatalf("cannot set SUID_DUMP_USER: %s", err) + if err := k.setDumpable(SUID_DUMP_USER); err != nil { + k.fatalf("cannot set SUID_DUMP_USER: %s", err) } - if err := os.WriteFile(FHSProc+"self/uid_map", + if err := k.writeFile(FHSProc+"self/uid_map", append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...), 0); err != nil { - log.Fatalf("%v", err) + k.fatalf("%v", err) } - if err := os.WriteFile(FHSProc+"self/setgroups", + if err := k.writeFile(FHSProc+"self/setgroups", []byte("deny\n"), 0); err != nil && !os.IsNotExist(err) { - log.Fatalf("%v", err) + k.fatalf("%v", err) } - if err := os.WriteFile(FHSProc+"self/gid_map", + if err := k.writeFile(FHSProc+"self/gid_map", append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...), 0); err != nil { - log.Fatalf("%v", err) + k.fatalf("%v", err) } - if err := SetDumpable(SUID_DUMP_DISABLE); err != nil { - log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) + if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil { + k.fatalf("cannot set SUID_DUMP_DISABLE: %s", err) } - oldmask := Umask(0) + oldmask := k.umask(0) if params.Hostname != "" { - if err := Sethostname([]byte(params.Hostname)); err != nil { - log.Fatalf("cannot set hostname: %v", err) + if err := k.sethostname([]byte(params.Hostname)); err != nil { + k.fatalf("cannot set hostname: %v", err) } } // cache sysctl before pivot_root - LastCap() + k.lastcap() - if err := Mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil { - log.Fatalf("cannot make / rslave: %v", err) + if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil { + k.fatalf("cannot make / rslave: %v", err) } state := &setupState{Params: ¶ms.Params} @@ -169,40 +171,40 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { the state of the mount namespace */ for i, op := range *params.Ops { if op == nil || !op.Valid() { - log.Fatalf("invalid op at index %d", i) + k.fatalf("invalid op at index %d", i) } - if err := op.early(state); err != nil { - msg.PrintBaseErr(err, - fmt.Sprintf("cannot prepare op %d:", i)) - msg.BeforeExit() - os.Exit(1) + if err := op.early(state, k); err != nil { + k.printBaseErr(err, + fmt.Sprintf("cannot prepare op at index %d:", i)) + k.beforeExit() + k.exit(1) } } - if err := Mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil { - log.Fatalf("cannot mount intermediate root: %v", err) + if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil { + k.fatalf("cannot mount intermediate root: %v", err) } - if err := os.Chdir(intermediateHostPath); err != nil { - log.Fatalf("cannot enter base path: %v", err) + if err := k.chdir(intermediateHostPath); err != nil { + k.fatalf("cannot enter intermediate host path: %v", err) } - if err := os.Mkdir(sysrootDir, 0755); err != nil { - log.Fatalf("%v", err) + if err := k.mkdir(sysrootDir, 0755); err != nil { + k.fatalf("%v", err) } - if err := Mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil { - log.Fatalf("cannot bind sysroot: %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) } - if err := os.Mkdir(hostDir, 0755); err != nil { - log.Fatalf("%v", err) + if err := k.mkdir(hostDir, 0755); err != nil { + k.fatalf("%v", err) } // pivot_root uncovers intermediateHostPath in hostDir - if err := PivotRoot(intermediateHostPath, hostDir); err != nil { - log.Fatalf("cannot pivot into intermediate root: %v", err) + if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil { + k.fatalf("cannot pivot into intermediate root: %v", err) } - if err := os.Chdir(FHSRoot); err != nil { - log.Fatalf("%v", err) + if err := k.chdir(FHSRoot); err != nil { + k.fatalf("cannot enter intermediate root: %v", err) } /* apply is called right after pivot_root and entering the new root; @@ -211,62 +213,62 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { chdir is allowed but discouraged */ for i, op := range *params.Ops { // ops already checked during early setup - msg.Verbosef("%s %s", op.prefix(), op) - if err := op.apply(state); err != nil { - msg.PrintBaseErr(err, - fmt.Sprintf("cannot apply op %d:", i)) - msg.BeforeExit() - os.Exit(1) + k.verbosef("%s %s", op.prefix(), op) + if err := op.apply(state, k); err != nil { + k.printBaseErr(err, + fmt.Sprintf("cannot apply op at index %d:", i)) + k.beforeExit() + k.exit(1) } } // setup requiring host root complete at this point - if err := Mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil { - log.Fatalf("cannot make host root rprivate: %v", err) + 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) } - if err := Unmount(hostDir, MNT_DETACH); err != nil { - log.Fatalf("cannot unmount host root: %v", err) + if err := k.unmount(hostDir, MNT_DETACH); err != nil { + k.fatalf("cannot unmount host root: %v", err) } { var fd int if err := IgnoringEINTR(func() (err error) { - fd, err = Open(FHSRoot, O_DIRECTORY|O_RDONLY, 0) + fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0) return }); err != nil { - log.Fatalf("cannot open intermediate root: %v", err) + k.fatalf("cannot open intermediate root: %v", err) } - if err := os.Chdir(sysrootPath); err != nil { - log.Fatalf("%v", err) + if err := k.chdir(sysrootPath); err != nil { + k.fatalf("cannot enter sysroot: %v", err) } - if err := PivotRoot(".", "."); err != nil { - log.Fatalf("cannot pivot into sysroot: %v", err) + if err := k.pivotRoot(".", "."); err != nil { + k.fatalf("cannot pivot into sysroot: %v", err) } - if err := Fchdir(fd); err != nil { - log.Fatalf("cannot re-enter intermediate root: %v", err) + if err := k.fchdir(fd); err != nil { + k.fatalf("cannot re-enter intermediate root: %v", err) } - if err := Unmount(".", MNT_DETACH); err != nil { - log.Fatalf("cannot unmount intemediate root: %v", err) + if err := k.unmount(".", MNT_DETACH); err != nil { + k.fatalf("cannot unmount intemediate root: %v", err) } - if err := os.Chdir(FHSRoot); err != nil { - log.Fatalf("%v", err) + if err := k.chdir(FHSRoot); err != nil { + k.fatalf("cannot enter root: %v", err) } - if err := Close(fd); err != nil { - log.Fatalf("cannot close intermediate root: %v", err) + if err := k.close(fd); err != nil { + k.fatalf("cannot close intermediate root: %v", err) } } - if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 { - log.Fatalf("cannot clear the ambient capability set: %v", errno) + if err := k.capAmbientClearAll(); err != nil { + k.fatalf("cannot clear the ambient capability set: %v", err) } - for i := uintptr(0); i <= LastCap(); i++ { + for i := uintptr(0); i <= k.lastcap(); i++ { if params.Privileged && i == CAP_SYS_ADMIN { continue } - if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 { - log.Fatalf("cannot drop capability from bonding set: %v", errno) + if err := k.capBoundingSetDrop(i); err != nil { + k.fatalf("cannot drop capability from bounding set: %v", err) } } @@ -274,38 +276,38 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { if params.Privileged { keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN) - if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 { - log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno) + if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil { + k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err) } } - if err := capset( + if err := k.capset( &capHeader{_LINUX_CAPABILITY_VERSION_3, 0}, &[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}}, ); err != nil { - log.Fatalf("cannot capset: %v", err) + k.fatalf("cannot capset: %v", err) } if !params.SeccompDisable { rules := params.SeccompRules if len(rules) == 0 { // non-empty rules slice always overrides presets - msg.Verbosef("resolving presets %#x", params.SeccompPresets) + k.verbosef("resolving presets %#x", params.SeccompPresets) rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags) } - if err := seccomp.Load(rules, params.SeccompFlags); err != nil { + if err := k.seccompLoad(rules, params.SeccompFlags); err != nil { // this also indirectly asserts PR_SET_NO_NEW_PRIVS - log.Fatalf("cannot load syscall filter: %v", err) + k.fatalf("cannot load syscall filter: %v", err) } - msg.Verbosef("%d filter rules loaded", len(rules)) + k.verbosef("%d filter rules loaded", len(rules)) } else { - msg.Verbose("syscall filter not configured") + k.verbose("syscall filter not configured") } extraFiles := make([]*os.File, params.Count) for i := range extraFiles { // setup fd is placed before all extra files - extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) + extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i)) } - Umask(oldmask) + k.umask(oldmask) cmd := exec.Command(params.Path.String()) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr @@ -314,14 +316,14 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { cmd.ExtraFiles = extraFiles cmd.Dir = params.Dir.String() - msg.Verbosef("starting initial program %s", params.Path) - if err := cmd.Start(); err != nil { - log.Fatalf("%v", err) + k.verbosef("starting initial program %s", params.Path) + if err := k.start(cmd); err != nil { + k.fatalf("%v", err) } - msg.Suspend() + k.suspend() if err := closeSetup(); err != nil { - log.Printf("cannot close setup pipe: %v", err) + k.printf("cannot close setup pipe: %v", err) // not fatal } @@ -351,11 +353,11 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { err = EINTR for errors.Is(err, EINTR) { - wpid, err = Wait4(-1, &wstatus, 0, nil) + wpid, err = k.wait4(-1, &wstatus, 0, nil) } } if !errors.Is(err, ECHILD) { - log.Printf("unexpected wait4 response: %v", err) + k.printf("unexpected wait4 response: %v", err) } close(done) @@ -363,7 +365,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { // handle signals to dump withheld messages sig := make(chan os.Signal, 2) - signal.Notify(sig, os.Interrupt, CancelSignal) + k.notify(sig, os.Interrupt, CancelSignal) // closed after residualProcessTimeout has elapsed after initial process death timeout := make(chan struct{}) @@ -372,45 +374,48 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) { for { select { case s := <-sig: - if msg.Resume() { - msg.Verbosef("%s after process start", s.String()) + if k.resume() { + k.verbosef("%s after process start", s.String()) } else { - msg.Verbosef("got %s", s.String()) + k.verbosef("got %s", s.String()) } if s == CancelSignal && params.ForwardCancel && cmd.Process != nil { - msg.Verbose("forwarding context cancellation") - if err := cmd.Process.Signal(os.Interrupt); err != nil { - log.Printf("cannot forward cancellation: %v", err) + k.verbose("forwarding context cancellation") + if err := k.signal(cmd, os.Interrupt); err != nil { + k.printf("cannot forward cancellation: %v", err) } continue } - os.Exit(0) + k.exit(0) + case w := <-info: if w.wpid == cmd.Process.Pid { // initial process exited, output is most likely available again - msg.Resume() + k.resume() switch { case w.wstatus.Exited(): r = w.wstatus.ExitStatus() - msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) + k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus()) case w.wstatus.Signaled(): r = 128 + int(w.wstatus.Signal()) - msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal()) + k.verbosef("initial process exited with signal %s", w.wstatus.Signal()) default: r = 255 - msg.Verbosef("initial process exited with status %#x", w.wstatus) + k.verbosef("initial process exited with status %#x", w.wstatus) } go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }() } + case <-done: - msg.BeforeExit() - os.Exit(r) + k.beforeExit() + k.exit(r) + case <-timeout: - log.Println("timeout exceeded waiting for lingering processes") - msg.BeforeExit() - os.Exit(r) + k.printf("timeout exceeded waiting for lingering processes") + k.beforeExit() + k.exit(r) } } } diff --git a/container/initbind.go b/container/initbind.go index e19cb5d..18f4493 100644 --- a/container/initbind.go +++ b/container/initbind.go @@ -4,8 +4,7 @@ import ( "encoding/gob" "fmt" "os" - "path/filepath" - . "syscall" + "syscall" ) func init() { gob.Register(new(BindMountOp)) } @@ -35,8 +34,8 @@ const ( BindDevice ) -func (b *BindMountOp) early(*setupState) error { - if pathname, err := filepath.EvalSymlinks(b.Source.String()); err != nil { +func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error { + if pathname, err := k.evalSymlinks(b.Source.String()); err != nil { if os.IsNotExist(err) && b.Flags&BindOptional != 0 { // leave sourceFinal as nil return nil @@ -48,11 +47,11 @@ func (b *BindMountOp) early(*setupState) error { } } -func (b *BindMountOp) apply(*setupState) error { +func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error { if b.sourceFinal == nil { if b.Flags&BindOptional == 0 { // unreachable - return EBADE + return msg.WrapErr(os.ErrClosed, "impossible bind state reached") } return nil } @@ -62,25 +61,25 @@ func (b *BindMountOp) apply(*setupState) error { // this perm value emulates bwrap behaviour as it clears bits from 0755 based on // op->perms which is never set for any bind setup op so always results in 0700 - if fi, err := os.Stat(source); err != nil { + if fi, err := k.stat(source); err != nil { return wrapErrSelf(err) } else if fi.IsDir() { - if err = os.MkdirAll(target, 0700); err != nil { + if err = k.mkdirAll(target, 0700); err != nil { return wrapErrSelf(err) } - } else if err = ensureFile(target, 0444, 0700); err != nil { + } else if err = k.ensureFile(target, 0444, 0700); err != nil { return err } - var flags uintptr = MS_REC + var flags uintptr = syscall.MS_REC if b.Flags&BindWritable == 0 { - flags |= MS_RDONLY + flags |= syscall.MS_RDONLY } if b.Flags&BindDevice == 0 { - flags |= MS_NODEV + flags |= syscall.MS_NODEV } - return hostProc.bindMount(source, target, flags, b.sourceFinal == b.Target) + return k.bindMount(source, target, flags, b.sourceFinal == b.Target) } func (b *BindMountOp) Is(op Op) bool { diff --git a/container/initbind_test.go b/container/initbind_test.go index bae33f8..1de47de 100644 --- a/container/initbind_test.go +++ b/container/initbind_test.go @@ -1,8 +1,134 @@ package container -import "testing" +import ( + "errors" + "os" + "syscall" + "testing" +) func TestBindMountOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"ENOENT not optional", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT}, + }, wrapErrSelf(syscall.ENOENT), nil, nil}, + + {"skip optional", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + Flags: BindOptional, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "", syscall.ENOENT}, + }, nil, nil, nil}, + + {"success optional", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + Flags: BindOptional, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, + {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil}, + }, nil}, + + {"ensureFile device", new(Params), &BindMountOp{ + Source: MustAbs("/dev/null"), + Target: MustAbs("/dev/null"), + Flags: BindWritable | BindDevice, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique}, + }, errUnique}, + + {"success device ro", new(Params), &BindMountOp{ + Source: MustAbs("/dev/null"), + Target: MustAbs("/dev/null"), + Flags: BindDevice, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil}, + }, nil}, + + {"success device", new(Params), &BindMountOp{ + Source: MustAbs("/dev/null"), + Target: MustAbs("/dev/null"), + Flags: BindWritable | BindDevice, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/dev/null"}, "/dev/null", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/dev/null"}, isDirFi(false), nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil}, + }, nil}, + + {"evalSymlinks", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"stat", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mkdirAll", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, + {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"bindMount", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, + {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, errUnique}, + }, errUnique}, + + {"success", new(Params), &BindMountOp{ + Source: MustAbs("/bin/"), + Target: MustAbs("/bin/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/bin/"}, "/usr/bin", nil}, + }, nil, []kexpect{ + {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, + {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil}, + }, nil}, + }) + + t.Run("unreachable", func(t *testing.T) { + t.Run("nil sourceFinal not optional", func(t *testing.T) { + wantErr := msg.WrapErr(os.ErrClosed, "impossible bind state reached") + if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) { + t.Errorf("apply: error = %v, want %v", err, wantErr) + } + }) + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*BindMountOp)(nil), false}, {"zero", new(BindMountOp), false}, diff --git a/container/initdev.go b/container/initdev.go index 48d70f3..b319291 100644 --- a/container/initdev.go +++ b/container/initdev.go @@ -3,10 +3,8 @@ package container import ( "encoding/gob" "fmt" - "os" "path" . "syscall" - "unsafe" ) func init() { gob.Register(new(MountDevOp)) } @@ -33,21 +31,21 @@ type MountDevOp struct { Write bool } -func (d *MountDevOp) Valid() bool { return d != nil && d.Target != nil } -func (d *MountDevOp) early(*setupState) error { return nil } -func (d *MountDevOp) apply(state *setupState) error { +func (d *MountDevOp) Valid() bool { return d != nil && d.Target != nil } +func (d *MountDevOp) early(*setupState, syscallDispatcher) error { return nil } +func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error { target := toSysroot(d.Target.String()) - if err := mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil { + if err := k.mountTmpfs(SourceTmpfsDevtmpfs, target, MS_NOSUID|MS_NODEV, 0, state.ParentPerm); err != nil { return err } for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} { targetPath := path.Join(target, name) - if err := ensureFile(targetPath, 0444, state.ParentPerm); err != nil { + if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil { return err } - if err := hostProc.bindMount( + if err := k.bindMount( toHost(FHSDev+name), targetPath, 0, @@ -57,7 +55,7 @@ func (d *MountDevOp) apply(state *setupState) error { } } for i, name := range []string{"stdin", "stdout", "stderr"} { - if err := os.Symlink( + if err := k.symlink( FHSProc+"self/fd/"+string(rune(i+'0')), path.Join(target, name), ); err != nil { @@ -69,34 +67,33 @@ func (d *MountDevOp) apply(state *setupState) error { {FHSProc + "kcore", "core"}, {"pts/ptmx", "ptmx"}, } { - if err := os.Symlink(pair[0], path.Join(target, pair[1])); err != nil { + if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil { return wrapErrSelf(err) } } devPtsPath := path.Join(target, "pts") for _, name := range []string{path.Join(target, "shm"), devPtsPath} { - if err := os.Mkdir(name, state.ParentPerm); err != nil { + if err := k.mkdir(name, state.ParentPerm); err != nil { return wrapErrSelf(err) } } - if err := Mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC, + if err := k.mount(SourceDevpts, devPtsPath, FstypeDevpts, MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=620"); err != nil { return wrapErrSuffix(err, fmt.Sprintf("cannot mount devpts on %q:", devPtsPath)) } if state.RetainSession { - var buf [8]byte - if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 { + if k.isatty(Stdout) { consolePath := path.Join(target, "console") - if err := ensureFile(consolePath, 0444, state.ParentPerm); err != nil { + if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil { return err } - if name, err := os.Readlink(hostProc.stdout()); err != nil { + if name, err := k.readlink(hostProc.stdout()); err != nil { return wrapErrSelf(err) - } else if err = hostProc.bindMount( + } else if err = k.bindMount( toHost(name), consolePath, 0, @@ -109,10 +106,10 @@ func (d *MountDevOp) apply(state *setupState) error { if d.Mqueue { mqueueTarget := path.Join(target, "mqueue") - if err := os.Mkdir(mqueueTarget, state.ParentPerm); err != nil { + if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil { return wrapErrSelf(err) } - if err := Mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil { + if err := k.mount(SourceMqueue, mqueueTarget, FstypeMqueue, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString); err != nil { return wrapErrSuffix(err, "cannot mount mqueue:") } } @@ -120,7 +117,7 @@ func (d *MountDevOp) apply(state *setupState) error { if d.Write { return nil } - return wrapErrSuffix(hostProc.remount(target, MS_RDONLY), + return wrapErrSuffix(k.remount(target, MS_RDONLY), fmt.Sprintf("cannot remount %q:", target)) } diff --git a/container/initdev_test.go b/container/initdev_test.go index 4e23e6b..9d2529c 100644 --- a/container/initdev_test.go +++ b/container/initdev_test.go @@ -1,8 +1,723 @@ package container -import "testing" +import ( + "os" + "testing" +) func TestMountDevOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"mountTmpfs", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"ensureFile null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"bindMount null", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, errUnique}, + }, errUnique}, + + {"ensureFile zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"bindMount zero", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, errUnique}, + }, errUnique}, + + {"ensureFile full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"bindMount full", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, errUnique}, + }, errUnique}, + + {"ensureFile random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"bindMount random", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, errUnique}, + }, errUnique}, + + {"ensureFile urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"bindMount urandom", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, errUnique}, + }, errUnique}, + + {"ensureFile tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"bindMount tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, errUnique}, + }, errUnique}, + + {"symlink stdin", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink stderr", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink fd", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink kcore", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"symlink ptmx", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mkdir shm", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mkdir devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mount devpts", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, errUnique}, + }, wrapErrSuffix(errUnique, `cannot mount devpts on "/sysroot/dev/pts":`)}, + + {"ensureFile stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, errUnique}, + }, errUnique}, + + {"readlink stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "", errUnique}, + }, wrapErrSelf(errUnique)}, + + {"bindMount stdout", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil}, + {"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, errUnique}, + }, errUnique}, + + {"mkdir mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil}, + {"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mount mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil}, + {"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, errUnique}, + }, wrapErrSuffix(errUnique, "cannot mount mqueue:")}, + + {"success no session", &Params{ParentPerm: 0755}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + Write: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0755)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0755)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0755)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0755)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0755)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0755)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0755)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0755)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0755)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0755)}, nil, nil}, + {"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil}, + }, nil}, + + {"success no tty", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + Write: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, false, nil}, + {"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil}, + }, nil}, + + {"success no mqueue", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil}, + {"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil}, + {"remount", expectArgs{"/sysroot/dev", uintptr(1)}, nil, nil}, + }, nil}, + + {"success rw", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + Write: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil}, + {"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil}, + }, nil}, + + {"success", &Params{ParentPerm: 0750, RetainSession: true}, &MountDevOp{ + Target: MustAbs("/dev/"), + Mqueue: true, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{"devtmpfs", "/sysroot/dev", uintptr(0x6), 0, os.FileMode(0750)}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/zero", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/zero", "/sysroot/dev/zero", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/full", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/full", "/sysroot/dev/full", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/random", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/random", "/sysroot/dev/random", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/urandom", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/urandom", "/sysroot/dev/urandom", uintptr(0), true}, nil, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/tty", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"bindMount", expectArgs{"/host/dev/tty", "/sysroot/dev/tty", uintptr(0), true}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/0", "/sysroot/dev/stdin"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/1", "/sysroot/dev/stdout"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd/2", "/sysroot/dev/stderr"}, nil, nil}, + {"symlink", expectArgs{"/proc/self/fd", "/sysroot/dev/fd"}, nil, nil}, + {"symlink", expectArgs{"/proc/kcore", "/sysroot/dev/core"}, nil, nil}, + {"symlink", expectArgs{"pts/ptmx", "/sysroot/dev/ptmx"}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/shm", os.FileMode(0750)}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/pts", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"devpts", "/sysroot/dev/pts", "devpts", uintptr(0xa), "newinstance,ptmxmode=0666,mode=620"}, nil, nil}, + {"isatty", expectArgs{1}, true, nil}, + {"ensureFile", expectArgs{"/sysroot/dev/console", os.FileMode(0444), os.FileMode(0750)}, nil, nil}, + {"readlink", expectArgs{"/host/proc/self/fd/1"}, "/dev/pts/2", nil}, + {"bindMount", expectArgs{"/host/dev/pts/2", "/sysroot/dev/console", uintptr(0), false}, nil, nil}, + {"mkdir", expectArgs{"/sysroot/dev/mqueue", os.FileMode(0750)}, nil, nil}, + {"mount", expectArgs{"mqueue", "/sysroot/dev/mqueue", "mqueue", uintptr(0xe), ""}, nil, nil}, + {"remount", expectArgs{"/sysroot/dev", uintptr(1)}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*MountDevOp)(nil), false}, {"zero", new(MountDevOp), false}, diff --git a/container/initmkdir.go b/container/initmkdir.go index 54aa4f7..2f77a52 100644 --- a/container/initmkdir.go +++ b/container/initmkdir.go @@ -20,10 +20,10 @@ type MkdirOp struct { Perm os.FileMode } -func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil } -func (m *MkdirOp) early(*setupState) error { return nil } -func (m *MkdirOp) apply(*setupState) error { - return wrapErrSelf(os.MkdirAll(toSysroot(m.Path.String()), m.Perm)) +func (m *MkdirOp) Valid() bool { return m != nil && m.Path != nil } +func (m *MkdirOp) early(*setupState, syscallDispatcher) error { return nil } +func (m *MkdirOp) apply(_ *setupState, k syscallDispatcher) error { + return wrapErrSelf(k.mkdirAll(toSysroot(m.Path.String()), m.Perm)) } func (m *MkdirOp) Is(op Op) bool { diff --git a/container/initmkdir_test.go b/container/initmkdir_test.go index ad32624..1f54e74 100644 --- a/container/initmkdir_test.go +++ b/container/initmkdir_test.go @@ -1,8 +1,20 @@ package container -import "testing" +import ( + "os" + "testing" +) func TestMkdirOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"success", new(Params), &MkdirOp{ + Path: MustAbs("/.hakurei"), + Perm: 0500, + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/.hakurei", os.FileMode(0500)}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*MkdirOp)(nil), false}, {"zero", new(MkdirOp), false}, diff --git a/container/initoverlay.go b/container/initoverlay.go index 6822274..2c6f43c 100644 --- a/container/initoverlay.go +++ b/container/initoverlay.go @@ -3,11 +3,9 @@ package container import ( "encoding/gob" "fmt" - "os" - "path/filepath" + "io/fs" "slices" "strings" - . "syscall" ) const ( @@ -81,14 +79,14 @@ func (o *MountOverlayOp) Valid() bool { return o.Target != nil } -func (o *MountOverlayOp) early(*setupState) error { +func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error { if o.Work == nil && o.Upper != nil { switch o.Upper.String() { case FHSRoot: // ephemeral o.ephemeral = true // intermediate root not yet available default: - return msg.WrapErr(EINVAL, fmt.Sprintf("upperdir has unexpected value %q", o.Upper)) + return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("upperdir has unexpected value %q", o.Upper)) } } // readonly handled in apply @@ -96,11 +94,11 @@ func (o *MountOverlayOp) early(*setupState) error { if !o.ephemeral { if o.Upper != o.Work && (o.Upper == nil || o.Work == nil) { // unreachable - return msg.WrapErr(ENOTRECOVERABLE, "impossible overlay state reached") + return msg.WrapErr(fs.ErrClosed, "impossible overlay state reached") } if o.Upper != nil { - if v, err := filepath.EvalSymlinks(o.Upper.String()); err != nil { + if v, err := k.evalSymlinks(o.Upper.String()); err != nil { return wrapErrSelf(err) } else { o.upper = EscapeOverlayDataSegment(toHost(v)) @@ -108,7 +106,7 @@ func (o *MountOverlayOp) early(*setupState) error { } if o.Work != nil { - if v, err := filepath.EvalSymlinks(o.Work.String()); err != nil { + if v, err := k.evalSymlinks(o.Work.String()); err != nil { return wrapErrSelf(err) } else { o.work = EscapeOverlayDataSegment(toHost(v)) @@ -118,7 +116,7 @@ func (o *MountOverlayOp) early(*setupState) error { o.lower = make([]string, len(o.Lower)) for i, a := range o.Lower { // nil checked in Valid - if v, err := filepath.EvalSymlinks(a.String()); err != nil { + if v, err := k.evalSymlinks(a.String()); err != nil { return wrapErrSelf(err) } else { o.lower[i] = EscapeOverlayDataSegment(toHost(v)) @@ -127,19 +125,19 @@ func (o *MountOverlayOp) early(*setupState) error { return nil } -func (o *MountOverlayOp) apply(state *setupState) error { +func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error { target := toSysroot(o.Target.String()) - if err := os.MkdirAll(target, state.ParentPerm); err != nil { + if err := k.mkdirAll(target, state.ParentPerm); err != nil { return wrapErrSelf(err) } if o.ephemeral { var err error // these directories are created internally, therefore early (absolute, symlink, prefix, escape) is bypassed - if o.upper, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { + if o.upper, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayUpper); err != nil { return wrapErrSelf(err) } - if o.work, err = os.MkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { + if o.work, err = k.mkdirTemp(FHSRoot, intermediatePatternOverlayWork); err != nil { return wrapErrSelf(err) } } @@ -148,12 +146,12 @@ func (o *MountOverlayOp) apply(state *setupState) error { if o.upper == zeroString && o.work == zeroString { // readonly if len(o.Lower) < 2 { - return msg.WrapErr(EINVAL, "readonly overlay requires at least two lowerdir") + return msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir") } // "upperdir=" and "workdir=" may be omitted. In that case the overlay will be read-only } else { if len(o.Lower) == 0 { - return msg.WrapErr(EINVAL, "overlay requires at least one lowerdir") + return msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir") } options = append(options, OptionOverlayUpperdir+"="+o.upper, @@ -163,7 +161,7 @@ func (o *MountOverlayOp) apply(state *setupState) error { OptionOverlayLowerdir+"="+strings.Join(o.lower, SpecialOverlayPath), OptionOverlayUserxattr) - return wrapErrSuffix(Mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)), + return wrapErrSuffix(k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, SpecialOverlayOption)), fmt.Sprintf("cannot mount overlay on %q:", o.Target)) } diff --git a/container/initoverlay_test.go b/container/initoverlay_test.go index 68377dd..f5f0aec 100644 --- a/container/initoverlay_test.go +++ b/container/initoverlay_test.go @@ -1,8 +1,237 @@ package container -import "testing" +import ( + "errors" + "io/fs" + "os" + "testing" +) func TestMountOverlayOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"mkdirTemp invalid ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ + Target: MustAbs("/"), + Lower: []*Absolute{ + MustAbs("/var/lib/planterette/base/debian:f92c9052"), + MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), + }, + Upper: MustAbs("/proc/"), + }, nil, msg.WrapErr(fs.ErrInvalid, `upperdir has unexpected value "/proc/"`), nil, nil}, + + {"mkdirTemp upper ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ + Target: MustAbs("/"), + Lower: []*Absolute{ + MustAbs("/var/lib/planterette/base/debian:f92c9052"), + MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), + }, + Upper: MustAbs("/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil}, + {"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mkdirTemp work ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ + Target: MustAbs("/"), + Lower: []*Absolute{ + MustAbs("/var/lib/planterette/base/debian:f92c9052"), + MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), + }, + Upper: MustAbs("/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil}, + {"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil}, + {"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", errUnique}, + }, wrapErrSelf(errUnique)}, + + {"success ephemeral", &Params{ParentPerm: 0705}, &MountOverlayOp{ + Target: MustAbs("/"), + Lower: []*Absolute{ + MustAbs("/var/lib/planterette/base/debian:f92c9052"), + MustAbs("/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"), + }, + Upper: MustAbs("/"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, "/var/lib/planterette/base/debian:f92c9052", nil}, + {"evalSymlinks", expectArgs{"/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052"}, "/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot", os.FileMode(0705)}, nil, nil}, + {"mkdirTemp", expectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil}, + {"mkdirTemp", expectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil}, + {"mount", expectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" + + "upperdir=overlay.upper.32768," + + "workdir=overlay.work.32768," + + "lowerdir=" + + `/host/var/lib/planterette/base/debian\:f92c9052:` + + `/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` + + "userxattr"}, nil, nil}, + }, nil}, + + {"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{ + MustAbs("/mnt-root/nix/.ro-store"), + }, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil}, + }, msg.WrapErr(fs.ErrInvalid, "readonly overlay requires at least two lowerdir")}, + + {"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{ + MustAbs("/mnt-root/nix/.ro-store"), + MustAbs("/mnt-root/nix/.ro-store0"), + }, + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/.ro-store", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil}, + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + + "lowerdir=" + + "/host/mnt-root/nix/.ro-store:" + + "/host/mnt-root/nix/.ro-store0," + + "userxattr"}, nil, nil}, + }, nil}, + + {"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, + }, msg.WrapErr(fs.ErrInvalid, "overlay requires at least one lowerdir")}, + + {"evalSymlinks upper", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"evalSymlinks work", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"evalSymlinks lower", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"mkdirAll", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"mount", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, errUnique}, + }, wrapErrSuffix(errUnique, `cannot mount overlay on "/nix/store":`)}, + + {"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{MustAbs("/mnt-root/nix/.ro-store")}, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + + "upperdir=/host/mnt-root/nix/.rw-store/.upper," + + "workdir=/host/mnt-root/nix/.rw-store/.work," + + "lowerdir=/host/mnt-root/nix/ro-store," + + "userxattr"}, nil, nil}, + }, nil}, + + {"success", &Params{ParentPerm: 0700}, &MountOverlayOp{ + Target: MustAbs("/nix/store"), + Lower: []*Absolute{ + MustAbs("/mnt-root/nix/.ro-store"), + MustAbs("/mnt-root/nix/.ro-store0"), + MustAbs("/mnt-root/nix/.ro-store1"), + MustAbs("/mnt-root/nix/.ro-store2"), + MustAbs("/mnt-root/nix/.ro-store3"), + }, + Upper: MustAbs("/mnt-root/nix/.rw-store/upper"), + Work: MustAbs("/mnt-root/nix/.rw-store/work"), + }, []kexpect{ + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/upper"}, "/mnt-root/nix/.rw-store/.upper", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.rw-store/work"}, "/mnt-root/nix/.rw-store/.work", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/ro-store0", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store1"}, "/mnt-root/nix/ro-store1", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store2"}, "/mnt-root/nix/ro-store2", nil}, + {"evalSymlinks", expectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil}, + {"mount", expectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" + + "upperdir=/host/mnt-root/nix/.rw-store/.upper," + + "workdir=/host/mnt-root/nix/.rw-store/.work," + + "lowerdir=" + + "/host/mnt-root/nix/ro-store:" + + "/host/mnt-root/nix/ro-store0:" + + "/host/mnt-root/nix/ro-store1:" + + "/host/mnt-root/nix/ro-store2:" + + "/host/mnt-root/nix/ro-store3," + + "userxattr"}, nil, nil}, + }, nil}, + }) + + t.Run("unreachable", func(t *testing.T) { + t.Run("nil Upper non-nil Work not ephemeral", func(t *testing.T) { + wantErr := msg.WrapErr(fs.ErrClosed, "impossible overlay state reached") + if err := (&MountOverlayOp{ + Work: MustAbs("/"), + }).early(nil, nil); !errors.Is(err, wantErr) { + t.Errorf("apply: error = %v, want %v", err, wantErr) + } + }) + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*MountOverlayOp)(nil), false}, {"zero", new(MountOverlayOp), false}, diff --git a/container/initplace.go b/container/initplace.go index 2d98d94..8b88345 100644 --- a/container/initplace.go +++ b/container/initplace.go @@ -3,8 +3,7 @@ package container import ( "encoding/gob" "fmt" - "os" - . "syscall" + "syscall" ) const ( @@ -35,11 +34,11 @@ type TmpfileOp struct { Data []byte } -func (t *TmpfileOp) Valid() bool { return t != nil && t.Path != nil } -func (t *TmpfileOp) early(*setupState) error { return nil } -func (t *TmpfileOp) apply(state *setupState) error { +func (t *TmpfileOp) Valid() bool { return t != nil && t.Path != nil } +func (t *TmpfileOp) early(*setupState, syscallDispatcher) error { return nil } +func (t *TmpfileOp) apply(state *setupState, k syscallDispatcher) error { var tmpPath string - if f, err := os.CreateTemp(FHSRoot, intermediatePatternTmpfile); err != nil { + if f, err := k.createTemp(FHSRoot, intermediatePatternTmpfile); err != nil { return wrapErrSelf(err) } else if _, err = f.Write(t.Data); err != nil { return wrapErrSuffix(err, @@ -52,16 +51,16 @@ func (t *TmpfileOp) apply(state *setupState) error { } target := toSysroot(t.Path.String()) - if err := ensureFile(target, 0444, state.ParentPerm); err != nil { + if err := k.ensureFile(target, 0444, state.ParentPerm); err != nil { return err - } else if err = hostProc.bindMount( + } else if err = k.bindMount( tmpPath, target, - MS_RDONLY|MS_NODEV, + syscall.MS_RDONLY|syscall.MS_NODEV, false, ); err != nil { return err - } else if err = os.Remove(tmpPath); err != nil { + } else if err = k.remove(tmpPath); err != nil { return wrapErrSelf(err) } return nil diff --git a/container/initplace_test.go b/container/initplace_test.go index db3d955..02c3733 100644 --- a/container/initplace_test.go +++ b/container/initplace_test.go @@ -1,25 +1,94 @@ package container -import "testing" +import ( + "os" + "testing" +) func TestTmpfileOp(t *testing.T) { + const sampleDataString = `chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh` + var ( + samplePath = MustAbs("/etc/passwd") + sampleData = []byte(sampleDataString) + ) + + checkOpBehaviour(t, []opBehaviourTestCase{ + {"createTemp", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), errUnique}, + }, wrapErrSelf(errUnique)}, + + {"Write", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, writeErrOsFile{errUnique}, nil}, + }, wrapErrSuffix(errUnique, "cannot write to intermediate file:")}, + + {"Close", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, errUnique), nil}, + }, wrapErrSuffix(errUnique, "cannot close intermediate file:")}, + + {"ensureFile", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, + {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, errUnique}, + }, errUnique}, + + {"bindMount", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, + {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, errUnique}, + }, errUnique}, + + {"remove", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, + {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil}, + {"remove", expectArgs{"tmp.32768"}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"success", &Params{ParentPerm: 0700}, &TmpfileOp{ + Path: samplePath, + Data: sampleData, + }, nil, nil, []kexpect{ + {"createTemp", expectArgs{"/", "tmp.*"}, newCheckedFile(t, "tmp.32768", sampleDataString, nil), nil}, + {"ensureFile", expectArgs{"/sysroot/etc/passwd", os.FileMode(0444), os.FileMode(0700)}, nil, nil}, + {"bindMount", expectArgs{"tmp.32768", "/sysroot/etc/passwd", uintptr(0x5), false}, nil, nil}, + {"remove", expectArgs{"tmp.32768"}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*TmpfileOp)(nil), false}, {"zero", new(TmpfileOp), false}, - {"valid", &TmpfileOp{Path: MustAbs("/etc/passwd")}, true}, + {"valid", &TmpfileOp{Path: samplePath}, true}, }) checkOpsBuilder(t, []opsBuilderTestCase{ - {"noref", new(Ops).Place(MustAbs("/etc/passwd"), []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`)), Ops{ + {"noref", new(Ops).Place(samplePath, sampleData), Ops{ &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Path: samplePath, + Data: sampleData, }, }}, - {"ref", new(Ops).PlaceP(MustAbs("/etc/passwd"), new(*[]byte)), Ops{ + {"ref", new(Ops).PlaceP(samplePath, new(*[]byte)), Ops{ &TmpfileOp{ - Path: MustAbs("/etc/passwd"), + Path: samplePath, Data: []byte{}, }, }}, @@ -30,33 +99,33 @@ func TestTmpfileOp(t *testing.T) { {"differs path", &TmpfileOp{ Path: MustAbs("/etc/group"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Data: sampleData, }, &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Path: samplePath, + Data: sampleData, }, false}, {"differs data", &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh` + "\x00"), + Path: samplePath, + Data: append(sampleData, 0), }, &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Path: samplePath, + Data: sampleData, }, false}, {"equals", &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Path: samplePath, + Data: sampleData, }, &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Path: samplePath, + Data: sampleData, }, true}, }) checkOpMeta(t, []opMetaTestCase{ {"passwd", &TmpfileOp{ - Path: MustAbs("/etc/passwd"), - Data: []byte(`chronos:x:65534:65534:Hakurei:/var/empty:/bin/zsh`), + Path: samplePath, + Data: sampleData, }, "placing", `tmpfile "/etc/passwd" (49 bytes)`}, }) } diff --git a/container/initproc.go b/container/initproc.go index 9b9769c..fe02aeb 100644 --- a/container/initproc.go +++ b/container/initproc.go @@ -3,7 +3,6 @@ package container import ( "encoding/gob" "fmt" - "os" . "syscall" ) @@ -18,14 +17,14 @@ func (f *Ops) Proc(target *Absolute) *Ops { // MountProcOp mounts a new instance of [FstypeProc] on container path Target. type MountProcOp struct{ Target *Absolute } -func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil } -func (p *MountProcOp) early(*setupState) error { return nil } -func (p *MountProcOp) apply(state *setupState) error { +func (p *MountProcOp) Valid() bool { return p != nil && p.Target != nil } +func (p *MountProcOp) early(*setupState, syscallDispatcher) error { return nil } +func (p *MountProcOp) apply(state *setupState, k syscallDispatcher) error { target := toSysroot(p.Target.String()) - if err := os.MkdirAll(target, state.ParentPerm); err != nil { + if err := k.mkdirAll(target, state.ParentPerm); err != nil { return wrapErrSelf(err) } - return wrapErrSuffix(Mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString), + return wrapErrSuffix(k.mount(SourceProc, target, FstypeProc, MS_NOSUID|MS_NOEXEC|MS_NODEV, zeroString), fmt.Sprintf("cannot mount proc on %q:", p.Target.String())) } diff --git a/container/initproc_test.go b/container/initproc_test.go index eb55e4e..2d72bd7 100644 --- a/container/initproc_test.go +++ b/container/initproc_test.go @@ -1,8 +1,28 @@ package container -import "testing" +import ( + "os" + "testing" +) func TestMountProcOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"mkdir", &Params{ParentPerm: 0755}, + &MountProcOp{ + Target: MustAbs("/proc/"), + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0755)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"success", &Params{ParentPerm: 0700}, + &MountProcOp{ + Target: MustAbs("/proc/"), + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/proc", os.FileMode(0700)}, nil, nil}, + {"mount", expectArgs{"proc", "/sysroot/proc", "proc", uintptr(0xe), ""}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*MountProcOp)(nil), false}, {"zero", new(MountProcOp), false}, diff --git a/container/initremount.go b/container/initremount.go index 74bea09..af58c74 100644 --- a/container/initremount.go +++ b/container/initremount.go @@ -19,10 +19,10 @@ type RemountOp struct { Flags uintptr } -func (r *RemountOp) Valid() bool { return r != nil && r.Target != nil } -func (*RemountOp) early(*setupState) error { return nil } -func (r *RemountOp) apply(*setupState) error { - return wrapErrSuffix(hostProc.remount(toSysroot(r.Target.String()), r.Flags), +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 wrapErrSuffix(k.remount(toSysroot(r.Target.String()), r.Flags), fmt.Sprintf("cannot remount %q:", r.Target)) } diff --git a/container/initremount_test.go b/container/initremount_test.go index db5476e..22c978f 100644 --- a/container/initremount_test.go +++ b/container/initremount_test.go @@ -6,6 +6,15 @@ import ( ) func TestRemountOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"success", new(Params), &RemountOp{ + Target: MustAbs("/"), + Flags: syscall.MS_RDONLY, + }, nil, nil, []kexpect{ + {"remount", expectArgs{"/sysroot", uintptr(1)}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*RemountOp)(nil), false}, {"zero", new(RemountOp), false}, diff --git a/container/initsymlink.go b/container/initsymlink.go index 3060817..fb80867 100644 --- a/container/initsymlink.go +++ b/container/initsymlink.go @@ -3,9 +3,8 @@ package container import ( "encoding/gob" "fmt" - "os" + "io/fs" "path" - "syscall" ) func init() { gob.Register(new(SymlinkOp)) } @@ -28,12 +27,12 @@ type SymlinkOp struct { func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkName != zeroString } -func (l *SymlinkOp) early(*setupState) error { +func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error { if l.Dereference { if !isAbs(l.LinkName) { - return msg.WrapErr(syscall.EBADE, fmt.Sprintf("path %q is not absolute", l.LinkName)) + return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("path %q is not absolute", l.LinkName)) } - if name, err := os.Readlink(l.LinkName); err != nil { + if name, err := k.readlink(l.LinkName); err != nil { return wrapErrSelf(err) } else { l.LinkName = name @@ -42,15 +41,12 @@ func (l *SymlinkOp) early(*setupState) error { return nil } -func (l *SymlinkOp) apply(state *setupState) error { +func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error { target := toSysroot(l.Target.String()) - if err := os.MkdirAll(path.Dir(target), state.ParentPerm); err != nil { + if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil { return wrapErrSelf(err) } - if err := os.Symlink(l.LinkName, target); err != nil { - return wrapErrSelf(err) - } - return nil + return wrapErrSelf(k.symlink(l.LinkName, target)) } func (l *SymlinkOp) Is(op Op) bool { diff --git a/container/initsymlink_test.go b/container/initsymlink_test.go index 4ae2da8..1bd0589 100644 --- a/container/initsymlink_test.go +++ b/container/initsymlink_test.go @@ -1,8 +1,54 @@ package container -import "testing" +import ( + "io/fs" + "os" + "testing" +) func TestSymlinkOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"mkdir", &Params{ParentPerm: 0700}, &SymlinkOp{ + Target: MustAbs("/etc/nixos"), + LinkName: "/etc/static/nixos", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, errUnique}, + }, wrapErrSelf(errUnique)}, + + {"abs", &Params{ParentPerm: 0755}, &SymlinkOp{ + Target: MustAbs("/etc/mtab"), + LinkName: "etc/mtab", + Dereference: true, + }, nil, msg.WrapErr(fs.ErrInvalid, `path "etc/mtab" is not absolute`), nil, nil}, + + {"readlink", &Params{ParentPerm: 0755}, &SymlinkOp{ + Target: MustAbs("/etc/mtab"), + LinkName: "/etc/mtab", + Dereference: true, + }, []kexpect{ + {"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", errUnique}, + }, wrapErrSelf(errUnique), nil, nil}, + + {"success noderef", &Params{ParentPerm: 0700}, &SymlinkOp{ + Target: MustAbs("/etc/nixos"), + LinkName: "/etc/static/nixos", + }, nil, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0700)}, nil, nil}, + {"symlink", expectArgs{"/etc/static/nixos", "/sysroot/etc/nixos"}, nil, nil}, + }, nil}, + + {"success", &Params{ParentPerm: 0755}, &SymlinkOp{ + Target: MustAbs("/etc/mtab"), + LinkName: "/etc/mtab", + Dereference: true, + }, []kexpect{ + {"readlink", expectArgs{"/etc/mtab"}, "/proc/mounts", nil}, + }, nil, []kexpect{ + {"mkdirAll", expectArgs{"/sysroot/etc", os.FileMode(0755)}, nil, nil}, + {"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*SymlinkOp)(nil), false}, {"zero", new(SymlinkOp), false}, diff --git a/container/inittmpfs.go b/container/inittmpfs.go index d313a11..0d50e46 100644 --- a/container/inittmpfs.go +++ b/container/inittmpfs.go @@ -3,6 +3,7 @@ package container import ( "encoding/gob" "fmt" + "io/fs" "math" "os" . "syscall" @@ -31,13 +32,13 @@ type MountTmpfsOp struct { Perm os.FileMode } -func (t *MountTmpfsOp) Valid() bool { return t != nil && t.Path != nil && t.FSName != zeroString } -func (t *MountTmpfsOp) early(*setupState) error { return nil } -func (t *MountTmpfsOp) apply(*setupState) error { +func (t *MountTmpfsOp) Valid() bool { return t != nil && t.Path != nil && t.FSName != zeroString } +func (t *MountTmpfsOp) early(*setupState, syscallDispatcher) error { return nil } +func (t *MountTmpfsOp) apply(_ *setupState, k syscallDispatcher) error { if t.Size < 0 || t.Size > math.MaxUint>>1 { - return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size)) + return msg.WrapErr(fs.ErrInvalid, fmt.Sprintf("size %d out of bounds", t.Size)) } - return mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm) + return k.mountTmpfs(t.FSName, toSysroot(t.Path.String()), t.Flags, t.Size, t.Perm) } func (t *MountTmpfsOp) Is(op Op) bool { diff --git a/container/inittmpfs_test.go b/container/inittmpfs_test.go index c924586..8110f44 100644 --- a/container/inittmpfs_test.go +++ b/container/inittmpfs_test.go @@ -1,11 +1,34 @@ package container import ( + "io/fs" + "os" "syscall" "testing" ) func TestMountTmpfsOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"size oob", new(Params), &MountTmpfsOp{ + Size: -1, + }, nil, nil, nil, msg.WrapErr(fs.ErrInvalid, "size -1 out of bounds")}, + + {"success", new(Params), &MountTmpfsOp{ + FSName: "ephemeral", + Path: MustAbs("/run/user/1000/"), + Size: 1 << 10, + Perm: 0700, + }, nil, nil, []kexpect{ + {"mountTmpfs", expectArgs{ + "ephemeral", // fsname + "/sysroot/run/user/1000", // target + uintptr(0), // flags + 0x400, // size + os.FileMode(0700), // perm + }, nil, nil}, + }, nil}, + }) + checkOpsValid(t, []opValidTestCase{ {"nil", (*MountTmpfsOp)(nil), false}, {"zero", new(MountTmpfsOp), false}, diff --git a/container/mount_test.go b/container/mount_test.go index 64cac28..eb90477 100644 --- a/container/mount_test.go +++ b/container/mount_test.go @@ -2,7 +2,6 @@ package container import ( "os" - "slices" "testing" ) @@ -48,85 +47,3 @@ func TestEscapeOverlayDataSegment(t *testing.T) { }) } } - -type opValidTestCase struct { - name string - op Op - want bool -} - -func checkOpsValid(t *testing.T, testCases []opValidTestCase) { - t.Run("valid", func(t *testing.T) { - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.op.Valid(); got != tc.want { - t.Errorf("Valid: %v, want %v", got, tc.want) - } - }) - } - }) -} - -type opsBuilderTestCase struct { - name string - ops *Ops - want Ops -} - -func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) { - t.Run("build", func(t *testing.T) { - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { - t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want) - } - }) - } - }) -} - -type opIsTestCase struct { - name string - op, v Op - want bool -} - -func checkOpIs(t *testing.T, testCases []opIsTestCase) { - t.Run("is", func(t *testing.T) { - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.op.Is(tc.v); got != tc.want { - t.Errorf("Is: %v, want %v", got, tc.want) - } - }) - } - }) -} - -type opMetaTestCase struct { - name string - op Op - - wantPrefix string - wantString string -} - -func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { - t.Run("meta", func(t *testing.T) { - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Run("prefix", func(t *testing.T) { - if got := tc.op.prefix(); got != tc.wantPrefix { - t.Errorf("prefix: %q, want %q", got, tc.wantPrefix) - } - }) - - t.Run("string", func(t *testing.T) { - if got := tc.op.String(); got != tc.wantString { - t.Errorf("String: %s, want %s", got, tc.wantString) - } - }) - }) - } - }) -} diff --git a/container/msg.go b/container/msg.go index 3f29e99..f66c804 100644 --- a/container/msg.go +++ b/container/msg.go @@ -1,8 +1,13 @@ package container import ( + "errors" + "fmt" "log" + "os" + "reflect" "sync/atomic" + "testing" ) type Msg interface { @@ -32,7 +37,27 @@ func (msg *DefaultMsg) Verbosef(format string, v ...any) { } } +// checkedWrappedErr implements error with strict checks for wrapped values. +type checkedWrappedErr struct { + err error + a []any +} + +func (c *checkedWrappedErr) Error() string { return fmt.Sprintf("%v, a = %s", c.err, c.a) } +func (c *checkedWrappedErr) Is(err error) bool { + var concreteErr *checkedWrappedErr + if !errors.As(err, &concreteErr) { + return false + } + return reflect.DeepEqual(c, concreteErr) +} + func (msg *DefaultMsg) WrapErr(err error, a ...any) error { + // provide a mostly bulletproof path to bypass this behaviour in tests + if testing.Testing() && os.Getenv("GOPATH") != Nonexistent { + return &checkedWrappedErr{err, a} + } + log.Println(a...) return err } diff --git a/container/msg_test.go b/container/msg_test.go index 6945c89..2ad4249 100644 --- a/container/msg_test.go +++ b/container/msg_test.go @@ -1,6 +1,7 @@ package container_test import ( + "errors" "log" "strings" "sync/atomic" @@ -12,6 +13,9 @@ import ( ) func TestDefaultMsg(t *testing.T) { + // bypass WrapErr testing behaviour + t.Setenv("GOPATH", container.Nonexistent) + { w := log.Writer() f := log.Flags() @@ -79,6 +83,25 @@ func TestDefaultMsg(t *testing.T) { // the function is a noop t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() }) + + t.Run("checkedWrappedErr", func(t *testing.T) { + // temporarily re-enable testing behaviour + t.Setenv("GOPATH", "") + wrappedErr := msg.WrapErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:", syscall.ENOTRECOVERABLE) + + t.Run("string", func(t *testing.T) { + want := "state not recoverable, a = [cannot cuddle cat: state not recoverable]" + if got := wrappedErr.Error(); got != want { + t.Errorf("Error: %q, want %q", got, want) + } + }) + + t.Run("bad concrete type", func(t *testing.T) { + if errors.Is(wrappedErr, syscall.ENOTRECOVERABLE) { + t.Error("incorrect type assertion") + } + }) + }) } type panicWriter struct{} diff --git a/container/path.go b/container/path.go index 8a01c93..be3cd38 100644 --- a/container/path.go +++ b/container/path.go @@ -151,8 +151,7 @@ func (p *procPaths) mountinfo(f func(d *vfs.MountInfoDecoder) error) error { d := vfs.NewMountInfoDecoder(r) err0 := f(d) if err = r.Close(); err != nil { - return wrapErrSuffix(err, - "cannot close mountinfo:") + return wrapErrSelf(err) } else if err = d.Err(); err != nil { return wrapErrSuffix(err, "cannot parse mountinfo:") diff --git a/container/path_test.go b/container/path_test.go index aea0e54..95fd246 100644 --- a/container/path_test.go +++ b/container/path_test.go @@ -2,6 +2,7 @@ package container import ( "errors" + "fmt" "io" "math" "os" @@ -55,10 +56,18 @@ func InternalToHostOvlEscape(s string) string { return EscapeOverlayDataSegment( func TestCreateFile(t *testing.T) { t.Run("nonexistent", func(t *testing.T) { - if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !os.IsNotExist(err) { + if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{ + Op: "mkdir", + Path: "/proc/nonexistent", + Err: syscall.ENOENT, + })) { t.Errorf("createFile: error = %v", err) } - if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !os.IsNotExist(err) { + if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !errors.Is(err, wrapErrSelf(&os.PathError{ + Op: "open", + Path: "/proc/nonexistent", + Err: syscall.ENOENT, + })) { t.Errorf("createFile: error = %v", err) } }) @@ -110,17 +119,26 @@ func TestEnsureFile(t *testing.T) { if err := os.Chmod(tempDir, 0); err != nil { t.Fatalf("Chmod: error = %v", err) } - if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, syscall.EACCES) { - t.Errorf("ensureFile: error = %v, want %v", err, syscall.EACCES) + + wantErr := wrapErrSelf(&os.PathError{ + Op: "stat", + Path: pathname, + Err: syscall.EACCES, + }) + if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) { + t.Errorf("ensureFile: error = %v, want %v", err, wantErr) } + if err := os.Chmod(tempDir, 0755); err != nil { t.Fatalf("Chmod: error = %v", err) } }) t.Run("directory", func(t *testing.T) { - if err := ensureFile(t.TempDir(), 0644, 0755); !errors.Is(err, syscall.EISDIR) { - t.Errorf("ensureFile: error = %v, want %v", err, syscall.EISDIR) + pathname := t.TempDir() + wantErr := msg.WrapErr(syscall.EISDIR, fmt.Sprintf("path %q is a directory", pathname)) + if err := ensureFile(pathname, 0644, 0755); !errors.Is(err, wantErr) { + t.Errorf("ensureFile: error = %v, want %v", err, wantErr) } }) @@ -159,8 +177,13 @@ func TestProcPaths(t *testing.T) { t.Run("mountinfo", func(t *testing.T) { t.Run("nonexistent", func(t *testing.T) { nonexistentProc := newProcPaths(t.TempDir()) - if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !os.IsNotExist(err) { - t.Errorf("mountinfo: error = %v", err) + wantErr := wrapErrSelf(&os.PathError{ + Op: "open", + Path: nonexistentProc.self + "/mountinfo", + Err: syscall.ENOENT, + }) + if err := nonexistentProc.mountinfo(func(*vfs.MountInfoDecoder) error { return syscall.EINVAL }); !errors.Is(err, wantErr) { + t.Errorf("mountinfo: error = %v, want %v", err, wantErr) } }) @@ -193,7 +216,13 @@ func TestProcPaths(t *testing.T) { }) t.Run("closed", func(t *testing.T) { - if err := newProcPaths(tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { + p := newProcPaths(tempDir) + wantErr := wrapErrSelf(&os.PathError{ + Op: "close", + Path: p.self + "/mountinfo", + Err: os.ErrClosed, + }) + if err := p.mountinfo(func(d *vfs.MountInfoDecoder) error { v := reflect.ValueOf(d).Elem().FieldByName("s").Elem().FieldByName("r") v = reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())) if f, ok := v.Elem().Interface().(io.ReadCloser); !ok { @@ -202,8 +231,8 @@ func TestProcPaths(t *testing.T) { } else { return f.Close() } - }); !errors.Is(err, os.ErrClosed) { - t.Errorf("mountinfo: error = %v, want %v", err, os.ErrClosed) + }); !errors.Is(err, wantErr) { + t.Errorf("mountinfo: error = %v, want %v", err, wantErr) } }) @@ -213,8 +242,9 @@ func TestProcPaths(t *testing.T) { t.Fatalf("WriteFile: error = %v", err) } - if err := newProcPaths(tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, vfs.ErrMountInfoFields) { - t.Fatalf("mountinfo: error = %v, want %v", err, vfs.ErrMountInfoFields) + wantErr := wrapErrSuffix(vfs.ErrMountInfoFields, "cannot parse mountinfo:") + if err := newProcPaths(tempDir).mountinfo(func(d *vfs.MountInfoDecoder) error { return d.Decode(new(*vfs.MountInfo)) }); !errors.Is(err, wantErr) { + t.Fatalf("mountinfo: error = %v, want %v", err, wantErr) } }) }) diff --git a/container/syscall.go b/container/syscall.go index 20ac068..edb6e7e 100644 --- a/container/syscall.go +++ b/container/syscall.go @@ -2,6 +2,7 @@ package container import ( "syscall" + "unsafe" ) // SetPtracer allows processes to ptrace(2) the calling process. @@ -37,6 +38,18 @@ func SetNoNewPrivs() error { return errno } +// Isatty tests whether a file descriptor refers to a terminal. +func Isatty(fd int) bool { + var buf [8]byte + r, _, _ := syscall.Syscall( + syscall.SYS_IOCTL, + uintptr(fd), + syscall.TIOCGWINSZ, + uintptr(unsafe.Pointer(&buf[0])), + ) + return r == 0 +} + // IgnoringEINTR makes a function call and repeats it if it returns an // EINTR error. This appears to be required even though we install all // signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.