internal: remove sys package
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m13s
Test / Hakurei (push) Successful in 3m14s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m39s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m19s

This package is replaced by container/stub. Remove and replace it with unexported implementation for the upcoming test suite rewrite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-09-25 13:46:21 +09:00
parent 6e3f34f2ec
commit ae2df2c450
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
17 changed files with 516 additions and 442 deletions

View File

@ -18,7 +18,6 @@ import (
"hakurei.app/internal/app" "hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -43,7 +42,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
config := tryPath(args[0]) config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...) config.Args = append(config.Args, args[1:]...)
app.Main(ctx, std, config) app.Main(ctx, config)
panic("unreachable") panic("unreachable")
}) })
@ -79,7 +78,7 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
passwd *user.User passwd *user.User
passwdOnce sync.Once passwdOnce sync.Once
passwdFunc = func() { 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 { if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us) hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{ 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") panic("unreachable")
}). }).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -219,7 +218,9 @@ func buildCommand(ctx context.Context, out io.Writer) command.Command {
{ {
var flagShort bool var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error { 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 return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
} }

View File

@ -15,7 +15,6 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
) )
var ( var (
@ -27,8 +26,6 @@ var (
func init() { hlog.Prepare("hakurei") } func init() { hlog.Prepare("hakurei") }
var std sys.State = new(sys.Std)
func main() { func main() {
// early init path, skips root check and duplicate PR_SET_DUMPABLE // early init path, skips root check and duplicate PR_SET_DUMPABLE
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput) container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)

View File

@ -11,6 +11,7 @@ import (
"syscall" "syscall"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
@ -87,7 +88,9 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
if likePrefix && len(name) >= 8 { if likePrefix && len(name) >= 8 {
hlog.Verbose("argument looks like prefix") 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 { if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err) log.Printf("cannot join store: %v", err)
// drop to fetch from file // drop to fetch from file

View File

@ -12,8 +12,8 @@ import (
"time" "time"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -21,10 +21,8 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
info := &hst.Info{ info := &hst.Info{User: new(app.Hsu).MustID()}
Paths: std.Paths(), app.CopyPaths(&info.Paths, info.User)
User: sys.MustGetUserID(std),
}
if flagJSON { if flagJSON {
printJSON(output, short, info) printJSON(output, short, info)

View File

@ -8,19 +8,17 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
) )
// Main runs an app according to [hst.Config] and terminates. Main does not return. // 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 var id state.ID
if err := state.NewAppID(&id); err != nil { if err := state.NewAppID(&id); err != nil {
log.Fatal(err) log.Fatal(err)
} }
var seal outcome seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}}
seal.id = &stringPair[state.ID]{id, id.String()} if err := seal.finalise(ctx, config); err != nil {
if err := seal.finalise(ctx, k, config); err != nil {
printMessageError("cannot seal app:", err) printMessageError("cannot seal app:", err)
os.Exit(1) os.Exit(1)
} }

View File

@ -1,34 +1,24 @@
package app_test package app
import ( import (
"fmt" "fmt"
"io/fs" "io/fs"
"log" "log"
"os/exec"
"os/user" "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 { type stubNixOS struct {
lookPathErr map[string]error lookPathErr map[string]error
usernameErr map[string]error usernameErr map[string]error
} }
func (s *stubNixOS) Getuid() int { return 1971 } func (k *stubNixOS) new(func(k syscallDispatcher)) { panic("not implemented") }
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 (s *stubNixOS) Println(v ...any) { log.Println(v...) } func (k *stubNixOS) getuid() int { return 1971 }
func (s *stubNixOS) Printf(format string, v ...any) { log.Printf(format, v...) } 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 { switch key {
case "SHELL": case "SHELL":
return "/run/current-system/sw/bin/zsh", true return "/run/current-system/sw/bin/zsh", true
@ -40,6 +30,8 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
return "", false return "", false
case "HOME": case "HOME":
return "/home/ophestra", true return "/home/ophestra", true
case "XDG_RUNTIME_DIR":
return "/run/user/1971", true
case "XDG_CONFIG_HOME": case "XDG_CONFIG_HOME":
return "/home/ophestra/xdg/config", true return "/home/ophestra/xdg/config", true
default: default:
@ -47,61 +39,7 @@ func (s *stubNixOS) LookupEnv(key string) (string, bool) {
} }
} }
func (s *stubNixOS) LookPath(file string) (string, error) { func (k *stubNixOS) stat(name string) (fs.FileInfo, 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) {
switch name { switch name {
case "/var/run/nscd": case "/var/run/nscd":
return nil, nil 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 { 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: 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 { func (k *stubNixOS) tempdir() string { return "/tmp/" }
return hst.Paths{
SharePath: m("/tmp/hakurei.1971"), func (k *stubNixOS) evalSymlinks(path string) (string, error) {
RuntimePath: m("/run/user/1971"), switch path {
RunDirPath: m("/run/user/1971/hakurei"), 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...) }

View File

@ -1,4 +1,4 @@
package app_test package app
import ( import (
"context" "context"
@ -13,9 +13,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/acl" "hakurei.app/system/acl"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
@ -24,7 +22,7 @@ import (
func TestApp(t *testing.T) { func TestApp(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
os sys.State k syscallDispatcher
config *hst.Config config *hst.Config
id state.ID id state.ID
wantSys *system.I wantSys *system.I
@ -40,11 +38,11 @@ func TestApp(t *testing.T) {
0xb9, 0xa6, 0x07, 0xac, 0xb9, 0xa6, 0x07, 0xac,
}, },
system.New(context.TODO(), 1000000). system.New(context.TODO(), 1000000).
Ensure("/tmp/hakurei.1971", 0711). Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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.0/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir/0", acl.Read, acl.Write, acl.Execute),
&container.Params{ &container.Params{
Dir: m("/home/chronos"), Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"), Path: m("/run/current-system/sw/bin/zsh"),
@ -71,8 +69,8 @@ func TestApp(t *testing.T) {
Tmpfs(m("/run/dbus"), 8192, 0755). Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY). Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755). Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/0"), m("/run/user/65534"), container.BindWritable). Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/0"), m("/tmp/"), 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/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")). Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Remount(m("/"), syscall.MS_RDONLY), Remount(m("/"), syscall.MS_RDONLY),
@ -132,19 +130,19 @@ func TestApp(t *testing.T) {
0x9b, 0x64, 0xce, 0x7c, 0x9b, 0x64, 0xce, 0x7c,
}, },
system.New(context.TODO(), 1000009). system.New(context.TODO(), 1000009).
Ensure("/tmp/hakurei.1971", 0711). Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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.0/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/tmpdir", acl.Execute).
Ensure("/tmp/hakurei.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/9", acl.Read, acl.Write, 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.1971/ebf083d1b175911782d413369b64ce7c", 0711). Ephemeral(system.Process, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c", 0711).
Wayland(new(*os.File), "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). 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/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 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). 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"). Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse").
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256). 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{ Talk: []string{
"org.freedesktop.Notifications", "org.freedesktop.Notifications",
"org.freedesktop.FileManager1", "org.freedesktop.FileManager1",
@ -166,7 +164,7 @@ func TestApp(t *testing.T) {
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*", "org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
}, },
Filter: true, Filter: true,
}, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{ }, "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
Talk: []string{ Talk: []string{
"org.bluez", "org.bluez",
"org.freedesktop.Avahi", "org.freedesktop.Avahi",
@ -174,8 +172,8 @@ func TestApp(t *testing.T) {
}, },
Filter: true, Filter: true,
}). }).
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
&container.Params{ &container.Params{
Dir: m("/home/chronos"), Dir: m("/home/chronos"),
Path: m("/run/current-system/sw/bin/zsh"), Path: m("/run/current-system/sw/bin/zsh"),
@ -208,15 +206,15 @@ func TestApp(t *testing.T) {
Tmpfs(m("/run/dbus"), 8192, 0755). Tmpfs(m("/run/dbus"), 8192, 0755).
Remount(m("/dev/"), syscall.MS_RDONLY). Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755). Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/9"), m("/run/user/65534"), container.BindWritable). Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/9"), m("/tmp/"), 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/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")). 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). Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
Place(m(hst.Tmp+"/pulse-cookie"), nil). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Bind(m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0). Bind(m("/tmp/hakurei.0/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/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY), Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
@ -283,19 +281,19 @@ func TestApp(t *testing.T) {
0xb4, 0x6e, 0xb5, 0xc1, 0xb4, 0x6e, 0xb5, 0xc1,
}, },
system.New(context.TODO(), 1000001). system.New(context.TODO(), 1000001).
Ensure("/tmp/hakurei.1971", 0711). Ensure("/tmp/hakurei.0", 0711).
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute). Ensure("/tmp/hakurei.0/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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.0/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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.0/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.0/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/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/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 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). 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). 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"). Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256). CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
Ephemeral(system.Process, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711). Ephemeral(system.Process, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
MustProxyDBus("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{ MustProxyDBus("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
Talk: []string{ Talk: []string{
"org.freedesktop.FileManager1", "org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets", "org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
@ -308,7 +306,7 @@ func TestApp(t *testing.T) {
}, },
Call: map[string]string{}, Broadcast: map[string]string{}, Call: map[string]string{}, Broadcast: map[string]string{},
Filter: true, Filter: true,
}, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{ }, "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
Talk: []string{ Talk: []string{
"org.bluez", "org.bluez",
"org.freedesktop.Avahi", "org.freedesktop.Avahi",
@ -316,8 +314,8 @@ func TestApp(t *testing.T) {
}, },
Filter: true, Filter: true,
}). }).
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write). UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write), UpdatePerm("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
&container.Params{ &container.Params{
Uid: 1971, Uid: 1971,
Gid: 100, 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). 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). Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755). Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.1971/runtime/1"), m("/run/user/1971"), container.BindWritable). Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), container.BindWritable).
Bind(m("/tmp/hakurei.1971/tmpdir/1"), m("/tmp/"), 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/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")). 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/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0). Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
Place(m(hst.Tmp+"/pulse-cookie"), nil). Place(m(hst.Tmp+"/pulse-cookie"), nil).
Bind(m("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0). Bind(m("/tmp/hakurei.0/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/system_bus_socket"), m("/run/dbus/system_bus_socket"), 0).
Remount(m("/"), syscall.MS_RDONLY), Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel, SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
HostNet: true, HostNet: true,
@ -378,7 +376,8 @@ func TestApp(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Run("finalise", 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 err != nil {
if s, ok := container.GetErrorMessage(err); !ok { if s, ok := container.GetErrorMessage(err); !ok {
t.Fatalf("Seal: error = %v", err) t.Fatalf("Seal: error = %v", err)
@ -388,14 +387,14 @@ func TestApp(t *testing.T) {
} }
t.Run("sys", func(t *testing.T) { t.Run("sys", func(t *testing.T) {
if !sys.Equal(tc.wantSys) { if !seal.sys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v", sys, tc.wantSys) t.Errorf("Seal: sys = %#v, want %#v", seal.sys, tc.wantSys)
} }
}) })
t.Run("params", func(t *testing.T) { t.Run("params", func(t *testing.T) {
if !reflect.DeepEqual(params, tc.wantParams) { if !reflect.DeepEqual(seal.container, tc.wantParams) {
t.Errorf("seal: params =\n%s\n, want\n%s", mustMarshal(params), mustMarshal(tc.wantParams)) t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(seal.container), mustMarshal(tc.wantParams))
} }
}) })
}) })

View File

@ -11,7 +11,6 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp" "hakurei.app/container/seccomp"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -20,7 +19,13 @@ const preallocateOpsCount = 1 << 5
// newContainer initialises [container.Params] via [hst.ContainerConfig]. // newContainer initialises [container.Params] via [hst.ContainerConfig].
// Note that remaining container setup must be queued by the caller. // 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 { if s == nil {
return nil, nil, newWithMessage("invalid container configuration") 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, ForwardCancel: s.WaitDelay >= 0,
} }
as := &hst.ApplyState{ as := &hst.ApplyState{AutoEtcPrefix: prefix}
AutoEtcPrefix: prefix,
}
{ {
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)) ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
params.Ops = &ops params.Ops = &ops
@ -65,13 +68,13 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
} }
if s.MapRealUID { if s.MapRealUID {
params.Uid = os.Getuid() params.Uid = k.getuid()
*uid = params.Uid *uid = params.Uid
params.Gid = os.Getgid() params.Gid = k.getgid()
*gid = params.Gid *gid = params.Gid
} else { } else {
*uid = container.OverflowUid() *uid = k.overflowUid()
*gid = container.OverflowGid() *gid = k.overflowGid()
} }
filesystem := s.Filesystem 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 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 */ and should not be treated as such, ALWAYS be careful with what you bind */
var hidePaths []string var hidePaths []string
sc := os.Paths()
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String()) hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
_, systemBusAddr := dbus.Address() _, systemBusAddr := dbus.Address()
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil { 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 // get parent dir of socket
dir := path.Dir(pair[1]) dir := path.Dir(pair[1])
if dir == "." || dir == container.FHSRoot { 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) hidePaths = append(hidePaths, dir)
} else { } 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)) hidePathMatch := make([]bool, len(hidePaths))
for i := range 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 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 // AutoRootOp is a collection of many BindMountOp internally
var autoRootEntries []fs.DirEntry var autoRootEntries []fs.DirEntry
if autoroot != nil { 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 return nil, nil, err
} else { } else {
// autoroot counter // 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()} 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 return nil, nil, err
} }
} }
@ -207,7 +209,7 @@ func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid
return nil, nil, err return nil, nil, err
} else if ok { } else if ok {
hidePathMatch[i] = true 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 return params, maps.Clone(s.Env), nil
} }
func evalSymlinks(os sys.State, v *string) error { // evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
if p, err := os.EvalSymlinks(*v); err != nil { func evalSymlinks(k syscallDispatcher, v *string) error {
if p, err := k.evalSymlinks(*v); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return err return err
} }
os.Printf("path %q does not yet exist", *v) k.verbosef("path %q does not yet exist", *v)
} else { } else {
*v = p *v = p
} }

View File

@ -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...) }

View File

@ -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)
}

View File

@ -21,7 +21,6 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
"hakurei.app/system" "hakurei.app/system"
"hakurei.app/system/acl" "hakurei.app/system/acl"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
@ -54,8 +53,9 @@ type outcome struct {
container *container.Params container *container.Params
env map[string]string env map[string]string
sync *os.File sync *os.File
active atomic.Bool
f atomic.Bool syscallDispatcher
} }
// shareHost holds optional share directory state that must not be accessed directly // shareHost holds optional share directory state that must not be accessed directly
@ -120,7 +120,7 @@ type hsuUser struct {
username string 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 ( const (
home = "HOME" home = "HOME"
shell = "SHELL" shell = "SHELL"
@ -144,14 +144,13 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf
// unreachable // unreachable
panic("invalid call to finalise") panic("invalid call to finalise")
} }
if seal.ctx != nil { if k.ctx != nil {
// unreachable // unreachable
panic("attempting to finalise twice") panic("attempting to finalise twice")
} }
seal.ctx = ctx k.ctx = ctx
if config == nil { if config == nil {
// unreachable
return newWithMessage("invalid configuration") return newWithMessage("invalid configuration")
} }
if config.Home == nil { 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 { if err := gob.NewEncoder(ct).Encode(config); err != nil {
return &hst.AppError{Step: "encode initial config", Err: err} 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 // 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)) return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity))
} }
seal.user = hsuUser{ k.user = hsuUser{
identity: newInt(config.Identity), identity: newInt(config.Identity),
home: config.Home, home: config.Home,
username: config.Username, username: config.Username,
} }
if seal.user.username == "" { hsu := Hsu{k: k}
seal.user.username = "chronos" if k.user.username == "" {
} else if !isValidUsername(seal.user.username) { k.user.username = "chronos"
return newWithMessage(fmt.Sprintf("invalid user name %q", seal.user.username)) } 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 { 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 var unknownGroupError user.UnknownGroupError
if errors.As(err, &unknownGroupError) { if errors.As(err, &unknownGroupError) {
return newWithMessageError(fmt.Sprintf("unknown group %q", name), 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} return &hst.AppError{Step: "look up group by name", Err: err}
} }
} else { } 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 { if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh") config.Shell = container.AbsFHSRoot.Append("bin", "sh")
s, _ := k.LookupEnv(shell) s, _ := k.lookupEnv(shell)
if a, err := container.NewAbs(s); err == nil { if a, err := container.NewAbs(s); err == nil {
config.Shell = a 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 // hsu clears the environment so resolve paths early
if config.Path == nil { if config.Path == nil {
if len(config.Args) > 0 { 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} return &hst.AppError{Step: "look up executable file", Err: err}
} else if config.Path, err = container.NewAbs(p); err != nil { } else if config.Path, err = container.NewAbs(p); err != nil {
return newWithMessageError(err.Error(), err) 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 // hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd") 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}}) 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") 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 mapuid, mapgid *stringPair[int]
{ {
var uid, gid int var uid, gid int
var err error var err error
seal.container, seal.env, err = newContainer(config.Container, k, seal.id.String(), &uid, &gid) k.container, k.env, err = newContainer(k, config.Container, k.id.String(), &share.sc, &uid, &gid)
seal.waitDelay = config.Container.WaitDelay k.waitDelay = config.Container.WaitDelay
if err != nil { if err != nil {
return &hst.AppError{Step: "initialise container configuration", Err: err} return &hst.AppError{Step: "initialise container configuration", Err: err}
} }
if len(config.Args) == 0 { if len(config.Args) == 0 {
config.Args = []string{config.Path.String()} config.Args = []string{config.Path.String()}
} }
seal.container.Path = config.Path k.container.Path = config.Path
seal.container.Args = config.Args k.container.Args = config.Args
mapuid = newInt(uid) mapuid = newInt(uid)
mapgid = newInt(gid) mapgid = newInt(gid)
if seal.env == nil { if k.env == nil {
seal.env = make(map[string]string, 1<<6) k.env = make(map[string]string, 1<<6)
} }
} }
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid // inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String()) innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
seal.env[xdgRuntimeDir] = innerRuntimeDir.String() k.env[xdgRuntimeDir] = innerRuntimeDir.String()
seal.env[xdgSessionClass] = "user" k.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" k.env[xdgSessionType] = "tty"
share := &shareHost{seal: seal, sc: k.Paths()} k.runDirPath = share.sc.RunDirPath
seal.runDirPath = share.sc.RunDirPath k.sys = system.New(k.ctx, k.user.uid.unwrap())
seal.sys = system.New(seal.ctx, seal.user.uid.unwrap()) k.sys.Ensure(share.sc.SharePath.String(), 0711)
seal.sys.Ensure(share.sc.SharePath.String(), 0711)
{ {
runtimeDir := share.sc.SharePath.Append("runtime") runtimeDir := share.sc.SharePath.Append("runtime")
seal.sys.Ensure(runtimeDir.String(), 0700) k.sys.Ensure(runtimeDir.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute) k.sys.UpdatePermType(system.User, runtimeDir.String(), acl.Execute)
runtimeDirInst := runtimeDir.Append(seal.user.identity.String()) runtimeDirInst := runtimeDir.Append(k.user.identity.String())
seal.sys.Ensure(runtimeDirInst.String(), 0700) k.sys.Ensure(runtimeDirInst.String(), 0700)
seal.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute) k.sys.UpdatePermType(system.User, runtimeDirInst.String(), acl.Read, acl.Write, acl.Execute)
seal.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755) k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable) k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
} }
{ {
tmpdir := share.sc.SharePath.Append("tmpdir") tmpdir := share.sc.SharePath.Append("tmpdir")
seal.sys.Ensure(tmpdir.String(), 0700) k.sys.Ensure(tmpdir.String(), 0700)
seal.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute) k.sys.UpdatePermType(system.User, tmpdir.String(), acl.Execute)
tmpdirInst := tmpdir.Append(seal.user.identity.String()) tmpdirInst := tmpdir.Append(k.user.identity.String())
seal.sys.Ensure(tmpdirInst.String(), 01700) k.sys.Ensure(tmpdirInst.String(), 01700)
seal.sys.UpdatePermType(system.User, tmpdirInst.String(), acl.Read, acl.Write, acl.Execute) 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 // 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" username := "chronos"
if seal.user.username != "" { if k.user.username != "" {
username = seal.user.username username = k.user.username
} }
seal.container.Dir = seal.user.home k.container.Dir = k.user.home
seal.env["HOME"] = seal.user.home.String() k.env["HOME"] = k.user.home.String()
seal.env["USER"] = username k.env["USER"] = username
seal.env[shell] = config.Shell.String() k.env[shell] = config.Shell.String()
seal.container.Place(container.AbsFHSEtc.Append("passwd"), k.container.Place(container.AbsFHSEtc.Append("passwd"),
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+seal.user.home.String()+":"+config.Shell.String()+"\n")) []byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n"))
seal.container.Place(container.AbsFHSEtc.Append("group"), k.container.Place(container.AbsFHSEtc.Append("group"),
[]byte("hakurei:x:"+mapgid.String()+":\n")) []byte("hakurei:x:"+mapgid.String()+":\n"))
} }
// pass TERM for proper terminal I/O in initial process // pass TERM for proper terminal I/O in initial process
if t, ok := k.LookupEnv(term); ok { if t, ok := k.lookupEnv(term); ok {
seal.env[term] = t k.env[term] = t
} }
if config.Enablements.Unwrap()&system.EWayland != 0 { if config.Enablements.Unwrap()&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath *container.Absolute 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) hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil { } 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) 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 if !config.DirectWayland { // set up security-context-v1
appID := config.ID appID := config.ID
if appID == "" { if appID == "" {
// use instance ID in case app id is not set // 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 // downstream socket paths
outerPath := share.instance().Append("wayland") outerPath := share.instance().Append("wayland")
seal.sys.Wayland(&seal.sync, outerPath.String(), socketPath.String(), appID, seal.id.String()) k.sys.Wayland(&k.sync, outerPath.String(), socketPath.String(), appID, k.id.String())
seal.container.Bind(outerPath, innerPath, 0) k.container.Bind(outerPath, innerPath, 0)
} else { // bind mount wayland socket (insecure) } else { // bind mount wayland socket (insecure)
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION") hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
share.ensureRuntimeDir() share.ensureRuntimeDir()
seal.container.Bind(socketPath, innerPath, 0) k.container.Bind(socketPath, innerPath, 0)
seal.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute) k.sys.UpdatePermType(system.EWayland, socketPath.String(), acl.Read, acl.Write, acl.Execute)
} }
} }
if config.Enablements.Unwrap()&system.EX11 != 0 { 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") return newWithMessage("DISPLAY is not set")
} else { } else {
socketDir := container.AbsFHSTmp.Append(".X11-unix") 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 socketPath != nil {
if _, err := k.Stat(socketPath.String()); err != nil { if _, err := k.stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
} }
} else { } 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 { if !config.Container.HostAbstract {
d = "unix:" + socketPath.String() d = "unix:" + socketPath.String()
} }
} }
} }
seal.sys.ChangeHosts("#" + seal.user.uid.String()) k.sys.ChangeHosts("#" + k.user.uid.String())
seal.env[display] = d k.env[display] = d
seal.container.Bind(socketDir, socketDir, 0) 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`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := pulseRuntimeDir.Append("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) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
} }
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) 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) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} 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 // hard link pulse socket into target-executable share
innerPulseRuntimeDir := share.runtime().Append("pulse") innerPulseRuntimeDir := share.runtime().Append("pulse")
innerPulseSocket := innerRuntimeDir.Append("pulse", "native") innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
seal.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String()) k.sys.Link(pulseSocket.String(), innerPulseRuntimeDir.String())
seal.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0) k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
seal.env[pulseServer] = "unix:" + innerPulseSocket.String() k.env[pulseServer] = "unix:" + innerPulseSocket.String()
// publish current user's pulse cookie for target user // publish current user's pulse cookie for target user
var paCookiePath *container.Absolute 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" const paLocateStep = "locate PulseAudio cookie"
// from environment // from environment
if p, ok := k.LookupEnv(pulseCookie); ok { if p, ok := k.lookupEnv(pulseCookie); ok {
if a, err := container.NewAbs(p); err != nil { if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err} return &hst.AppError{Step: paLocateStep, Err: err}
} else { } else {
@ -468,14 +471,14 @@ func (seal *outcome) finalise(ctx context.Context, k sys.State, config *hst.Conf
} }
// $HOME/.pulse-cookie // $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 { if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err} return &hst.AppError{Step: paLocateStep, Err: err}
} else { } else {
paCookiePath = a.Append(".pulse-cookie") 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 paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err} 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 // $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 { if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err} return &hst.AppError{Step: paLocateStep, Err: err}
} else { } else {
paCookiePath = a.Append("pulse", "cookie") 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 paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err} 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 { if paCookiePath != nil {
innerDst := hst.AbsTmp.Append("/pulse-cookie") innerDst := hst.AbsTmp.Append("/pulse-cookie")
seal.env[pulseCookie] = innerDst.String() k.env[pulseCookie] = innerDst.String()
var payload *[]byte var payload *[]byte
seal.container.PlaceP(innerDst, &payload) k.container.PlaceP(innerDst, &payload)
seal.sys.CopyFile(payload, paCookiePath.String(), 256, 256) k.sys.CopyFile(payload, paCookiePath.String(), 256, 256)
} else { } else {
hlog.Verbose("cannot locate PulseAudio cookie (tried " + hlog.Verbose("cannot locate PulseAudio cookie (tried " +
"$PULSE_COOKIE, " + "$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") sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
// configure dbus proxy // configure dbus proxy
if f, err := seal.sys.ProxyDBus( if f, err := k.sys.ProxyDBus(
config.SessionBus, config.SystemBus, config.SessionBus, config.SystemBus,
sessionPath.String(), systemPath.String(), sessionPath.String(), systemPath.String(),
); err != nil { ); err != nil {
return err return err
} else { } else {
seal.dbusMsg = f k.dbusMsg = f
} }
// share proxy sockets // share proxy sockets
sessionInner := innerRuntimeDir.Append("bus") sessionInner := innerRuntimeDir.Append("bus")
seal.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String() k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
seal.container.Bind(sessionPath, sessionInner, 0) k.container.Bind(sessionPath, sessionInner, 0)
seal.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write) k.sys.UpdatePerm(sessionPath.String(), acl.Read, acl.Write)
if config.SystemBus != nil { if config.SystemBus != nil {
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket") systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
seal.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String() k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
seal.container.Bind(systemPath, systemInner, 0) k.container.Bind(systemPath, systemInner, 0)
seal.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write) k.sys.UpdatePerm(systemPath.String(), acl.Read, acl.Write)
} }
} }
// mount root read-only as the final setup Op // 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 // append ExtraPerms last
for _, p := range config.ExtraPerms { 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 { if p.Ensure {
seal.sys.Ensure(p.Path.String(), 0700) k.sys.Ensure(p.Path.String(), 0700)
} }
perms := make(acl.Perms, 0, 3) 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 { if p.Execute {
perms = append(perms, acl.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 // flatten and sort env for deterministic behaviour
seal.container.Env = make([]string, 0, len(seal.env)) k.container.Env = make([]string, 0, len(k.env))
for k, v := range seal.env { for key, value := range k.env {
if strings.IndexByte(k, '=') != -1 { if strings.IndexByte(key, '=') != -1 {
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL, 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() { if hlog.Load() {
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d", 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 return nil

View File

@ -1,4 +1,4 @@
package sys package app
import ( import (
"errors" "errors"
@ -11,7 +11,6 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
) )
@ -20,15 +19,28 @@ type Hsu struct {
idOnce sync.Once idOnce sync.Once
idErr error idErr error
id int id int
kOnce sync.Once
k syscallDispatcher
} }
var ErrHsuAccess = errors.New("current user is not in the hsurc file") 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. // ID returns the current user hsurc identifier. ErrHsuAccess is returned if the current user is not in hsurc.
func (h *Hsu) ID() (int, error) { func (h *Hsu) ID() (int, error) {
h.ensureDispatcher()
h.idOnce.Do(func() { h.idOnce.Do(func() {
h.id = -1 h.id = -1
hsuPath := internal.MustHsuPath() hsuPath := h.k.mustHsuPath()
cmd := exec.Command(hsuPath) cmd := exec.Command(hsuPath)
cmd.Path = hsuPath cmd.Path = hsuPath
@ -41,7 +53,7 @@ func (h *Hsu) ID() (int, error) {
) )
const step = "obtain uid from hsu" 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)) h.id, h.idErr = strconv.Atoi(string(p))
if h.idErr != nil { if h.idErr != nil {
h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"} 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 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() id, err := h.ID()
if err == nil { if err == nil {
return 1000000 + id*10000 + identity, nil return id
}
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
} }
const fallback = "cannot obtain uid from setuid wrapper:" const fallback = "cannot retrieve user id from setuid wrapper:"
if errors.Is(err, ErrHsuAccess) { if errors.Is(err, ErrHsuAccess) {
hlog.Verbose("*"+fallback, err) hlog.Verbose("*"+fallback, err)
os.Exit(1) os.Exit(1)
@ -86,3 +90,7 @@ func MustUid(s State, identity int) int {
return -0xdeadbeef 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 }

36
internal/app/paths.go Normal file
View File

@ -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)
}

View File

@ -33,12 +33,12 @@ type mainState struct {
// Time is nil if no process was ever created. // Time is nil if no process was ever created.
Time *time.Time Time *time.Time
seal *outcome
store state.Store store state.Store
cancel context.CancelFunc cancel context.CancelFunc
cmd *exec.Cmd cmd *exec.Cmd
cmdWait chan error cmdWait chan error
k *outcome
uintptr uintptr
} }
@ -79,13 +79,13 @@ func (ms mainState) beforeExit(isFault bool) {
waitDone := make(chan struct{}) waitDone := make(chan struct{})
// TODO(ophestra): enforce this limit early so it does not have to be done twice // TODO(ophestra): enforce this limit early so it does not have to be done twice
shimTimeoutCompensated := shimWaitTimeout shimTimeoutCompensated := shimWaitTimeout
if ms.seal.waitDelay > MaxShimWaitDelay { if ms.k.waitDelay > MaxShimWaitDelay {
shimTimeoutCompensated += MaxShimWaitDelay shimTimeoutCompensated += MaxShimWaitDelay
} else { } else {
shimTimeoutCompensated += ms.seal.waitDelay shimTimeoutCompensated += ms.k.waitDelay
} }
// this ties waitDone to ctx with the additional compensated timeout duration // 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 { select {
case err := <-ms.cmdWait: case err := <-ms.cmdWait:
@ -128,20 +128,20 @@ func (ms mainState) beforeExit(isFault bool) {
} }
hlog.Resume() hlog.Resume()
if ms.seal.sync != nil { if ms.k.sync != nil {
if err := ms.seal.sync.Close(); err != nil { if err := ms.k.sync.Close(); err != nil {
perror(err, "close wayland security context") perror(err, "close wayland security context")
} }
} }
if ms.seal.dbusMsg != nil { if ms.k.dbusMsg != nil {
ms.seal.dbusMsg() ms.k.dbusMsg()
} }
} }
if ms.uintptr&mainNeedsRevert != 0 { 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 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") perror(err, "destroy state entry")
} }
} }
@ -151,7 +151,7 @@ func (ms mainState) beforeExit(isFault bool) {
// it is impossible to continue from this point; // it is impossible to continue from this point;
// revert per-process state here to limit damage // revert per-process state here to limit damage
ec := system.Process 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 { var joinError interface {
Unwrap() []error Unwrap() []error
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") 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. // main carries out outcome and terminates. main does not return.
func (seal *outcome) main() { func (k *outcome) main() {
if !seal.f.CompareAndSwap(false, true) { if !k.active.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice") panic("outcome: attempted to run twice")
} }
@ -228,15 +228,15 @@ func (seal *outcome) main() {
hsuPath := internal.MustHsuPath() hsuPath := internal.MustHsuPath()
// ms.beforeExit required beyond this point // 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.fatal("cannot commit system setup:", err)
} }
ms.uintptr |= mainNeedsRevert 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() defer cancel()
ms.cancel = cancel ms.cancel = cancel
@ -255,14 +255,14 @@ func (seal *outcome) main() {
// passed through to shim by hsu // passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd), shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu // interpreted by hsu
"HAKUREI_IDENTITY=" + seal.user.identity.String(), "HAKUREI_IDENTITY=" + k.user.identity.String(),
} }
} }
if len(seal.user.supp) > 0 { if len(k.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp) hlog.Verbosef("attaching supplementary group ids %s", k.user.supp)
// interpreted by hsu // 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) hlog.Verbosef("setuid helper at %s", hsuPath)
@ -284,8 +284,8 @@ func (seal *outcome) main() {
go func() { go func() {
setupErr <- e.Encode(&shimParams{ setupErr <- e.Encode(&shimParams{
os.Getpid(), os.Getpid(),
seal.waitDelay, k.waitDelay,
seal.container, k.container,
hlog.Load(), hlog.Load(),
}) })
}() }()
@ -302,12 +302,12 @@ func (seal *outcome) main() {
} }
// shim accepted setup payload, create process state // 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{ if err := c.Save(&state.State{
ID: seal.id.unwrap(), ID: k.id.unwrap(),
PID: ms.cmd.Process.Pid, PID: ms.cmd.Process.Pid,
Time: *ms.Time, Time: *ms.Time,
}, seal.ct); err != nil { }, k.ct); err != nil {
ms.fatal("cannot save state entry:", err) ms.fatal("cannot save state entry:", err)
} }
}); err != nil { }); err != nil {

View File

@ -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)
}

View File

@ -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
}

View File

@ -100,7 +100,7 @@ print(machine.fail("sudo -u alice -i hsu"))
# Verify hsu fault behaviour: # Verify hsu fault behaviour:
if denyOutput != "hsu: uid 1001 is not in the hsurc file\n": if denyOutput != "hsu: uid 1001 is not in the hsurc file\n":
raise Exception(f"unexpected deny output:\n{denyOutput}") 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}") raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
# Verify timeout behaviour: # Verify timeout behaviour: