From ae2df2c450dd655b93a950124b6ab16e2b4d967b Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 25 Sep 2025 13:46:21 +0900 Subject: [PATCH] internal: remove sys package This package is replaced by container/stub. Remove and replace it with unexported implementation for the upcoming test suite rewrite. Signed-off-by: Ophestra --- cmd/hakurei/command.go | 11 +- cmd/hakurei/main.go | 3 - cmd/hakurei/parse.go | 5 +- cmd/hakurei/print.go | 8 +- internal/app/app.go | 8 +- internal/app/app_stub_test.go | 221 +++++++++++++++++--------- internal/app/app_test.go | 91 ++++++----- internal/app/container.go | 41 ++--- internal/app/dispatcher.go | 99 ++++++++++++ internal/app/export_test.go | 16 -- internal/app/{seal.go => finalise.go} | 203 +++++++++++------------ internal/{sys => app}/hsu.go | 40 +++-- internal/app/paths.go | 36 +++++ internal/app/process.go | 54 +++---- internal/sys/interface.go | 76 --------- internal/sys/std.go | 44 ----- test/test.py | 2 +- 17 files changed, 516 insertions(+), 442 deletions(-) create mode 100644 internal/app/dispatcher.go delete mode 100644 internal/app/export_test.go rename internal/app/{seal.go => finalise.go} (73%) rename internal/{sys => app}/hsu.go (70%) create mode 100644 internal/app/paths.go delete mode 100644 internal/sys/interface.go delete mode 100644 internal/sys/std.go diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 0600a55..1671902 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -18,7 +18,6 @@ import ( "hakurei.app/internal/app" "hakurei.app/internal/app/state" "hakurei.app/internal/hlog" - "hakurei.app/internal/sys" "hakurei.app/system" "hakurei.app/system/dbus" ) @@ -43,7 +42,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { config := tryPath(args[0]) config.Args = append(config.Args, args[1:]...) - app.Main(ctx, std, config) + app.Main(ctx, config) panic("unreachable") }) @@ -79,7 +78,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { passwd *user.User passwdOnce sync.Once passwdFunc = func() { - us := strconv.Itoa(sys.MustUid(std, flagIdentity)) + us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(), flagIdentity)) if u, err := user.LookupId(us); err != nil { hlog.Verbosef("cannot look up uid %s", us) passwd = &user.User{ @@ -163,7 +162,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { } } - app.Main(ctx, std, config) + app.Main(ctx, config) panic("unreachable") }). Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), @@ -219,7 +218,9 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command { { var flagShort bool c.NewCommand("ps", "List active instances", func(args []string) error { - printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), flagShort, flagJSON) + var sc hst.Paths + app.CopyPaths(&sc, new(app.Hsu).MustID()) + printPs(os.Stdout, time.Now().UTC(), state.NewMulti(sc.RunDirPath.String()), flagShort, flagJSON) return errSuccess }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") } diff --git a/cmd/hakurei/main.go b/cmd/hakurei/main.go index b71f50c..2b9193b 100644 --- a/cmd/hakurei/main.go +++ b/cmd/hakurei/main.go @@ -15,7 +15,6 @@ import ( "hakurei.app/container" "hakurei.app/internal" "hakurei.app/internal/hlog" - "hakurei.app/internal/sys" ) var ( @@ -27,8 +26,6 @@ var ( func init() { hlog.Prepare("hakurei") } -var std sys.State = new(sys.Std) - func main() { // early init path, skips root check and duplicate PR_SET_DUMPABLE container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index 56ff83e..897382e 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -11,6 +11,7 @@ import ( "syscall" "hakurei.app/hst" + "hakurei.app/internal/app" "hakurei.app/internal/app/state" "hakurei.app/internal/hlog" ) @@ -87,7 +88,9 @@ func tryShort(name string) (config *hst.Config, entry *state.State) { if likePrefix && len(name) >= 8 { hlog.Verbose("argument looks like prefix") - s := state.NewMulti(std.Paths().RunDirPath.String()) + var sc hst.Paths + app.CopyPaths(&sc, new(app.Hsu).MustID()) + s := state.NewMulti(sc.RunDirPath.String()) if entries, err := state.Join(s); err != nil { log.Printf("cannot join store: %v", err) // drop to fetch from file diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 387b5c2..938517b 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -12,8 +12,8 @@ import ( "time" "hakurei.app/hst" + "hakurei.app/internal/app" "hakurei.app/internal/app/state" - "hakurei.app/internal/sys" "hakurei.app/system/dbus" ) @@ -21,10 +21,8 @@ func printShowSystem(output io.Writer, short, flagJSON bool) { t := newPrinter(output) defer t.MustFlush() - info := &hst.Info{ - Paths: std.Paths(), - User: sys.MustGetUserID(std), - } + info := &hst.Info{User: new(app.Hsu).MustID()} + app.CopyPaths(&info.Paths, info.User) if flagJSON { printJSON(output, short, info) diff --git a/internal/app/app.go b/internal/app/app.go index 4ecf1cc..b533156 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -8,19 +8,17 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app/state" - "hakurei.app/internal/sys" ) // Main runs an app according to [hst.Config] and terminates. Main does not return. -func Main(ctx context.Context, k sys.State, config *hst.Config) { +func Main(ctx context.Context, config *hst.Config) { var id state.ID if err := state.NewAppID(&id); err != nil { log.Fatal(err) } - var seal outcome - seal.id = &stringPair[state.ID]{id, id.String()} - if err := seal.finalise(ctx, k, config); err != nil { + seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}} + if err := seal.finalise(ctx, config); err != nil { printMessageError("cannot seal app:", err) os.Exit(1) } diff --git a/internal/app/app_stub_test.go b/internal/app/app_stub_test.go index f386eb0..ff21849 100644 --- a/internal/app/app_stub_test.go +++ b/internal/app/app_stub_test.go @@ -1,34 +1,24 @@ -package app_test +package app import ( "fmt" "io/fs" "log" + "os/exec" "os/user" - "strconv" - - "hakurei.app/hst" ) -// fs methods are not implemented using a real FS -// to help better understand filesystem access behaviour type stubNixOS struct { lookPathErr map[string]error usernameErr map[string]error } -func (s *stubNixOS) Getuid() int { return 1971 } -func (s *stubNixOS) Getgid() int { return 100 } -func (s *stubNixOS) TempDir() string { return "/tmp" } -func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/hakurei" } -func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) } -func (s *stubNixOS) EvalSymlinks(path string) (string, error) { return path, nil } -func (s *stubNixOS) Uid(aid int) (int, error) { return 1000000 + 0*10000 + aid, nil } +func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") } -func (s *stubNixOS) Println(v ...any) { log.Println(v...) } -func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) } +func (k *stubNixOS) getuid() int { return 1971 } +func (k *stubNixOS) getgid() int { return 100 } -func (s *stubNixOS) LookupEnv(key string) (string, bool) { +func (k *stubNixOS) lookupEnv(key string) (string, bool) { switch key { case "SHELL": return "/run/current-system/sw/bin/zsh", true @@ -40,6 +30,8 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) { return "", false case "HOME": return "/home/ophestra", true + case "XDG_RUNTIME_DIR": + return "/run/user/1971", true case "XDG_CONFIG_HOME": return "/home/ophestra/xdg/config", true default: @@ -47,61 +39,7 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) { } } -func (s *stubNixOS) LookPath(file string) (string, error) { - if s.lookPathErr != nil { - if err, ok := s.lookPathErr[file]; ok { - return "", err - } - } - - switch file { - case "zsh": - return "/run/current-system/sw/bin/zsh", nil - default: - panic(fmt.Sprintf("attempted to look up unexpected executable %q", file)) - } -} - -func (s *stubNixOS) LookupGroup(name string) (*user.Group, error) { - switch name { - case "video": - return &user.Group{Gid: "26", Name: "video"}, nil - default: - return nil, user.UnknownGroupError(name) - } -} - -func (s *stubNixOS) ReadDir(name string) ([]fs.DirEntry, error) { - switch name { - case "/": - return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib", - "lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var") - case "/run": - return stubDirEntries("agetty.reload", "binfmt", "booted-system", - "credentials", "cryptsetup", "current-system", "dbus", "host", "keys", - "libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager", - "nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm", - "store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2", - "user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state") - case "/etc": - return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default", - "ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid", - "hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd", - "libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm", - "machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc", - "netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf", - "opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", - "profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba", - "sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh", - "ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo", - "tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc", - "zoneinfo", "zprofile", "zshenv", "zshrc") - default: - panic(fmt.Sprintf("attempted to read unexpected directory %q", name)) - } -} - -func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) { +func (k *stubNixOS) stat(name string) (fs.FileInfo, error) { switch name { case "/var/run/nscd": return nil, nil @@ -118,17 +56,144 @@ func (s *stubNixOS) Stat(name string) (fs.FileInfo, error) { } } -func (s *stubNixOS) Open(name string) (fs.File, error) { +func (k *stubNixOS) readdir(name string) ([]fs.DirEntry, error) { switch name { + case "/": + return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib", + "lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var") + + case "/run": + return stubDirEntries("agetty.reload", "binfmt", "booted-system", + "credentials", "cryptsetup", "current-system", "dbus", "host", "keys", + "libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager", + "nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm", + "store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2", + "user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state") + + case "/etc": + return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default", + "ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid", + "hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd", + "libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm", + "machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc", + "netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf", + "opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", + "profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba", + "sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh", + "ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo", + "tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc", + "zoneinfo", "zprofile", "zshenv", "zshrc") + default: - panic(fmt.Sprintf("attempted to open unexpected file %q", name)) + panic(fmt.Sprintf("attempted to read unexpected directory %q", name)) } } -func (s *stubNixOS) Paths() hst.Paths { - return hst.Paths{ - SharePath: m("/tmp/hakurei.1971"), - RuntimePath: m("/run/user/1971"), - RunDirPath: m("/run/user/1971/hakurei"), +func (k *stubNixOS) tempdir() string { return "/tmp/" } + +func (k *stubNixOS) evalSymlinks(path string) (string, error) { + switch path { + case "/run/user/1971": + return "/run/user/1971", nil + case "/tmp/hakurei.0": + return "/tmp/hakurei.0", nil + case "/run/dbus": + return "/run/dbus", nil + case "/dev/kvm": + return "/dev/kvm", nil + case "/etc/": + return "/etc/", nil + case "/bin": + return "/bin", nil + case "/boot": + return "/boot", nil + case "/home": + return "/home", nil + case "/lib": + return "/lib", nil + case "/lib64": + return "/lib64", nil + case "/nix": + return "/nix", nil + case "/root": + return "/root", nil + case "/run": + return "/run", nil + case "/srv": + return "/srv", nil + case "/sys": + return "/sys", nil + case "/usr": + return "/usr", nil + case "/var": + return "/var", nil + case "/dev/dri": + return "/dev/dri", nil + case "/usr/bin/": + return "/usr/bin/", nil + case "/nix/store": + return "/nix/store", nil + case "/run/current-system": + return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos-system-satori-25.05.99999999.aaaaaaa", nil + case "/sys/block": + return "/sys/block", nil + case "/sys/bus": + return "/sys/bus", nil + case "/sys/class": + return "/sys/class", nil + case "/sys/dev": + return "/sys/dev", nil + case "/sys/devices": + return "/sys/devices", nil + case "/run/opengl-driver": + return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-graphics-drivers", nil + case "/var/lib/persist/module/hakurei/0/1": + return "/var/lib/persist/module/hakurei/0/1", nil + default: + panic(fmt.Sprintf("attempted to evaluate unexpected path %q", path)) } } + +func (k *stubNixOS) lookPath(file string) (string, error) { + if k.lookPathErr != nil { + if err, ok := k.lookPathErr[file]; ok { + return "", err + } + } + + switch file { + case "zsh": + return "/run/current-system/sw/bin/zsh", nil + default: + panic(fmt.Sprintf("attempted to look up unexpected executable %q", file)) + } +} + +func (k *stubNixOS) lookupGroupId(name string) (string, error) { + switch name { + case "video": + return "26", nil + default: + return "", user.UnknownGroupError(name) + } +} + +func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) { + switch cmd.Path { + case "/proc/nonexistent/hsu": + return []byte{'0'}, nil + default: + panic(fmt.Sprintf("unexpected cmd %#v", cmd)) + } +} + +func (k *stubNixOS) overflowUid() int { return 65534 } +func (k *stubNixOS) overflowGid() int { return 65534 } + +func (k *stubNixOS) mustHsuPath() string { return "/proc/nonexistent/hsu" } + +func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) } + +func (k *stubNixOS) isVerbose() bool { return true } +func (k *stubNixOS) verbose(v ...any) { log.Print(v...) } +func (k *stubNixOS) verbosef(format string, v ...any) { log.Printf(format, v...) } diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 9da7239..005b721 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -1,4 +1,4 @@ -package app_test +package app import ( "context" @@ -13,9 +13,7 @@ import ( "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/hst" - "hakurei.app/internal/app" "hakurei.app/internal/app/state" - "hakurei.app/internal/sys" "hakurei.app/system" "hakurei.app/system/acl" "hakurei.app/system/dbus" @@ -24,7 +22,7 @@ import ( func TestApp(t *testing.T) { testCases := []struct { name string - os sys.State + k syscallDispatcher config *hst.Config id state.ID wantSys *system.I @@ -40,11 +38,11 @@ func TestApp(t *testing.T) { 0xb9, 0xa6, 0x07, 0xac, }, system.New(context.TODO(), 1000000). - Ensure("/tmp/hakurei.1971", 0711). - Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). - Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute). - Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). - Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute), + Ensure("/tmp/hakurei.0", 0711). + Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute). + Ensure("/tmp/hakurei.0/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/0", acl.Read, acl.Write, acl.Execute). + Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute). + Ensure("/tmp/hakurei.0/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/0", acl.Read, acl.Write, acl.Execute), &container.Params{ Dir: m("/home/chronos"), Path: m("/run/current-system/sw/bin/zsh"), @@ -71,8 +69,8 @@ func TestApp(t *testing.T) { Tmpfs(m("/run/dbus"), 8192, 0755). Remount(m("/dev/"), syscall.MS_RDONLY). Tmpfs(m("/run/user/"), 4096, 0755). - Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable). - Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), container.BindWritable). + Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), container.BindWritable). + Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), container.BindWritable). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). Remount(m("/"), syscall.MS_RDONLY), @@ -132,19 +130,19 @@ func TestApp(t *testing.T) { 0x9b, 0x64, 0xce, 0x7c, }, system.New(context.TODO(), 1000009). - Ensure("/tmp/hakurei.1971", 0711). - Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). - Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute). - Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). - Ensure("/tmp/hakurei.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute). - Ephemeral(system.Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c", 0711). - Wayland(new(*os.File), "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). + Ensure("/tmp/hakurei.0", 0711). + Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute). + Ensure("/tmp/hakurei.0/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/9", acl.Read, acl.Write, acl.Execute). + Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute). + Ensure("/tmp/hakurei.0/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/9", acl.Read, acl.Write, acl.Execute). + Ephemeral(system.Process, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c", 0711). + Wayland(new(*os.File), "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute). Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset Ephemeral(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", acl.Execute). Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"). CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256). - MustProxyDBus("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{ + MustProxyDBus("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{ Talk: []string{ "org.freedesktop.Notifications", "org.freedesktop.FileManager1", @@ -166,7 +164,7 @@ func TestApp(t *testing.T) { "org.freedesktop.portal.*": "@/org/freedesktop/portal/*", }, Filter: true, - }, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{ + }, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{ Talk: []string{ "org.bluez", "org.freedesktop.Avahi", @@ -174,8 +172,8 @@ func TestApp(t *testing.T) { }, Filter: true, }). - UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). - UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), + UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). + UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), &container.Params{ Dir: m("/home/chronos"), Path: m("/run/current-system/sw/bin/zsh"), @@ -208,15 +206,15 @@ func TestApp(t *testing.T) { Tmpfs(m("/run/dbus"), 8192, 0755). Remount(m("/dev/"), syscall.MS_RDONLY). Tmpfs(m("/run/user/"), 4096, 0755). - Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable). - Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), container.BindWritable). + Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), container.BindWritable). + Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), container.BindWritable). Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")). - Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0). + Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0). Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0). Place(m(hst.Tmp+"/pulse-cookie"), nil). - Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). - Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). + Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). + Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). Remount(m("/"), syscall.MS_RDONLY), SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, HostNet: true, @@ -283,19 +281,19 @@ func TestApp(t *testing.T) { 0xb4, 0x6e, 0xb5, 0xc1, }, system.New(context.TODO(), 1000001). - Ensure("/tmp/hakurei.1971", 0711). - Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). - Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute). - Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute). - Ensure("/tmp/hakurei.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute). + Ensure("/tmp/hakurei.0", 0711). + Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime", acl.Execute). + Ensure("/tmp/hakurei.0/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/runtime/1", acl.Read, acl.Write, acl.Execute). + Ensure("/tmp/hakurei.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute). + Ensure("/tmp/hakurei.0/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/1", acl.Read, acl.Write, acl.Execute). Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute). Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute). Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute). Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"). CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256). - Ephemeral(system.Process, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711). - MustProxyDBus("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{ + Ephemeral(system.Process, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1", 0711). + MustProxyDBus("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{ Talk: []string{ "org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.ScreenSaver", "org.freedesktop.secrets", @@ -308,7 +306,7 @@ func TestApp(t *testing.T) { }, Call: map[string]string{}, Broadcast: map[string]string{}, Filter: true, - }, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{ + }, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{ Talk: []string{ "org.bluez", "org.freedesktop.Avahi", @@ -316,8 +314,8 @@ func TestApp(t *testing.T) { }, Filter: true, }). - UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). - UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), + UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). + UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), &container.Params{ Uid: 1971, Gid: 100, @@ -358,15 +356,15 @@ func TestApp(t *testing.T) { Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), container.BindWritable|container.BindEnsure). Remount(m("/dev/"), syscall.MS_RDONLY). Tmpfs(m("/run/user/"), 4096, 0755). - Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable). - Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), container.BindWritable). + Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), container.BindWritable). + Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), container.BindWritable). Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")). Place(m("/etc/group"), []byte("hakurei:x:100:\n")). Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0). Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0). Place(m(hst.Tmp+"/pulse-cookie"), nil). - Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0). - Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). + Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0). + Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0). Remount(m("/"), syscall.MS_RDONLY), SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, HostNet: true, @@ -378,7 +376,8 @@ func TestApp(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Run("finalise", func(t *testing.T) { - sys, params, err := app.FinaliseIParams(t.Context(), tc.os, tc.config, &tc.id) + seal := outcome{syscallDispatcher: tc.k, id: &stringPair[state.ID]{tc.id, tc.id.String()}} + err := seal.finalise(t.Context(), tc.config) if err != nil { if s, ok := container.GetErrorMessage(err); !ok { t.Fatalf("Seal: error = %v", err) @@ -388,14 +387,14 @@ func TestApp(t *testing.T) { } t.Run("sys", func(t *testing.T) { - if !sys.Equal(tc.wantSys) { - t.Errorf("Seal: sys = %#v, want %#v", sys, tc.wantSys) + if !seal.sys.Equal(tc.wantSys) { + t.Errorf("Seal: sys = %#v, want %#v", seal.sys, tc.wantSys) } }) t.Run("params", func(t *testing.T) { - if !reflect.DeepEqual(params, tc.wantParams) { - t.Errorf("seal: params =\n%s\n, want\n%s", mustMarshal(params), mustMarshal(tc.wantParams)) + if !reflect.DeepEqual(seal.container, tc.wantParams) { + t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(seal.container), mustMarshal(tc.wantParams)) } }) }) diff --git a/internal/app/container.go b/internal/app/container.go index 26bda26..d406d21 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -11,7 +11,6 @@ import ( "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/hst" - "hakurei.app/internal/sys" "hakurei.app/system/dbus" ) @@ -20,7 +19,13 @@ const preallocateOpsCount = 1 << 5 // newContainer initialises [container.Params] via [hst.ContainerConfig]. // Note that remaining container setup must be queued by the caller. -func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) { +func newContainer( + k syscallDispatcher, + s *hst.ContainerConfig, + prefix string, + sc *hst.Paths, + uid, gid *int, +) (*container.Params, map[string]string, error) { if s == nil { return nil, nil, newWithMessage("invalid container configuration") } @@ -38,9 +43,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid ForwardCancel: s.WaitDelay >= 0, } - as := &hst.ApplyState{ - AutoEtcPrefix: prefix, - } + as := &hst.ApplyState{AutoEtcPrefix: prefix} { ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)) params.Ops = &ops @@ -65,13 +68,13 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid } if s.MapRealUID { - params.Uid = os.Getuid() + params.Uid = k.getuid() *uid = params.Uid - params.Gid = os.Getgid() + params.Gid = k.getgid() *gid = params.Gid } else { - *uid = container.OverflowUid() - *gid = container.OverflowGid() + *uid = k.overflowUid() + *gid = k.overflowGid() } filesystem := s.Filesystem @@ -107,7 +110,6 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid to warn about issues in custom configuration; it is NOT a security feature and should not be treated as such, ALWAYS be careful with what you bind */ var hidePaths []string - sc := os.Paths() hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String()) _, systemBusAddr := dbus.Address() if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { @@ -124,11 +126,11 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid // get parent dir of socket dir := path.Dir(pair[1]) if dir == "." || dir == container.FHSRoot { - os.Printf("dbus socket %q is in an unusual location", pair[1]) + k.verbosef("dbus socket %q is in an unusual location", pair[1]) } hidePaths = append(hidePaths, dir) } else { - os.Printf("dbus socket %q is not absolute", pair[1]) + k.verbosef("dbus socket %q is not absolute", pair[1]) } } } @@ -136,7 +138,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid } hidePathMatch := make([]bool, len(hidePaths)) for i := range hidePaths { - if err := evalSymlinks(os, &hidePaths[i]); err != nil { + if err := evalSymlinks(k, &hidePaths[i]); err != nil { return nil, nil, err } } @@ -155,7 +157,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid // AutoRootOp is a collection of many BindMountOp internally var autoRootEntries []fs.DirEntry if autoroot != nil { - if d, err := os.ReadDir(autoroot.Source.String()); err != nil { + if d, err := k.readdir(autoroot.Source.String()); err != nil { return nil, nil, err } else { // autoroot counter @@ -191,7 +193,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid } hidePathSourceEval[i] = [2]string{a.String(), a.String()} - if err := evalSymlinks(os, &hidePathSourceEval[i][0]); err != nil { + if err := evalSymlinks(k, &hidePathSourceEval[i][0]); err != nil { return nil, nil, err } } @@ -207,7 +209,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid return nil, nil, err } else if ok { hidePathMatch[i] = true - os.Printf("hiding path %q from %q", hidePaths[i], p[1]) + k.verbosef("hiding path %q from %q", hidePaths[i], p[1]) } } } @@ -238,12 +240,13 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid return params, maps.Clone(s.Env), nil } -func evalSymlinks(os sys.State, v *string) error { - if p, err := os.EvalSymlinks(*v); err != nil { +// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist]. +func evalSymlinks(k syscallDispatcher, v *string) error { + if p, err := k.evalSymlinks(*v); err != nil { if !errors.Is(err, fs.ErrNotExist) { return err } - os.Printf("path %q does not yet exist", *v) + k.verbosef("path %q does not yet exist", *v) } else { *v = p } diff --git a/internal/app/dispatcher.go b/internal/app/dispatcher.go new file mode 100644 index 0000000..deecfa9 --- /dev/null +++ b/internal/app/dispatcher.go @@ -0,0 +1,99 @@ +package app + +import ( + "log" + "os" + "os/exec" + "os/user" + "path/filepath" + + "hakurei.app/container" + "hakurei.app/internal" + "hakurei.app/internal/hlog" +) + +// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. +type syscallDispatcher interface { + // new starts a goroutine with a new instance of syscallDispatcher. + // A syscallDispatcher must never be used in any goroutine other than the one owning it, + // just synchronising access is not enough, as this is for test instrumentation. + new(f func(k syscallDispatcher)) + + // getuid provides [os.Getuid]. + getuid() int + // getgid provides [os.Getgid]. + getgid() int + // lookupEnv provides [os.LookupEnv]. + lookupEnv(key string) (string, bool) + // stat provides [os.Stat]. + stat(name string) (os.FileInfo, error) + // readdir provides [os.ReadDir]. + readdir(name string) ([]os.DirEntry, error) + // tempdir provides [os.TempDir]. + tempdir() string + + // evalSymlinks provides [filepath.EvalSymlinks]. + evalSymlinks(path string) (string, error) + + // lookPath provides exec.LookPath. + lookPath(file string) (string, error) + + // lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct. + lookupGroupId(name string) (string, error) + + // cmdOutput provides the Output method of [exec.Cmd]. + cmdOutput(cmd *exec.Cmd) ([]byte, error) + + // overflowUid provides [container.OverflowUid]. + overflowUid() int + // overflowGid provides [container.OverflowGid]. + overflowGid() int + + // mustHsuPath provides [internal.MustHsuPath]. + mustHsuPath() string + + // fatalf provides [log.Fatalf]. + fatalf(format string, v ...any) + + isVerbose() bool + verbose(v ...any) + verbosef(format string, v ...any) +} + +// direct implements syscallDispatcher on the current kernel. +type direct struct{} + +func (k direct) new(f func(k syscallDispatcher)) { go f(k) } + +func (direct) getuid() int { return os.Getuid() } +func (direct) getgid() int { return os.Getgid() } +func (direct) lookupEnv(key string) (string, bool) { return os.LookupEnv(key) } +func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) } +func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } +func (direct) tempdir() string { return os.TempDir() } + +func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) } + +func (direct) lookPath(file string) (string, error) { return exec.LookPath(file) } + +func (direct) lookupGroupId(name string) (gid string, err error) { + var group *user.Group + group, err = user.LookupGroup(name) + if group != nil { + gid = group.Gid + } + return +} + +func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() } + +func (direct) overflowUid() int { return container.OverflowUid() } +func (direct) overflowGid() int { return container.OverflowGid() } + +func (direct) mustHsuPath() string { return internal.MustHsuPath() } + +func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) } + +func (k direct) isVerbose() bool { return hlog.Load() } +func (direct) verbose(v ...any) { hlog.Verbose(v...) } +func (direct) verbosef(format string, v ...any) { hlog.Verbosef(format, v...) } diff --git a/internal/app/export_test.go b/internal/app/export_test.go deleted file mode 100644 index 1bfba48..0000000 --- a/internal/app/export_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package app - -import ( - "context" - - "hakurei.app/container" - "hakurei.app/hst" - "hakurei.app/internal/app/state" - "hakurei.app/internal/sys" - "hakurei.app/system" -) - -func FinaliseIParams(ctx context.Context, k sys.State, config *hst.Config, id *state.ID) (*system.I, *container.Params, error) { - seal := outcome{id: &stringPair[state.ID]{*id, id.String()}} - return seal.sys, seal.container, seal.finalise(ctx, k, config) -} diff --git a/internal/app/seal.go b/internal/app/finalise.go similarity index 73% rename from internal/app/seal.go rename to internal/app/finalise.go index c7dd9d6..2a0e5e6 100644 --- a/internal/app/seal.go +++ b/internal/app/finalise.go @@ -21,7 +21,6 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app/state" "hakurei.app/internal/hlog" - "hakurei.app/internal/sys" "hakurei.app/system" "hakurei.app/system/acl" "hakurei.app/system/dbus" @@ -54,8 +53,9 @@ type outcome struct { container *container.Params env map[string]string sync *os.File + active atomic.Bool - f atomic.Bool + syscallDispatcher } // shareHost holds optional share directory state that must not be accessed directly @@ -120,7 +120,7 @@ type hsuUser struct { username string } -func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Config) error { +func (k *outcome) finalise(ctx context.Context, config *hst.Config) error { const ( home = "HOME" shell = "SHELL" @@ -144,14 +144,13 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf // unreachable panic("invalid call to finalise") } - if seal.ctx != nil { + if k.ctx != nil { // unreachable panic("attempting to finalise twice") } - seal.ctx = ctx + k.ctx = ctx if config == nil { - // unreachable return newWithMessage("invalid configuration") } if config.Home == nil { @@ -164,7 +163,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf if err := gob.NewEncoder(ct).Encode(config); err != nil { return &hst.AppError{Step: "encode initial config", Err: err} } - seal.ct = ct + k.ct = ct } // allowed identity range 0 to 9999, this is checked again in hsu @@ -172,22 +171,23 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity)) } - seal.user = hsuUser{ + k.user = hsuUser{ identity: newInt(config.Identity), home: config.Home, username: config.Username, } - if seal.user.username == "" { - seal.user.username = "chronos" - } else if !isValidUsername(seal.user.username) { - return newWithMessage(fmt.Sprintf("invalid user name %q", seal.user.username)) + hsu := Hsu{k: k} + if k.user.username == "" { + k.user.username = "chronos" + } else if !isValidUsername(k.user.username) { + return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username)) } - seal.user.uid = newInt(sys.MustUid(k, seal.user.identity.unwrap())) + k.user.uid = newInt(HsuUid(hsu.MustID(), k.user.identity.unwrap())) - seal.user.supp = make([]string, len(config.Groups)) + k.user.supp = make([]string, len(config.Groups)) for i, name := range config.Groups { - if g, err := k.LookupGroup(name); err != nil { + if gid, err := k.lookupGroupId(name); err != nil { var unknownGroupError user.UnknownGroupError if errors.As(err, &unknownGroupError) { return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError) @@ -195,7 +195,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf return &hst.AppError{Step: "look up group by name", Err: err} } } else { - seal.user.supp[i] = g.Gid + k.user.supp[i] = gid } } @@ -205,7 +205,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf if config.Shell == nil { config.Shell = container.AbsFHSRoot.Append("bin", "sh") - s, _ := k.LookupEnv(shell) + s, _ := k.lookupEnv(shell) if a, err := container.NewAbs(s); err == nil { config.Shell = a } @@ -214,7 +214,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf // hsu clears the environment so resolve paths early if config.Path == nil { if len(config.Args) > 0 { - if p, err := k.LookPath(config.Args[0]); err != nil { + if p, err := k.lookPath(config.Args[0]); err != nil { return &hst.AppError{Step: "look up executable file", Err: err} } else if config.Path, err = container.NewAbs(p); err != nil { return newWithMessageError(err.Error(), err) @@ -250,7 +250,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf // hide nscd from container if present nscd := container.AbsFHSVar.Append("run/nscd") - if _, err := k.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { + if _, err := k.stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}}) } @@ -274,86 +274,89 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf return newWithMessage("invalid program path") } + // TODO(ophestra): revert this after params to shim + share := &shareHost{seal: k} + copyPaths(k.syscallDispatcher, &share.sc, hsu.MustID()) + var mapuid, mapgid *stringPair[int] { var uid, gid int var err error - seal.container, seal.env, err = newContainer(config.Container, k, seal.id.String(), &uid, &gid) - seal.waitDelay = config.Container.WaitDelay + k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid) + k.waitDelay = config.Container.WaitDelay if err != nil { return &hst.AppError{Step: "initialise container configuration", Err: err} } if len(config.Args) == 0 { config.Args = []string{config.Path.String()} } - seal.container.Path = config.Path - seal.container.Args = config.Args + k.container.Path = config.Path + k.container.Args = config.Args mapuid = newInt(uid) mapgid = newInt(gid) - if seal.env == nil { - seal.env = make(map[string]string, 1<<6) + if k.env == nil { + k.env = make(map[string]string, 1<<6) } } // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String()) - seal.env[xdgRuntimeDir] = innerRuntimeDir.String() - seal.env[xdgSessionClass] = "user" - seal.env[xdgSessionType] = "tty" + k.env[xdgRuntimeDir] = innerRuntimeDir.String() + k.env[xdgSessionClass] = "user" + k.env[xdgSessionType] = "tty" - share := &shareHost{seal: seal, sc: k.Paths()} - seal.runDirPath = share.sc.RunDirPath - seal.sys = system.New(seal.ctx, seal.user.uid.unwrap()) - seal.sys.Ensure(share.sc.SharePath.String(), 0711) + k.runDirPath = share.sc.RunDirPath + k.sys = system.New(k.ctx, k.user.uid.unwrap()) + k.sys.Ensure(share.sc.SharePath.String(), 0711) { runtimeDir := share.sc.SharePath.Append("runtime") - seal.sys.Ensure(runtimeDir.String(), 0700) - seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) - runtimeDirInst := runtimeDir.Append(seal.user.identity.String()) - seal.sys.Ensure(runtimeDirInst.String(), 0700) - seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) - seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) - seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) + k.sys.Ensure(runtimeDir.String(), 0700) + k.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) + runtimeDirInst := runtimeDir.Append(k.user.identity.String()) + k.sys.Ensure(runtimeDirInst.String(), 0700) + k.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) + k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) + k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) } { tmpdir := share.sc.SharePath.Append("tmpdir") - seal.sys.Ensure(tmpdir.String(), 0700) - seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) - tmpdirInst := tmpdir.Append(seal.user.identity.String()) - seal.sys.Ensure(tmpdirInst.String(), 01700) - seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) + k.sys.Ensure(tmpdir.String(), 0700) + k.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) + tmpdirInst := tmpdir.Append(k.user.identity.String()) + k.sys.Ensure(tmpdirInst.String(), 01700) + k.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) // mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp - seal.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable) + k.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable) } { username := "chronos" - if seal.user.username != "" { - username = seal.user.username + if k.user.username != "" { + username = k.user.username } - seal.container.Dir = seal.user.home - seal.env["HOME"] = seal.user.home.String() - seal.env["USER"] = username - seal.env[shell] = config.Shell.String() + k.container.Dir = k.user.home + k.env["HOME"] = k.user.home.String() + k.env["USER"] = username + k.env[shell] = config.Shell.String() - seal.container.Place(container.AbsFHSEtc.Append("passwd"), - []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n")) - seal.container.Place(container.AbsFHSEtc.Append("group"), + k.container.Place(container.AbsFHSEtc.Append("passwd"), + []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n")) + k.container.Place(container.AbsFHSEtc.Append("group"), []byte("hakurei:x:"+mapgid.String()+":\n")) } // pass TERM for proper terminal I/O in initial process - if t, ok := k.LookupEnv(term); ok { - seal.env[term] = t + if t, ok := k.lookupEnv(term); ok { + k.env[term] = t } if config.Enablements.Unwrap()&system.EWayland != 0 { // outer wayland socket (usually `/run/user/%d/wayland-%d`) var socketPath *container.Absolute - if name, ok := k.LookupEnv(wayland.WaylandDisplay); !ok { + if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok { hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) } else if a, err := container.NewAbs(name); err != nil { @@ -363,28 +366,28 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf } innerPath := innerRuntimeDir.Append(wayland.FallbackName) - seal.env[wayland.WaylandDisplay] = wayland.FallbackName + k.env[wayland.WaylandDisplay] = wayland.FallbackName if !config.DirectWayland { // set up security-context-v1 appID := config.ID if appID == "" { // use instance ID in case app id is not set - appID = "app.hakurei." + seal.id.String() + appID = "app.hakurei." + k.id.String() } // downstream socket paths outerPath := share.instance().Append("wayland") - seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String()) - seal.container.Bind(outerPath, innerPath, 0) + k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String()) + k.container.Bind(outerPath, innerPath, 0) } else { // bind mount wayland socket (insecure) hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") share.ensureRuntimeDir() - seal.container.Bind(socketPath, innerPath, 0) - seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) + k.container.Bind(socketPath, innerPath, 0) + k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) } } if config.Enablements.Unwrap()&system.EX11 != 0 { - if d, ok := k.LookupEnv(display); !ok { + if d, ok := k.lookupEnv(display); !ok { return newWithMessage("DISPLAY is not set") } else { socketDir := container.AbsFHSTmp.Append(".X11-unix") @@ -402,21 +405,21 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf } } if socketPath != nil { - if _, err := k.Stat(socketPath.String()); err != nil { + if _, err := k.stat(socketPath.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} } } else { - seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute) + k.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute) if !config.Container.HostAbstract { d = "unix:" + socketPath.String() } } } - seal.sys.ChangeHosts("#" + seal.user.uid.String()) - seal.env[display] = d - seal.container.Bind(socketDir, socketDir, 0) + k.sys.ChangeHosts("#" + k.user.uid.String()) + k.env[display] = d + k.container.Bind(socketDir, socketDir, 0) } } @@ -426,14 +429,14 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf // PulseAudio socket (usually `/run/user/%d/pulse/native`) pulseSocket := pulseRuntimeDir.Append("native") - if _, err := k.Stat(pulseRuntimeDir.String()); err != nil { + if _, err := k.stat(pulseRuntimeDir.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} } return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) } - if s, err := k.Stat(pulseSocket.String()); err != nil { + if s, err := k.stat(pulseSocket.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} } @@ -447,9 +450,9 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf // hard link pulse socket into target-executable share innerPulseRuntimeDir := share.runtime().Append("pulse") innerPulseSocket := innerRuntimeDir.Append("pulse", "native") - seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) - seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) - seal.env[pulseServer] = "unix:" + innerPulseSocket.String() + k.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) + k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) + k.env[pulseServer] = "unix:" + innerPulseSocket.String() // publish current user's pulse cookie for target user var paCookiePath *container.Absolute @@ -457,7 +460,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf const paLocateStep = "locate PulseAudio cookie" // from environment - if p, ok := k.LookupEnv(pulseCookie); ok { + if p, ok := k.lookupEnv(pulseCookie); ok { if a, err := container.NewAbs(p); err != nil { return &hst.AppError{Step: paLocateStep, Err: err} } else { @@ -468,14 +471,14 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf } // $HOME/.pulse-cookie - if p, ok := k.LookupEnv(home); ok { + if p, ok := k.lookupEnv(home); ok { if a, err := container.NewAbs(p); err != nil { return &hst.AppError{Step: paLocateStep, Err: err} } else { paCookiePath = a.Append(".pulse-cookie") } - if s, err := k.Stat(paCookiePath.String()); err != nil { + if s, err := k.stat(paCookiePath.String()); err != nil { paCookiePath = nil if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: "access PulseAudio cookie", Err: err} @@ -489,13 +492,13 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf } // $XDG_CONFIG_HOME/pulse/cookie - if p, ok := k.LookupEnv(xdgConfigHome); ok { + if p, ok := k.lookupEnv(xdgConfigHome); ok { if a, err := container.NewAbs(p); err != nil { return &hst.AppError{Step: paLocateStep, Err: err} } else { paCookiePath = a.Append("pulse", "cookie") } - if s, err := k.Stat(paCookiePath.String()); err != nil { + if s, err := k.stat(paCookiePath.String()); err != nil { paCookiePath = nil if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: "access PulseAudio cookie", Err: err} @@ -512,10 +515,10 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf if paCookiePath != nil { innerDst := hst.AbsTmp.Append("/pulse-cookie") - seal.env[pulseCookie] = innerDst.String() + k.env[pulseCookie] = innerDst.String() var payload *[]byte - seal.container.PlaceP(innerDst, &payload) - seal.sys.CopyFile(payload, paCookiePath.String(), 256, 256) + k.container.PlaceP(innerDst, &payload) + k.sys.CopyFile(payload, paCookiePath.String(), 256, 256) } else { hlog.Verbose("cannot locate PulseAudio cookie (tried " + "$PULSE_COOKIE, " + @@ -534,30 +537,30 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket") // configure dbus proxy - if f, err := seal.sys.ProxyDBus( + if f, err := k.sys.ProxyDBus( config.SessionBus, config.SystemBus, sessionPath.String(), systemPath.String(), ); err != nil { return err } else { - seal.dbusMsg = f + k.dbusMsg = f } // share proxy sockets sessionInner := innerRuntimeDir.Append("bus") - seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() - seal.container.Bind(sessionPath, sessionInner, 0) - seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) + k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() + k.container.Bind(sessionPath, sessionInner, 0) + k.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) if config.SystemBus != nil { systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket") - seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() - seal.container.Bind(systemPath, systemInner, 0) - seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) + k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() + k.container.Bind(systemPath, systemInner, 0) + k.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) } } // mount root read-only as the final setup Op - seal.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY) + k.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY) // append ExtraPerms last for _, p := range config.ExtraPerms { @@ -566,7 +569,7 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf } if p.Ensure { - seal.sys.Ensure(p.Path.String(), 0700) + k.sys.Ensure(p.Path.String(), 0700) } perms := make(acl.Perms, 0, 3) @@ -579,23 +582,23 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf if p.Execute { perms = append(perms, acl.Execute) } - seal.sys.UpdatePermType(system.User, p.Path.String(), perms...) + k.sys.UpdatePermType(system.User, p.Path.String(), perms...) } // flatten and sort env for deterministic behaviour - seal.container.Env = make([]string, 0, len(seal.env)) - for k, v := range seal.env { - if strings.IndexByte(k, '=') != -1 { + k.container.Env = make([]string, 0, len(k.env)) + for key, value := range k.env { + if strings.IndexByte(key, '=') != -1 { return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL, - Msg: fmt.Sprintf("invalid environment variable %s", k)} + Msg: fmt.Sprintf("invalid environment variable %s", key)} } - seal.container.Env = append(seal.container.Env, k+"="+v) + k.container.Env = append(k.container.Env, key+"="+value) } - slices.Sort(seal.container.Env) + slices.Sort(k.container.Env) if hlog.Load() { hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d", - seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops)) + k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops)) } return nil diff --git a/internal/sys/hsu.go b/internal/app/hsu.go similarity index 70% rename from internal/sys/hsu.go rename to internal/app/hsu.go index 5ae1204..0078ef0 100644 --- a/internal/sys/hsu.go +++ b/internal/app/hsu.go @@ -1,4 +1,4 @@ -package sys +package app import ( "errors" @@ -11,7 +11,6 @@ import ( "hakurei.app/container" "hakurei.app/hst" - "hakurei.app/internal" "hakurei.app/internal/hlog" ) @@ -20,15 +19,28 @@ type Hsu struct { idOnce sync.Once idErr error id int + + kOnce sync.Once + k syscallDispatcher } var ErrHsuAccess = errors.New("current user is not in the hsurc file") +// ensureDispatcher ensures Hsu.k is not nil. +func (h *Hsu) ensureDispatcher() { + h.kOnce.Do(func() { + if h.k == nil { + h.k = direct{} + } + }) +} + // ID returns the current user hsurc identifier. ErrHsuAccess is returned if the current user is not in hsurc. func (h *Hsu) ID() (int, error) { + h.ensureDispatcher() h.idOnce.Do(func() { h.id = -1 - hsuPath := internal.MustHsuPath() + hsuPath := h.k.mustHsuPath() cmd := exec.Command(hsuPath) cmd.Path = hsuPath @@ -41,7 +53,7 @@ func (h *Hsu) ID() (int, error) { ) const step = "obtain uid from hsu" - if p, h.idErr = cmd.Output(); h.idErr == nil { + if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil { h.id, h.idErr = strconv.Atoi(string(p)) if h.idErr != nil { h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"} @@ -58,22 +70,14 @@ func (h *Hsu) ID() (int, error) { return h.id, h.idErr } -func (h *Hsu) Uid(identity int) (int, error) { +// MustID calls [Hsu.ID] and terminates on error. +func (h *Hsu) MustID() int { id, err := h.ID() if err == nil { - return 1000000 + id*10000 + identity, nil - } - return id, err -} - -// MustUid calls [State.Uid] and terminates on error. -func MustUid(s State, identity int) int { - uid, err := s.Uid(identity) - if err == nil { - return uid + return id } - const fallback = "cannot obtain uid from setuid wrapper:" + const fallback = "cannot retrieve user id from setuid wrapper:" if errors.Is(err, ErrHsuAccess) { hlog.Verbose("*"+fallback, err) os.Exit(1) @@ -86,3 +90,7 @@ func MustUid(s State, identity int) int { return -0xdeadbeef } } + +// HsuUid returns target uid for the stable hsu uid format. +// No bounds check is performed, a value retrieved from hsu is expected. +func HsuUid(id, identity int) int { return 1000000 + id*10000 + identity } diff --git a/internal/app/paths.go b/internal/app/paths.go new file mode 100644 index 0000000..3316a22 --- /dev/null +++ b/internal/app/paths.go @@ -0,0 +1,36 @@ +package app + +import ( + "strconv" + + "hakurei.app/container" + "hakurei.app/hst" +) + +// CopyPaths populates a [hst.Paths] struct. +func CopyPaths(v *hst.Paths, userid int) { copyPaths(direct{}, v, userid) } + +// copyPaths populates a [hst.Paths] struct. +func copyPaths(k syscallDispatcher, v *hst.Paths, userid int) { + const xdgRuntimeDir = "XDG_RUNTIME_DIR" + + if tempDir, err := container.NewAbs(k.tempdir()); err != nil { + k.fatalf("invalid TMPDIR: %v", err) + } else { + v.TempDir = tempDir + } + + v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid)) + k.verbosef("process share directory at %q", v.SharePath) + + r, _ := k.lookupEnv(xdgRuntimeDir) + if a, err := container.NewAbs(r); err != nil { + // fall back to path in share since hakurei has no hard XDG dependency + v.RunDirPath = v.SharePath.Append("run") + v.RuntimePath = v.RunDirPath.Append("compat") + } else { + v.RuntimePath = a + v.RunDirPath = v.RuntimePath.Append("hakurei") + } + k.verbosef("runtime directory at %q", v.RunDirPath) +} diff --git a/internal/app/process.go b/internal/app/process.go index 368b655..8ac0585 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -33,12 +33,12 @@ type mainState struct { // Time is nil if no process was ever created. Time *time.Time - seal *outcome store state.Store cancel context.CancelFunc cmd *exec.Cmd cmdWait chan error + k *outcome uintptr } @@ -79,13 +79,13 @@ func (ms mainState) beforeExit(isFault bool) { waitDone := make(chan struct{}) // TODO(ophestra): enforce this limit early so it does not have to be done twice shimTimeoutCompensated := shimWaitTimeout - if ms.seal.waitDelay > MaxShimWaitDelay { + if ms.k.waitDelay > MaxShimWaitDelay { shimTimeoutCompensated += MaxShimWaitDelay } else { - shimTimeoutCompensated += ms.seal.waitDelay + shimTimeoutCompensated += ms.k.waitDelay } // this ties waitDone to ctx with the additional compensated timeout duration - go func() { <-ms.seal.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }() + go func() { <-ms.k.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }() select { case err := <-ms.cmdWait: @@ -128,20 +128,20 @@ func (ms mainState) beforeExit(isFault bool) { } hlog.Resume() - if ms.seal.sync != nil { - if err := ms.seal.sync.Close(); err != nil { + if ms.k.sync != nil { + if err := ms.k.sync.Close(); err != nil { perror(err, "close wayland security context") } } - if ms.seal.dbusMsg != nil { - ms.seal.dbusMsg() + if ms.k.dbusMsg != nil { + ms.k.dbusMsg() } } if ms.uintptr&mainNeedsRevert != 0 { - if ok, err := ms.store.Do(ms.seal.user.identity.unwrap(), func(c state.Cursor) { + if ok, err := ms.store.Do(ms.k.user.identity.unwrap(), func(c state.Cursor) { if ms.uintptr&mainNeedsDestroy != 0 { - if err := c.Destroy(ms.seal.id.unwrap()); err != nil { + if err := c.Destroy(ms.k.id.unwrap()); err != nil { perror(err, "destroy state entry") } } @@ -151,7 +151,7 @@ func (ms mainState) beforeExit(isFault bool) { // it is impossible to continue from this point; // revert per-process state here to limit damage ec := system.Process - if revertErr := ms.seal.sys.Revert((*system.Criteria)(&ec)); revertErr != nil { + if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil { var joinError interface { Unwrap() []error error @@ -189,7 +189,7 @@ func (ms mainState) beforeExit(isFault bool) { } } - if err = ms.seal.sys.Revert((*system.Criteria)(&ec)); err != nil { + if err = ms.k.sys.Revert((*system.Criteria)(&ec)); err != nil { perror(err, "revert system setup") } } @@ -219,8 +219,8 @@ func (ms mainState) fatal(fallback string, ferr error) { } // main carries out outcome and terminates. main does not return. -func (seal *outcome) main() { - if !seal.f.CompareAndSwap(false, true) { +func (k *outcome) main() { + if !k.active.CompareAndSwap(false, true) { panic("outcome: attempted to run twice") } @@ -228,15 +228,15 @@ func (seal *outcome) main() { hsuPath := internal.MustHsuPath() // ms.beforeExit required beyond this point - ms := &mainState{seal: seal} + ms := &mainState{k: k} - if err := seal.sys.Commit(); err != nil { + if err := k.sys.Commit(); err != nil { ms.fatal("cannot commit system setup:", err) } ms.uintptr |= mainNeedsRevert - ms.store = state.NewMulti(seal.runDirPath.String()) + ms.store = state.NewMulti(k.runDirPath.String()) - ctx, cancel := context.WithCancel(seal.ctx) + ctx, cancel := context.WithCancel(k.ctx) defer cancel() ms.cancel = cancel @@ -255,14 +255,14 @@ func (seal *outcome) main() { // passed through to shim by hsu shimEnv + "=" + strconv.Itoa(fd), // interpreted by hsu - "HAKUREI_IDENTITY=" + seal.user.identity.String(), + "HAKUREI_IDENTITY=" + k.user.identity.String(), } } - if len(seal.user.supp) > 0 { - hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp) + if len(k.user.supp) > 0 { + hlog.Verbosef("attaching supplementary group ids %s", k.user.supp) // interpreted by hsu - ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " ")) + ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.user.supp, " ")) } hlog.Verbosef("setuid helper at %s", hsuPath) @@ -284,8 +284,8 @@ func (seal *outcome) main() { go func() { setupErr <- e.Encode(&shimParams{ os.Getpid(), - seal.waitDelay, - seal.container, + k.waitDelay, + k.container, hlog.Load(), }) }() @@ -302,12 +302,12 @@ func (seal *outcome) main() { } // shim accepted setup payload, create process state - if ok, err := ms.store.Do(seal.user.identity.unwrap(), func(c state.Cursor) { + if ok, err := ms.store.Do(k.user.identity.unwrap(), func(c state.Cursor) { if err := c.Save(&state.State{ - ID: seal.id.unwrap(), + ID: k.id.unwrap(), PID: ms.cmd.Process.Pid, Time: *ms.Time, - }, seal.ct); err != nil { + }, k.ct); err != nil { ms.fatal("cannot save state entry:", err) } }); err != nil { diff --git a/internal/sys/interface.go b/internal/sys/interface.go deleted file mode 100644 index f2465e6..0000000 --- a/internal/sys/interface.go +++ /dev/null @@ -1,76 +0,0 @@ -// Package sys wraps OS interaction library functions. -package sys - -import ( - "io/fs" - "log" - "os/user" - "strconv" - - "hakurei.app/container" - "hakurei.app/hst" - "hakurei.app/internal/hlog" -) - -// State provides safe interaction with operating system state. -type State interface { - // Getuid provides [os.Getuid]. - Getuid() int - // Getgid provides [os.Getgid]. - Getgid() int - // LookupEnv provides [os.LookupEnv]. - LookupEnv(key string) (string, bool) - // TempDir provides [os.TempDir]. - TempDir() string - // LookPath provides exec.LookPath. - LookPath(file string) (string, error) - // MustExecutable provides [container.MustExecutable]. - MustExecutable() string - // LookupGroup provides [user.LookupGroup]. - LookupGroup(name string) (*user.Group, error) - // ReadDir provides [os.ReadDir]. - ReadDir(name string) ([]fs.DirEntry, error) - // Stat provides [os.Stat]. - Stat(name string) (fs.FileInfo, error) - // Open provides [os.Open]. - Open(name string) (fs.File, error) - // EvalSymlinks provides filepath.EvalSymlinks. - EvalSymlinks(path string) (string, error) - // Exit provides [os.Exit]. - Exit(code int) - - Println(v ...any) - Printf(format string, v ...any) - - // Paths returns a populated [hst.Paths] struct. - Paths() hst.Paths - // Uid invokes hsu and returns target uid. - // Any errors returned by Uid is already wrapped [hlog.BaseError]. - Uid(identity int) (int, error) -} - -// MustGetUserID obtains user id from hsu by querying uid of identity 0. -func MustGetUserID(os State) int { return (MustUid(os, 0) / 10000) - 100 } - -// CopyPaths is a generic implementation of [hst.Paths]. -func CopyPaths(os State, v *hst.Paths, userid int) { - if tempDir, err := container.NewAbs(os.TempDir()); err != nil { - log.Fatalf("invalid TMPDIR: %v", err) - } else { - v.TempDir = tempDir - } - - v.SharePath = v.TempDir.Append("hakurei." + strconv.Itoa(userid)) - hlog.Verbosef("process share directory at %q", v.SharePath) - - r, _ := os.LookupEnv(xdgRuntimeDir) - if a, err := container.NewAbs(r); err != nil { - // fall back to path in share since hakurei has no hard XDG dependency - v.RunDirPath = v.SharePath.Append("run") - v.RuntimePath = v.RunDirPath.Append("compat") - } else { - v.RuntimePath = a - v.RunDirPath = v.RuntimePath.Append("hakurei") - } - hlog.Verbosef("runtime directory at %q", v.RunDirPath) -} diff --git a/internal/sys/std.go b/internal/sys/std.go deleted file mode 100644 index 11334c7..0000000 --- a/internal/sys/std.go +++ /dev/null @@ -1,44 +0,0 @@ -package sys - -import ( - "io/fs" - "os" - "os/exec" - "os/user" - "path/filepath" - "sync" - - "hakurei.app/container" - "hakurei.app/hst" - "hakurei.app/internal" - "hakurei.app/internal/hlog" -) - -// Std implements System using the standard library. -type Std struct { - paths hst.Paths - pathsOnce sync.Once - Hsu -} - -func (s *Std) Getuid() int { return os.Getuid() } -func (s *Std) Getgid() int { return os.Getgid() } -func (s *Std) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } -func (s *Std) TempDir() string { return os.TempDir() } -func (s *Std) LookPath(file string) (string, error) { return exec.LookPath(file) } -func (s *Std) MustExecutable() string { return container.MustExecutable() } -func (s *Std) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } -func (s *Std) ReadDir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) } -func (s *Std) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } -func (s *Std) Open(name string) (fs.File, error) { return os.Open(name) } -func (s *Std) EvalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) } -func (s *Std) Exit(code int) { internal.Exit(code) } -func (s *Std) Println(v ...any) { hlog.Verbose(v...) } -func (s *Std) Printf(format string, v ...any) { hlog.Verbosef(format, v...) } - -const xdgRuntimeDir = "XDG_RUNTIME_DIR" - -func (s *Std) Paths() hst.Paths { - s.pathsOnce.Do(func() { CopyPaths(s, &s.paths, MustGetUserID(s)) }) - return s.paths -} diff --git a/test/test.py b/test/test.py index ae4dfda..a1e6b0f 100644 --- a/test/test.py +++ b/test/test.py @@ -100,7 +100,7 @@ print(machine.fail("sudo -u alice -i hsu")) # Verify hsu fault behaviour: if denyOutput != "hsu: uid 1001 is not in the hsurc file\n": raise Exception(f"unexpected deny output:\n{denyOutput}") -if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot obtain uid from setuid wrapper: current user is not in the hsurc file\n": +if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot retrieve user id from setuid wrapper: current user is not in the hsurc file\n": raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}") # Verify timeout behaviour: