aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-10-29 04:32:43 +0900
committerOphestra <cat@gensokyo.uk>2025-10-29 04:33:13 +0900
commita0b4e47acc228e48165966d8e139277b8f6f450e (patch)
treed363a6781536ce946aa445271ca1d3df587e6031 /internal/app
parenta52f7038e5a607dd0901244abb791c96deaf9c2d (diff)
internal/outcome: rename from app
This is less ambiguous, and more accurately describes the purpose of the package. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app')
-rw-r--r--internal/app/app.go28
-rw-r--r--internal/app/app_test.go940
-rw-r--r--internal/app/dispatcher.go163
-rw-r--r--internal/app/dispatcher_test.go644
-rw-r--r--internal/app/finalise.go85
-rw-r--r--internal/app/hsu.go101
-rw-r--r--internal/app/hsu_test.go84
-rw-r--r--internal/app/outcome.go302
-rw-r--r--internal/app/outcome_test.go34
-rw-r--r--internal/app/process.go321
-rw-r--r--internal/app/shim-signal.c65
-rw-r--r--internal/app/shim-signal.h3
-rw-r--r--internal/app/shim.go235
-rw-r--r--internal/app/shim_test.go156
-rw-r--r--internal/app/spaccount.go56
-rw-r--r--internal/app/spaccount_test.go72
-rw-r--r--internal/app/spcontainer.go391
-rw-r--r--internal/app/spcontainer_test.go477
-rw-r--r--internal/app/spdbus.go58
-rw-r--r--internal/app/spdbus_test.go190
-rw-r--r--internal/app/sppulse.go210
-rw-r--r--internal/app/sppulse_test.go460
-rw-r--r--internal/app/spruntime.go125
-rw-r--r--internal/app/spruntime_test.go128
-rw-r--r--internal/app/sptmpdir.go44
-rw-r--r--internal/app/sptmpdir_test.go34
-rw-r--r--internal/app/spwayland.go63
-rw-r--r--internal/app/spwayland_test.go104
-rw-r--r--internal/app/spx11.go71
-rw-r--r--internal/app/spx11_test.go119
-rw-r--r--internal/app/state/data.go52
-rw-r--r--internal/app/state/data_test.go145
-rw-r--r--internal/app/state/header.go86
-rw-r--r--internal/app/state/header_test.go184
-rw-r--r--internal/app/state/join.go64
-rw-r--r--internal/app/state/segment.go161
-rw-r--r--internal/app/state/segment_test.go259
-rw-r--r--internal/app/state/state.go131
-rw-r--r--internal/app/state/state_test.go120
-rw-r--r--internal/app/state/store.go162
-rw-r--r--internal/app/state/store_test.go254
41 files changed, 0 insertions, 7381 deletions
diff --git a/internal/app/app.go b/internal/app/app.go
deleted file mode 100644
index 03608517..00000000
--- a/internal/app/app.go
+++ /dev/null
@@ -1,28 +0,0 @@
-// Package app implements high-level hakurei container behaviour.
-package app
-
-import (
- "context"
- "log"
- "os"
-
- "hakurei.app/hst"
- "hakurei.app/message"
-)
-
-// Main runs an app according to [hst.Config] and terminates. Main does not return.
-func Main(ctx context.Context, msg message.Msg, config *hst.Config) {
- var id hst.ID
- if err := hst.NewInstanceID(&id); err != nil {
- log.Fatal(err.Error())
- }
-
- seal := outcome{syscallDispatcher: direct{msg}}
- if err := seal.finalise(ctx, msg, &id, config); err != nil {
- printMessageError("cannot seal app:", err)
- os.Exit(1)
- }
-
- seal.main(msg)
- panic("unreachable")
-}
diff --git a/internal/app/app_test.go b/internal/app/app_test.go
deleted file mode 100644
index 96113eb8..00000000
--- a/internal/app/app_test.go
+++ /dev/null
@@ -1,940 +0,0 @@
-package app
-
-import (
- "bytes"
- "encoding/gob"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "log"
- "os/exec"
- "os/user"
- "reflect"
- "syscall"
- "testing"
- "time"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/container/seccomp"
- "hakurei.app/hst"
- "hakurei.app/message"
- "hakurei.app/system"
- "hakurei.app/system/acl"
- "hakurei.app/system/dbus"
-)
-
-func TestApp(t *testing.T) {
- t.Parallel()
- msg := message.NewMsg(nil)
- msg.SwapVerbose(testing.Verbose())
-
- testCases := []struct {
- name string
- k syscallDispatcher
- config *hst.Config
- id hst.ID
- wantSys *system.I
- wantParams *container.Params
- }{
- {"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.NewMsg(nil), 1000009).
- // spParamsOp
- Ensure(m("/tmp/hakurei.0"), 0711).
-
- // spRuntimeOp
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
-
- // spTmpdirOp
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
-
- // instance
- Ephemeral(system.Process, m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0711).
-
- // spWaylandOp
- Wayland(
- m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"),
- m("/run/user/1971/wayland-0"),
- "org.chromium.Chromium",
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- ).
-
- // ensureRuntimeDir
- Ensure(m("/run/user/1971/hakurei"), 0700).
- UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
- Ensure(m("/run/user/1971"), 0700).
- UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
-
- // runtime
- Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
- UpdatePerm(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), acl.Execute).
-
- // spPulseOp
- Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse")).
-
- // spDBusOp
- MustProxyDBus(
- hst.Template().SessionBus,
- hst.Template().SystemBus, dbus.ProxyPair{
- "unix:path=/run/user/1971/bus",
- "/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus",
- }, dbus.ProxyPair{
- "unix:path=/var/run/dbus/system_bus_socket",
- "/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket",
- },
- ).UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), acl.Read, acl.Write).
- UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), acl.Read, acl.Write).
-
- // spFilesystemOp
- Ensure(m("/var/lib/hakurei/u0"), 0700).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), acl.Read, acl.Write, acl.Execute), &container.Params{
-
- Dir: m("/data/data/org.chromium.Chromium"),
- Env: []string{
- "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
- "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
- "GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
- "GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
- "GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
- "HOME=/data/data/org.chromium.Chromium",
- "PULSE_COOKIE=/.hakurei/pulse-cookie",
- "PULSE_SERVER=unix:/run/user/1971/pulse/native",
- "SHELL=/run/current-system/sw/bin/zsh",
- "TERM=xterm-256color",
- "USER=chronos",
- "WAYLAND_DISPLAY=wayland-0",
- "XDG_RUNTIME_DIR=/run/user/1971",
- "XDG_SESSION_CLASS=user",
- "XDG_SESSION_TYPE=wayland",
- },
-
- // spParamsOp
- Hostname: "localhost",
- RetainSession: true,
- HostNet: true,
- HostAbstract: true,
- Path: m("/run/current-system/sw/bin/chromium"),
- Args: []string{
- "chromium",
- "--ignore-gpu-blocklist",
- "--disable-smooth-scrolling",
- "--enable-features=UseOzonePlatform",
- "--ozone-platform=wayland",
- },
- SeccompFlags: seccomp.AllowMultiarch,
- Uid: 1971,
- Gid: 100,
-
- Ops: new(container.Ops).
- // resolveRoot
- Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
- // spParamsOp
- Proc(fhs.AbsProc).
- Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
- Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice).
- Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777).
-
- // spRuntimeOp
- Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
- Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), comp.BindWritable).
-
- // spTmpdirOp
- Bind(m("/tmp/hakurei.0/tmpdir/9"), fhs.AbsTmp, comp.BindWritable).
-
- // spAccountOp
- Place(m("/etc/passwd"), []byte("chronos:x:1971:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
- Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
-
- // spWaylandOp
- Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
-
- // spPulseOp
- Bind(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1971/pulse/native"), 0).
- Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
-
- // spDBusOp
- Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
- Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
-
- // spFilesystemOp
- Etc(fhs.AbsEtc, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
- Tmpfs(fhs.AbsTmp, 0, 0755).
- Overlay(m("/nix/store"),
- fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
- fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
- Link(m("/run/current-system"), "/run/current-system", true).
- Link(m("/run/opengl-driver"), "/run/opengl-driver", true).
- Bind(fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
- m("/data/data/org.chromium.Chromium"),
- comp.BindWritable|comp.BindEnsure).
- Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"),
- comp.BindOptional|comp.BindWritable|comp.BindDevice).
- Remount(fhs.AbsRoot, syscall.MS_RDONLY),
- }},
-
- {"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{Container: &hst.ContainerConfig{
- Filesystem: []hst.FilesystemConfigJSON{
- {FilesystemConfig: &hst.FSBind{
- Target: fhs.AbsRoot,
- Source: fhs.AbsRoot,
- Write: true,
- Special: true,
- }},
- {FilesystemConfig: &hst.FSBind{
- Source: fhs.AbsDev.Append("kvm"),
- Device: true,
- Optional: true,
- }},
- {FilesystemConfig: &hst.FSBind{
- Target: fhs.AbsEtc,
- Source: fhs.AbsEtc,
- Special: true,
- }},
- },
-
- Username: "chronos",
- Shell: m("/run/current-system/sw/bin/zsh"),
- Home: m("/home/chronos"),
-
- Path: m("/run/current-system/sw/bin/zsh"),
- Args: []string{"/run/current-system/sw/bin/zsh"},
-
- Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty | hst.FShareRuntime | hst.FShareTmpdir,
- }}, hst.ID{
- 0x4a, 0x45, 0x0b, 0x65,
- 0x96, 0xd7, 0xbc, 0x15,
- 0xbd, 0x01, 0x78, 0x0e,
- 0xb9, 0xa6, 0x07, 0xac,
- }, system.New(t.Context(), msg, 1000000).
- Ensure(m("/tmp/hakurei.0"), 0711).
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/0"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).
- UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700).
- UpdatePermType(system.User, m("/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"),
- Args: []string{"/run/current-system/sw/bin/zsh"},
- Env: []string{
- "HOME=/home/chronos",
- "SHELL=/run/current-system/sw/bin/zsh",
- "TERM=xterm-256color",
- "USER=chronos",
- "XDG_RUNTIME_DIR=/run/user/65534",
- "XDG_SESSION_CLASS=user",
- "XDG_SESSION_TYPE=tty",
- },
- Ops: new(container.Ops).
- Root(m("/"), comp.BindWritable).
- Proc(m("/proc/")).
- Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
- DevWritable(m("/dev/"), true).
- Tmpfs(m("/dev/shm"), 0, 01777).
- Tmpfs(m("/run/user/"), 4096, 0755).
- Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), comp.BindWritable).
- Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), comp.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("/dev/kvm"), m("/dev/kvm"), comp.BindWritable|comp.BindDevice|comp.BindOptional).
- Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
- Tmpfs(m("/run/user/1971"), 8192, 0755).
- Tmpfs(m("/run/nscd"), 8192, 0755).
- Tmpfs(m("/run/dbus"), 8192, 0755).
- Remount(m("/dev/"), syscall.MS_RDONLY).
- Remount(m("/"), syscall.MS_RDONLY),
- SeccompPresets: comp.PresetExt | comp.PresetDenyDevel,
- HostNet: true,
- HostAbstract: true,
- RetainSession: true,
- ForwardCancel: true,
- }},
-
- {"nixos permissive defaults chromium", new(stubNixOS), &hst.Config{
- ID: "org.chromium.Chromium",
- Identity: 9,
- Groups: []string{"video"},
- SessionBus: &hst.BusConfig{
- Talk: []string{
- "org.freedesktop.Notifications",
- "org.freedesktop.FileManager1",
- "org.freedesktop.ScreenSaver",
- "org.freedesktop.secrets",
- "org.kde.kwalletd5",
- "org.kde.kwalletd6",
- "org.gnome.SessionManager",
- },
- Own: []string{
- "org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.chromium.*",
- },
- Call: map[string]string{
- "org.freedesktop.portal.*": "*",
- },
- Broadcast: map[string]string{
- "org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
- },
- Filter: true,
- },
- SystemBus: &hst.BusConfig{
- Talk: []string{
- "org.bluez",
- "org.freedesktop.Avahi",
- "org.freedesktop.UPower",
- },
- Filter: true,
- },
- Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
-
- Container: &hst.ContainerConfig{
- Filesystem: []hst.FilesystemConfigJSON{
- {FilesystemConfig: &hst.FSBind{
- Target: fhs.AbsRoot,
- Source: fhs.AbsRoot,
- Write: true,
- Special: true,
- }},
- {FilesystemConfig: &hst.FSBind{
- Source: fhs.AbsDev.Append("dri"),
- Device: true,
- Optional: true,
- }},
- {FilesystemConfig: &hst.FSBind{
- Source: fhs.AbsDev.Append("kvm"),
- Device: true,
- Optional: true,
- }},
- {FilesystemConfig: &hst.FSBind{
- Target: fhs.AbsEtc,
- Source: fhs.AbsEtc,
- Special: true,
- }},
- },
-
- Username: "chronos",
- Shell: m("/run/current-system/sw/bin/zsh"),
- Home: m("/home/chronos"),
-
- Path: m("/run/current-system/sw/bin/zsh"),
- Args: []string{"zsh", "-c", "exec chromium "},
-
- Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty | hst.FShareRuntime | hst.FShareTmpdir,
- },
- }, hst.ID{
- 0xeb, 0xf0, 0x83, 0xd1,
- 0xb1, 0x75, 0x91, 0x17,
- 0x82, 0xd4, 0x13, 0x36,
- 0x9b, 0x64, 0xce, 0x7c,
- }, system.New(t.Context(), msg, 1000009).
- Ensure(m("/tmp/hakurei.0"), 0711).
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
- Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
- Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
- Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
- Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/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, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
- Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
- MustProxyDBus(&hst.BusConfig{
- Talk: []string{
- "org.freedesktop.Notifications",
- "org.freedesktop.FileManager1",
- "org.freedesktop.ScreenSaver",
- "org.freedesktop.secrets",
- "org.kde.kwalletd5",
- "org.kde.kwalletd6",
- "org.gnome.SessionManager",
- },
- Own: []string{
- "org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.chromium.*",
- },
- Call: map[string]string{
- "org.freedesktop.portal.*": "*",
- },
- Broadcast: map[string]string{
- "org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
- },
- Filter: true,
- }, &hst.BusConfig{
- Talk: []string{
- "org.bluez",
- "org.freedesktop.Avahi",
- "org.freedesktop.UPower",
- },
- Filter: true,
- }, dbus.ProxyPair{
- "unix:path=/run/user/1971/bus",
- "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus",
- }, dbus.ProxyPair{
- "unix:path=/var/run/dbus/system_bus_socket",
- "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket",
- }).
- UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write).
- UpdatePerm(m("/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"),
- Args: []string{"zsh", "-c", "exec chromium "},
- Env: []string{
- "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
- "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
- "HOME=/home/chronos",
- "PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
- "PULSE_SERVER=unix:/run/user/65534/pulse/native",
- "SHELL=/run/current-system/sw/bin/zsh",
- "TERM=xterm-256color",
- "USER=chronos",
- "WAYLAND_DISPLAY=wayland-0",
- "XDG_RUNTIME_DIR=/run/user/65534",
- "XDG_SESSION_CLASS=user",
- "XDG_SESSION_TYPE=wayland",
- },
- Ops: new(container.Ops).
- Root(m("/"), comp.BindWritable).
- Proc(m("/proc/")).
- Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
- DevWritable(m("/dev/"), true).
- Tmpfs(m("/dev/shm"), 0, 01777).
- Tmpfs(m("/run/user/"), 4096, 0755).
- Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), comp.BindWritable).
- Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), comp.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.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.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
- Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
- Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
- Bind(m("/dev/dri"), m("/dev/dri"), comp.BindWritable|comp.BindDevice|comp.BindOptional).
- Bind(m("/dev/kvm"), m("/dev/kvm"), comp.BindWritable|comp.BindDevice|comp.BindOptional).
- Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
- Tmpfs(m("/run/user/1971"), 8192, 0755).
- Tmpfs(m("/run/nscd"), 8192, 0755).
- Tmpfs(m("/run/dbus"), 8192, 0755).
- Remount(m("/dev/"), syscall.MS_RDONLY).
- Remount(m("/"), syscall.MS_RDONLY),
- SeccompPresets: comp.PresetExt | comp.PresetDenyDevel,
- HostNet: true,
- HostAbstract: true,
- RetainSession: true,
- ForwardCancel: true,
- }},
-
- {"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
- ID: "org.chromium.Chromium",
- Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
- Container: &hst.ContainerConfig{
- Env: nil,
- Filesystem: []hst.FilesystemConfigJSON{
- f(&hst.FSBind{Source: m("/bin")}),
- f(&hst.FSBind{Source: m("/usr/bin/")}),
- f(&hst.FSBind{Source: m("/nix/store")}),
- f(&hst.FSBind{Source: m("/run/current-system")}),
- f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
- f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
- f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
- f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
- f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
- f(&hst.FSBind{Source: m("/run/opengl-driver")}),
- f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
- f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
- f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
- },
-
- Username: "u0_a1",
- Shell: m("/run/current-system/sw/bin/zsh"),
- Home: m("/var/lib/persist/module/hakurei/0/1"),
-
- Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
-
- Flags: hst.FUserns | hst.FHostNet | hst.FMapRealUID | hst.FShareRuntime | hst.FShareTmpdir,
- },
- SystemBus: &hst.BusConfig{
- Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
- Filter: true,
- },
- SessionBus: &hst.BusConfig{
- Talk: []string{
- "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
- "org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
- "org.kde.kwalletd5", "org.kde.kwalletd6",
- },
- Own: []string{
- "org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.chromium.*",
- },
- Call: map[string]string{}, Broadcast: map[string]string{},
- Filter: true,
- },
- DirectWayland: true,
-
- Identity: 1, Groups: []string{},
- }, hst.ID{
- 0x8e, 0x2c, 0x76, 0xb0,
- 0x66, 0xda, 0xbe, 0x57,
- 0x4c, 0xf0, 0x73, 0xbd,
- 0xb4, 0x6e, 0xb5, 0xc1,
- }, system.New(t.Context(), msg, 1000001).
- Ensure(m("/tmp/hakurei.0"), 0711).
- Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/runtime/1"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read, acl.Write, acl.Execute).
- Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
- Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
- UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
- Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
- Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
- Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
- MustProxyDBus(&hst.BusConfig{
- Talk: []string{
- "org.freedesktop.FileManager1", "org.freedesktop.Notifications",
- "org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
- "org.kde.kwalletd5", "org.kde.kwalletd6",
- },
- Own: []string{
- "org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "org.mpris.MediaPlayer2.chromium.*",
- },
- Call: map[string]string{}, Broadcast: map[string]string{},
- Filter: true,
- }, &hst.BusConfig{
- Talk: []string{
- "org.bluez",
- "org.freedesktop.Avahi",
- "org.freedesktop.UPower",
- },
- Filter: true,
- }, dbus.ProxyPair{
- "unix:path=/run/user/1971/bus",
- "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus",
- }, dbus.ProxyPair{
- "unix:path=/var/run/dbus/system_bus_socket",
- "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket",
- }).
- UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write).
- UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write), &container.Params{
-
- Uid: 1971,
- Gid: 100,
- Dir: m("/var/lib/persist/module/hakurei/0/1"),
- Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
- Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
- Env: []string{
- "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
- "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
- "HOME=/var/lib/persist/module/hakurei/0/1",
- "PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
- "PULSE_SERVER=unix:/run/user/1971/pulse/native",
- "SHELL=/run/current-system/sw/bin/zsh",
- "TERM=xterm-256color",
- "USER=u0_a1",
- "WAYLAND_DISPLAY=wayland-0",
- "XDG_RUNTIME_DIR=/run/user/1971",
- "XDG_SESSION_CLASS=user",
- "XDG_SESSION_TYPE=wayland",
- },
- Ops: new(container.Ops).
- Proc(m("/proc/")).
- Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
- DevWritable(m("/dev/"), true).
- Tmpfs(m("/dev/shm"), 0, 01777).
- Tmpfs(m("/run/user/"), 4096, 0755).
- Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), comp.BindWritable).
- Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), comp.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.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
- Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
- Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
- Bind(m("/bin"), m("/bin"), 0).
- Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
- Bind(m("/nix/store"), m("/nix/store"), 0).
- Bind(m("/run/current-system"), m("/run/current-system"), 0).
- Bind(m("/sys/block"), m("/sys/block"), comp.BindOptional).
- Bind(m("/sys/bus"), m("/sys/bus"), comp.BindOptional).
- Bind(m("/sys/class"), m("/sys/class"), comp.BindOptional).
- Bind(m("/sys/dev"), m("/sys/dev"), comp.BindOptional).
- Bind(m("/sys/devices"), m("/sys/devices"), comp.BindOptional).
- Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
- Bind(m("/dev/dri"), m("/dev/dri"), comp.BindDevice|comp.BindWritable|comp.BindOptional).
- Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
- Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), comp.BindWritable|comp.BindEnsure).
- Remount(m("/dev/"), syscall.MS_RDONLY).
- Remount(m("/"), syscall.MS_RDONLY),
- SeccompPresets: comp.PresetExt | comp.PresetDenyTTY | comp.PresetDenyDevel,
- HostNet: true,
- ForwardCancel: true,
- }},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- gr, gw := io.Pipe()
-
- var gotSys *system.I
- {
- sPriv := newOutcomeState(tc.k, msg, &tc.id, tc.config, &Hsu{k: tc.k})
- if err := sPriv.populateLocal(tc.k, msg); err != nil {
- t.Fatalf("populateLocal: error = %#v", err)
- }
-
- gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap())
- if err := sPriv.newSys(tc.config, gotSys).toSystem(); err != nil {
- t.Fatalf("toSystem: error = %#v", err)
- }
-
- go func() {
- e := gob.NewEncoder(gw)
- if err := errors.Join(e.Encode(&sPriv)); err != nil {
- t.Errorf("Encode: error = %v", err)
- panic("unexpected encode fault")
- }
- }()
- }
-
- var gotParams *container.Params
- {
- var sShim outcomeState
-
- d := gob.NewDecoder(gr)
- if err := errors.Join(d.Decode(&sShim)); err != nil {
- t.Fatalf("Decode: error = %v", err)
- }
- if err := sShim.populateLocal(tc.k, msg); err != nil {
- t.Fatalf("populateLocal: error = %#v", err)
- }
-
- stateParams := sShim.newParams()
- for _, op := range sShim.Shim.Ops {
- if err := op.toContainer(stateParams); err != nil {
- t.Fatalf("toContainer: error = %#v", err)
- }
- }
- gotParams = stateParams.params
- }
-
- t.Run("sys", func(t *testing.T) {
- if !gotSys.Equal(tc.wantSys) {
- t.Errorf("toSystem: sys = %#v, want %#v", gotSys, tc.wantSys)
- }
- })
-
- t.Run("params", func(t *testing.T) {
- if !reflect.DeepEqual(gotParams, tc.wantParams) {
- t.Errorf("toContainer: params =\n%s\n, want\n%s", mustMarshal(gotParams), mustMarshal(tc.wantParams))
- }
- })
- })
- }
-}
-
-func mustMarshal(v any) string {
- if b, err := json.Marshal(v); err != nil {
- panic(err.Error())
- } else {
- return string(b)
- }
-}
-
-func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
- e = make([]fs.DirEntry, len(names))
- for i, name := range names {
- e[i] = stubDirEntryPath(name)
- }
- return
-}
-
-type stubDirEntryPath string
-
-func (p stubDirEntryPath) Name() string { return string(p) }
-func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
-func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
-func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
-
-type stubFileInfoMode fs.FileMode
-
-func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
-func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
-func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
-func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
-func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
-func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
-
-type stubFileInfoIsDir bool
-
-func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
-func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
-func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
-func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
-func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
-func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
-
-type stubFileInfoPulseCookie struct{ stubFileInfoIsDir }
-
-func (s stubFileInfoPulseCookie) Size() int64 { return pulseCookieSizeMax }
-
-type stubOsFileReadCloser struct{ io.ReadCloser }
-
-func (s stubOsFileReadCloser) Name() string { panic("attempting to call Name") }
-func (s stubOsFileReadCloser) Write([]byte) (int, error) { panic("attempting to call Write") }
-func (s stubOsFileReadCloser) Stat() (fs.FileInfo, error) { panic("attempting to call Stat") }
-
-type stubNixOS struct {
- usernameErr map[string]error
- panicDispatcher
-}
-
-func (k *stubNixOS) getpid() int { return 0xdeadbeef }
-func (k *stubNixOS) getuid() int { return 1971 }
-func (k *stubNixOS) getgid() int { return 100 }
-
-func (k *stubNixOS) lookupEnv(key string) (string, bool) {
- switch key {
- case "SHELL":
- return "/run/current-system/sw/bin/zsh", true
- case "TERM":
- return "xterm-256color", true
- case "WAYLAND_DISPLAY":
- return "wayland-0", true
- case "PULSE_COOKIE":
- 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
- case "DBUS_SYSTEM_BUS_ADDRESS":
- return "", false
- default:
- panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
- }
-}
-
-func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
- switch name {
- case "/var/run/nscd":
- return nil, nil
- case "/run/user/1971/pulse":
- return nil, nil
- case "/run/user/1971/pulse/native":
- return stubFileInfoMode(0666), nil
- case "/home/ophestra/.pulse-cookie":
- return stubFileInfoIsDir(true), nil
- case "/home/ophestra/xdg/config/pulse/cookie":
- return stubFileInfoPulseCookie{false}, nil
- default:
- panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
- }
-}
-
-func (k *stubNixOS) open(name string) (osFile, error) {
- switch name {
- case "/home/ophestra/xdg/config/pulse/cookie":
- return stubOsFileReadCloser{io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{0}, pulseCookieSizeMax)))}, nil
- default:
- panic(fmt.Sprintf("attempted to open unexpected path %q", name))
- }
-}
-
-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")
-
- case "/var/lib/hakurei/base/org.debian":
- return stubDirEntries("bin", "dev", "etc", "home", "lib64", "lost+found",
- "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
-
- default:
- panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
- }
-}
-
-func (k *stubNixOS) tempdir() string { return "/tmp/" }
-
-func (k *stubNixOS) evalSymlinks(path string) (string, error) {
- switch path {
- case "/var/run/nscd":
- return "/run/nscd", nil
- case "/run/user/1971":
- return "/run/user/1971", nil
- case "/tmp/hakurei.0":
- return "/tmp/hakurei.0", nil
- case "/var/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
-
- case "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper":
- return "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper", nil
- case "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work":
- return "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work", nil
- case "/var/lib/hakurei/base/org.nixos/ro-store":
- return "/var/lib/hakurei/base/org.nixos/ro-store", nil
- case "/var/lib/hakurei/u0/org.chromium.Chromium":
- return "/var/lib/hakurei/u0/org.chromium.Chromium", nil
- case "/var/lib/hakurei/base/org.debian/bin":
- return "/var/lib/hakurei/base/org.debian/bin", nil
- case "/var/lib/hakurei/base/org.debian/home":
- return "/var/lib/hakurei/base/org.debian/home", nil
- case "/var/lib/hakurei/base/org.debian/lib64":
- return "/var/lib/hakurei/base/org.debian/lib64", nil
- case "/var/lib/hakurei/base/org.debian/lost+found":
- return "/var/lib/hakurei/base/org.debian/lost+found", nil
- case "/var/lib/hakurei/base/org.debian/nix":
- return "/var/lib/hakurei/base/org.debian/nix", nil
- case "/var/lib/hakurei/base/org.debian/root":
- return "/var/lib/hakurei/base/org.debian/root", nil
- case "/var/lib/hakurei/base/org.debian/run":
- return "/var/lib/hakurei/base/org.debian/run", nil
- case "/var/lib/hakurei/base/org.debian/srv":
- return "/var/lib/hakurei/base/org.debian/srv", nil
- case "/var/lib/hakurei/base/org.debian/sys":
- return "/var/lib/hakurei/base/org.debian/sys", nil
- case "/var/lib/hakurei/base/org.debian/usr":
- return "/var/lib/hakurei/base/org.debian/usr", nil
- case "/var/lib/hakurei/base/org.debian/var":
- return "/var/lib/hakurei/base/org.debian/var", nil
-
- default:
- panic(fmt.Sprintf("attempted to evaluate unexpected path %q", path))
- }
-}
-
-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(message.Msg) int { return 65534 }
-func (k *stubNixOS) overflowGid(message.Msg) int { return 65534 }
-
-func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") }
-
-func (k *stubNixOS) dbusAddress() (string, string) {
- return "unix:path=/run/user/1971/bus", "unix:path=/var/run/dbus/system_bus_socket"
-}
-
-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...) }
-
-func m(pathname string) *check.Absolute {
- return check.MustAbs(pathname)
-}
-
-func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
- return hst.FilesystemConfigJSON{FilesystemConfig: c}
-}
diff --git a/internal/app/dispatcher.go b/internal/app/dispatcher.go
deleted file mode 100644
index b081ece1..00000000
--- a/internal/app/dispatcher.go
+++ /dev/null
@@ -1,163 +0,0 @@
-package app
-
-import (
- "context"
- "io"
- "io/fs"
- "os"
- "os/exec"
- "os/signal"
- "os/user"
- "path/filepath"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/container/seccomp"
- "hakurei.app/internal"
- "hakurei.app/message"
- "hakurei.app/system/dbus"
-)
-
-// osFile represents [os.File].
-type osFile interface {
- Name() string
- io.Writer
- fs.File
-}
-
-// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
-type syscallDispatcher interface {
- // 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, msg message.Msg))
-
- // getpid provides [os.Getpid].
- getpid() int
- // getuid provides [os.Getuid].
- getuid() int
- // getgid provides [os.Getgid].
- getgid() int
- // lookupEnv provides [os.LookupEnv].
- lookupEnv(key string) (string, bool)
- // pipe provides os.Pipe.
- pipe() (r, w *os.File, err error)
- // stat provides [os.Stat].
- stat(name string) (os.FileInfo, error)
- // open provides [os.Open].
- open(name string) (osFile, error)
- // readdir provides [os.ReadDir].
- readdir(name string) ([]os.DirEntry, error)
- // tempdir provides [os.TempDir].
- tempdir() string
- // exit provides [os.Exit].
- exit(code int)
-
- // evalSymlinks provides [filepath.EvalSymlinks].
- evalSymlinks(path 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)
-
- // notifyContext provides [signal.NotifyContext].
- notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
-
- // prctl provides [container.Prctl].
- prctl(op, arg2, arg3 uintptr) error
- // overflowUid provides [container.OverflowUid].
- overflowUid(msg message.Msg) int
- // overflowGid provides [container.OverflowGid].
- overflowGid(msg message.Msg) int
- // setDumpable provides [container.SetDumpable].
- setDumpable(dumpable uintptr) error
- // receive provides [container.Receive].
- receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
-
- // containerStart provides the Start method of [container.Container].
- containerStart(z *container.Container) error
- // containerStart provides the Serve method of [container.Container].
- containerServe(z *container.Container) error
- // containerStart provides the Wait method of [container.Container].
- containerWait(z *container.Container) error
-
- // seccompLoad provides [seccomp.Load].
- seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
-
- // mustHsuPath provides [internal.MustHsuPath].
- mustHsuPath() *check.Absolute
-
- // dbusAddress provides [dbus.Address].
- dbusAddress() (session, system string)
-
- // setupContSignal provides setupContSignal.
- setupContSignal(pid int) (io.ReadCloser, func(), error)
-
- // getMsg returns the [message.Msg] held by syscallDispatcher.
- getMsg() message.Msg
- // fatal provides [log.Fatal].
- fatal(v ...any)
- // fatalf provides [log.Fatalf].
- fatalf(format string, v ...any)
-}
-
-// direct implements syscallDispatcher on the current kernel.
-type direct struct{ msg message.Msg }
-
-func (k direct) new(f func(k syscallDispatcher, msg message.Msg)) { go f(k, k.msg) }
-
-func (direct) getpid() int { return os.Getpid() }
-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) pipe() (r, w *os.File, err error) { return os.Pipe() }
-func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
-func (direct) open(name string) (osFile, error) { return os.Open(name) }
-func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
-func (direct) tempdir() string { return os.TempDir() }
-func (direct) exit(code int) { os.Exit(code) }
-
-func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
-
-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) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
- return signal.NotifyContext(parent, signals...)
-}
-
-func (direct) prctl(op, arg2, arg3 uintptr) error { return container.Prctl(op, arg2, arg3) }
-func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
-func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
-func (direct) setDumpable(dumpable uintptr) error { return container.SetDumpable(dumpable) }
-func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
- return container.Receive(key, e, fdp)
-}
-
-func (direct) containerStart(z *container.Container) error { return z.Start() }
-func (direct) containerServe(z *container.Container) error { return z.Serve() }
-func (direct) containerWait(z *container.Container) error { return z.Wait() }
-
-func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
- return seccomp.Load(rules, flags)
-}
-
-func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() }
-
-func (direct) dbusAddress() (session, system string) { return dbus.Address() }
-
-func (direct) setupContSignal(pid int) (io.ReadCloser, func(), error) { return setupContSignal(pid) }
-
-func (k direct) getMsg() message.Msg { return k.msg }
-func (k direct) fatal(v ...any) { k.msg.GetLogger().Fatal(v...) }
-func (k direct) fatalf(format string, v ...any) { k.msg.GetLogger().Fatalf(format, v...) }
diff --git a/internal/app/dispatcher_test.go b/internal/app/dispatcher_test.go
deleted file mode 100644
index 839e9e42..00000000
--- a/internal/app/dispatcher_test.go
+++ /dev/null
@@ -1,644 +0,0 @@
-package app
-
-import (
- "bytes"
- "context"
- "io"
- "io/fs"
- "log"
- "maps"
- "os"
- "os/exec"
- "reflect"
- "slices"
- "sync"
- "testing"
- "time"
- "unsafe"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/container/seccomp"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/message"
- "hakurei.app/system"
-)
-
-// call initialises a [stub.Call].
-// This keeps composites analysis happy without making the test cases too bloated.
-func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
- return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
-}
-
-const (
- // checkExpectUid is the uid value used by checkOpBehaviour to initialise [system.I].
- checkExpectUid = 0xcafebabe
- // wantAutoEtcPrefix is the autoetc prefix corresponding to checkExpectInstanceId.
- wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
- // wantInstancePrefix is the SharePath corresponding to checkExpectInstanceId.
- wantInstancePrefix = container.Nonexistent + "/tmp/hakurei.0/" + wantAutoEtcPrefix
-
- // wantRuntimePath is the XDG_RUNTIME_DIR value returned during testing.
- wantRuntimePath = "/proc/nonexistent/xdg_runtime_dir"
- // wantRunDirPath is the RunDirPath value resolved during testing.
- wantRunDirPath = wantRuntimePath + "/hakurei"
- // wantRuntimeSharePath is the runtimeSharePath value resolved during testing.
- wantRuntimeSharePath = wantRunDirPath + "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-)
-
-// checkExpectInstanceId is the [hst.ID] value used by checkOpBehaviour to initialise outcomeState.
-var checkExpectInstanceId = *(*hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{})))
-
-type (
- // pStateSysFunc is called before each test case is run to prepare outcomeStateSys.
- pStateSysFunc = func(state *outcomeStateSys)
- // pStateContainerFunc is called before each test case is run to prepare outcomeStateParams.
- pStateContainerFunc = func(state *outcomeStateParams)
-
- // extraCheckSysFunc is called to check outcomeStateSys and must not have side effects.
- extraCheckSysFunc = func(t *testing.T, state *outcomeStateSys)
- // extraCheckParamsFunc is called to check outcomeStateParams and must not have side effects.
- extraCheckParamsFunc = func(t *testing.T, state *outcomeStateParams)
-)
-
-// insertsOps prepares outcomeStateParams to allow [container.Op] to be inserted.
-func insertsOps(next pStateContainerFunc) pStateContainerFunc {
- return func(state *outcomeStateParams) {
- state.params.Ops = new(container.Ops)
-
- if next != nil {
- next(state)
- }
- }
-}
-
-// afterSpRuntimeOp prepares outcomeStateParams for an outcomeOp meant to run after spRuntimeOp.
-func afterSpRuntimeOp(next pStateContainerFunc) pStateContainerFunc {
- return func(state *outcomeStateParams) {
- // emulates spRuntimeOp
- state.runtimeDir = m("/run/user/1000")
-
- if next != nil {
- next(state)
- }
- }
-}
-
-// sysUsesInstance checks for use of the outcomeStateSys.instance method.
-func sysUsesInstance(next extraCheckSysFunc) extraCheckSysFunc {
- return func(t *testing.T, state *outcomeStateSys) {
- if want := m(wantInstancePrefix); !reflect.DeepEqual(state.sharePath, want) {
- t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
- }
-
- if next != nil {
- next(t, state)
- }
- }
-}
-
-// sysUsesRuntime checks for use of the outcomeStateSys.runtime method.
-func sysUsesRuntime(next extraCheckSysFunc) extraCheckSysFunc {
- return func(t *testing.T, state *outcomeStateSys) {
- if want := m(wantRuntimeSharePath); !reflect.DeepEqual(state.runtimeSharePath, want) {
- t.Errorf("outcomeStateSys: runtimeSharePath = %v, want %v", state.runtimeSharePath, want)
- }
-
- if next != nil {
- next(t, state)
- }
- }
-}
-
-// paramsWantEnv checks outcomeStateParams.env for inserted entries on top of [hst.Config].
-func paramsWantEnv(config *hst.Config, wantEnv map[string]string, next extraCheckParamsFunc) extraCheckParamsFunc {
- want := make(map[string]string, len(wantEnv)+len(config.Container.Env))
- maps.Copy(want, wantEnv)
- maps.Copy(want, config.Container.Env)
- return func(t *testing.T, state *outcomeStateParams) {
- if !maps.Equal(state.env, want) {
- t.Errorf("toContainer: env = %#v, want %#v", state.env, want)
- }
-
- if next != nil {
- next(t, state)
- }
- }
-}
-
-// opBehaviourTestCase checks outcomeOp behaviour against outcomeStateSys and outcomeStateParams.
-type opBehaviourTestCase struct {
- name string
- // newOp returns a new instance of outcomeOp under testing that is safe to clobber.
- newOp func(isShim, clearUnexported bool) outcomeOp
- // newConfig returns a new instance of [hst.Config] that is checked not to be clobbered by outcomeOp.
- newConfig func() *hst.Config
-
- // pStateSys is called before outcomeOp.toSystem to prepare outcomeStateSys.
- pStateSys pStateSysFunc
- // toSystem are expected syscallDispatcher calls during outcomeOp.toSystem.
- toSystem []stub.Call
- // wantSys is the expected [system.I] state after outcomeOp.toSystem.
- wantSys *system.I
- // extraCheckSys is called after outcomeOp.toSystem to check the state of outcomeStateSys.
- extraCheckSys extraCheckSysFunc
- // wantErrSystem is the expected error value returned by outcomeOp.toSystem.
- // Further testing is skipped if not nil.
- wantErrSystem error
-
- // pStateContainer is called before outcomeOp.toContainer to prepare outcomeStateParams.
- pStateContainer pStateContainerFunc
- // toContainer are expected syscallDispatcher calls during outcomeOp.toContainer.
- toContainer []stub.Call
- // wantParams is the expected [container.Params] after outcomeOp.toContainer.
- wantParams *container.Params
- // extraCheckParams is called after outcomeOp.toContainer to check the state of outcomeStateParams.
- extraCheckParams extraCheckParamsFunc
- // wantErrContainer is the expected error value returned by outcomeOp.toContainer.
- wantErrContainer error
-}
-
-// checkOpBehaviour runs a slice of opBehaviourTestCase.
-func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
- t.Helper()
-
- wantNewState := []stub.Call{
- // newOutcomeState
- call("getpid", stub.ExpectArgs{}, 0xdead, nil),
- call("isVerbose", stub.ExpectArgs{}, true, nil),
- call("mustHsuPath", stub.ExpectArgs{}, m(container.Nonexistent), nil),
- call("cmdOutput", stub.ExpectArgs{container.Nonexistent, os.Stderr, []string{}, "/"}, []byte("0"), nil),
- call("tempdir", stub.ExpectArgs{}, container.Nonexistent+"/tmp", nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_RUNTIME_DIR"}, wantRuntimePath, nil),
- call("getuid", stub.ExpectArgs{}, 1000, nil),
- call("getgid", stub.ExpectArgs{}, 100, nil),
-
- // populateLocal
- call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{
- m(container.Nonexistent + "/tmp/hakurei.0"),
- m(container.Nonexistent + "/xdg_runtime_dir/hakurei"),
- }}, nil, nil),
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Helper()
- t.Parallel()
-
- wantCallsFull := slices.Concat(wantNewState, tc.toSystem, []stub.Call{{Name: stub.CallSeparator}})
- if tc.wantErrSystem == nil {
- wantCallsFull = append(wantCallsFull, slices.Concat(wantNewState, tc.toContainer)...)
- }
-
- wantConfig := tc.newConfig()
- k := &kstub{panicDispatcher{}, stub.New(t,
- func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} },
- stub.Expect{Calls: wantCallsFull},
- )}
- defer stub.HandleExit(t)
-
- {
- config := tc.newConfig()
- s := newOutcomeState(k, k, &checkExpectInstanceId, config, &Hsu{k: k})
- if err := s.populateLocal(k, k); err != nil {
- t.Fatalf("populateLocal: error = %v", err)
- }
- stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid))
- if tc.pStateSys != nil {
- tc.pStateSys(stateSys)
- }
- op := tc.newOp(false, true)
-
- if err := op.toSystem(stateSys); !reflect.DeepEqual(err, tc.wantErrSystem) {
- t.Fatalf("toSystem: error = %#v, want %#v", err, tc.wantErrSystem)
- }
- k.Expects(stub.CallSeparator)
- if !reflect.DeepEqual(config, wantConfig) {
- t.Errorf("toSystem clobbered config: %#v, want %#v", config, wantConfig)
- }
-
- if tc.wantErrSystem != nil {
- goto out
- }
-
- if !stateSys.sys.Equal(tc.wantSys) {
- t.Errorf("toSystem: %#v, want %#v", stateSys.sys, tc.wantSys)
- }
- if tc.extraCheckSys != nil {
- tc.extraCheckSys(t, stateSys)
- }
- if wantOpSys := tc.newOp(true, false); !reflect.DeepEqual(op, wantOpSys) {
- t.Errorf("toSystem: op = %#v, want %#v", op, wantOpSys)
- }
- }
-
- {
- config := tc.newConfig()
- s := newOutcomeState(k, k, &checkExpectInstanceId, config, &Hsu{k: k})
- stateParams := s.newParams()
- if err := s.populateLocal(k, k); err != nil {
- t.Fatalf("populateLocal: error = %v", err)
- }
- if tc.pStateContainer != nil {
- tc.pStateContainer(stateParams)
- }
- op := tc.newOp(true, true)
-
- if err := op.toContainer(stateParams); !reflect.DeepEqual(err, tc.wantErrContainer) {
- t.Fatalf("toContainer: error = %#v, want %#v", err, tc.wantErrContainer)
- }
-
- if tc.wantErrContainer != nil {
- goto out
- }
-
- if !reflect.DeepEqual(stateParams.params, tc.wantParams) {
- t.Errorf("toContainer:\n%s\nwant\n%s", mustMarshal(stateParams.params), mustMarshal(tc.wantParams))
- }
- if tc.extraCheckParams != nil {
- tc.extraCheckParams(t, stateParams)
- }
- }
-
- out:
- k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
- count := k.Pos() - 1 // separator
- if count-len(wantNewState) < len(tc.toSystem) {
- t.Errorf("toSystem: %d calls, want %d", count-len(wantNewState), len(tc.toSystem))
- } else {
- t.Errorf("toContainer: %d calls, want %d", count-len(tc.toSystem)-2*len(wantNewState), len(tc.toContainer))
- }
- })
- })
- }
-}
-
-func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid) }
-
-// simpleTestCase is a simple freeform test case utilising kstub.
-type simpleTestCase struct {
- name string
- f func(k *kstub) error
- // want are expected syscallDispatcher calls during f.
- want stub.Expect
- // wantErr is the expected error value returned by f.
- wantErr error
-}
-
-// checkSimple runs a slice of simpleTestCase.
-func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
- t.Helper()
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Helper()
- t.Parallel()
-
- defer stub.HandleExit(t)
- k := &kstub{panicDispatcher{}, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} }, tc.want)}
- if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
- t.Errorf("%s: error = %#v, want %#v", fname, err, tc.wantErr)
- }
- k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
- t.Helper()
-
- t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
- })
- })
- }
-}
-
-// kstub partially implements syscallDispatcher via [stub.Stub].
-type kstub struct {
- panicDispatcher
- *stub.Stub[syscallDispatcher]
-}
-
-func (k *kstub) new(f func(k syscallDispatcher, msg message.Msg)) {
- k.New(func(k syscallDispatcher) { f(k, k.(*kstub)) })
-}
-
-func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
-func (k *kstub) getuid() int { k.Helper(); return k.Expects("getuid").Ret.(int) }
-func (k *kstub) getgid() int { k.Helper(); return k.Expects("getgid").Ret.(int) }
-func (k *kstub) lookupEnv(key string) (string, bool) {
- k.Helper()
- expect := k.Expects("lookupEnv")
- if expect.Error(
- stub.CheckArg(k.Stub, "key", key, 0)) != nil {
- k.FailNow()
- }
- if expect.Ret == nil {
- return "\x00", false
- }
- return expect.Ret.(string), true
-}
-func (k *kstub) stat(name string) (os.FileInfo, error) {
- k.Helper()
- expect := k.Expects("stat")
- return expect.Ret.(os.FileInfo), expect.Error(
- stub.CheckArg(k.Stub, "name", name, 0))
-}
-func (k *kstub) open(name string) (osFile, error) {
- k.Helper()
- expect := k.Expects("open")
- return expect.Ret.(osFile), expect.Error(
- stub.CheckArg(k.Stub, "name", name, 0))
-}
-func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
- k.Helper()
- expect := k.Expects("readdir")
- return expect.Ret.([]os.DirEntry), expect.Error(
- stub.CheckArg(k.Stub, "name", name, 0))
-}
-func (k *kstub) tempdir() string { k.Helper(); return k.Expects("tempdir").Ret.(string) }
-func (k *kstub) evalSymlinks(path string) (string, error) {
- k.Helper()
- expect := k.Expects("evalSymlinks")
- return expect.Ret.(string), expect.Error(
- stub.CheckArg(k.Stub, "path", path, 0))
-}
-
-func (k *kstub) prctl(op, arg2, arg3 uintptr) error {
- k.Helper()
- return k.Expects("prctl").Error(
- stub.CheckArg(k.Stub, "op", op, 0),
- stub.CheckArg(k.Stub, "arg2", arg2, 1),
- stub.CheckArg(k.Stub, "arg3", arg3, 2))
-}
-
-func (k *kstub) setDumpable(dumpable uintptr) error {
- k.Helper()
- return k.Expects("setDumpable").Error(
- stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
-}
-
-func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
- k.Helper()
- expect := k.Expects("receive")
- reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
- if expect.Args[2] != nil {
- *fdp = expect.Args[2].(uintptr)
- }
- return func() error { return k.Expects("closeReceive").Err }, expect.Error(
- stub.CheckArg(k.Stub, "key", key, 0))
-}
-
-func (k *kstub) expectCheckContainer(expect *stub.Call, z *container.Container) error {
- k.Helper()
- err := expect.Error(
- stub.CheckArgReflect(k.Stub, "params", &z.Params, 0))
- if err != nil {
- k.Errorf("params:\n%s\n%s", mustMarshal(&z.Params), mustMarshal(expect.Args[0]))
- }
- return err
-}
-
-func (k *kstub) containerStart(z *container.Container) error {
- k.Helper()
- return k.expectCheckContainer(k.Expects("containerStart"), z)
-}
-func (k *kstub) containerServe(z *container.Container) error {
- k.Helper()
- return k.expectCheckContainer(k.Expects("containerServe"), z)
-}
-func (k *kstub) containerWait(z *container.Container) error {
- k.Helper()
- return k.expectCheckContainer(k.Expects("containerWait"), z)
-}
-
-func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
- k.Helper()
- return k.Expects("seccompLoad").Error(
- stub.CheckArgReflect(k.Stub, "rules", rules, 0),
- stub.CheckArg(k.Stub, "flags", flags, 1))
-}
-
-func (k *kstub) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
- k.Helper()
- expect := k.Expects("cmdOutput")
- return expect.Ret.([]byte), expect.Error(
- stub.CheckArg(k.Stub, "cmd.Path", cmd.Path, 0),
- stub.CheckArgReflect(k.Stub, "cmd.Stderr", cmd.Stderr, 1),
- stub.CheckArgReflect(k.Stub, "cmd.Env", cmd.Env, 2),
- stub.CheckArg(k.Stub, "cmd.Dir", cmd.Dir, 3))
-}
-
-func (k *kstub) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
- k.Helper()
- if k.Expects("notifyContext").Error(
- stub.CheckArgReflect(k.Stub, "parent", parent, 0),
- stub.CheckArgReflect(k.Stub, "signals", signals, 1)) != nil {
- k.FailNow()
- }
- return k.Context(), func() { k.Helper(); k.Expects("notifyContextStop") }
-}
-
-func (k *kstub) mustHsuPath() *check.Absolute {
- k.Helper()
- return k.Expects("mustHsuPath").Ret.(*check.Absolute)
-}
-
-func (k *kstub) dbusAddress() (session, system string) {
- k.Helper()
- ret := k.Expects("dbusAddress").Ret.([2]string)
- return ret[0], ret[1]
-}
-
-// stubTrackReader embeds kstub but switches the underlying [stub.Stub] index to sub on its first Read.
-// The resulting kstub does not share any state with the instance passed to the instrumented goroutine.
-// Therefore, any method making use of such must not be called.
-type stubTrackReader struct {
- sub int
- subOnce sync.Once
-
- *kstub
-}
-
-func (r *stubTrackReader) Read(p []byte) (n int, err error) {
- r.subOnce.Do(func() {
- subVal := reflect.ValueOf(r.kstub.Stub).Elem().FieldByName("sub")
- r.kstub = &kstub{panicDispatcher{}, reflect.
- NewAt(subVal.Type(), unsafe.Pointer(subVal.UnsafeAddr())).Elem().
- Interface().([]*stub.Stub[syscallDispatcher])[r.sub]}
- })
-
- return r.kstub.Read(p)
-}
-
-func (k *kstub) setupContSignal(pid int) (io.ReadCloser, func(), error) {
- k.Helper()
- expect := k.Expects("setupContSignal")
- return &stubTrackReader{sub: expect.Ret.(int), kstub: k}, func() { k.Expects("wKeepAlive") }, expect.Error(
- stub.CheckArg(k.Stub, "pid", pid, 0))
-}
-
-func (k *kstub) getMsg() message.Msg { k.Helper(); k.Expects("getMsg"); return k }
-
-func (k *kstub) Close() error { k.Helper(); return k.Expects("rcClose").Err }
-func (k *kstub) Read(p []byte) (n int, err error) {
- k.Helper()
- expect := k.Expects("rcRead")
-
- // special case to terminate exit outcomes goroutine
- // to proceed with further testing of the entrypoint
- if expect.Ret == nil {
- panic(stub.PanicExit)
- }
-
- return copy(p, expect.Ret.([]byte)), expect.Err
-}
-
-func (k *kstub) GetLogger() *log.Logger { k.Helper(); return k.Expects("getLogger").Ret.(*log.Logger) }
-func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
-func (k *kstub) SwapVerbose(verbose bool) bool {
- k.Helper()
- expect := k.Expects("swapVerbose")
- if expect.Error(
- stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
- k.FailNow()
- }
- return expect.Ret.(bool)
-}
-
-// ignoreValue marks a value to be ignored by the test suite.
-type ignoreValue struct{}
-
-func (k *kstub) Verbose(v ...any) {
- k.Helper()
- expect := k.Expects("verbose")
-
- // translate ignores in v
- if want, ok := expect.Args[0].([]any); ok && len(v) == len(want) {
- for i, a := range want {
- if _, ok = a.(ignoreValue); ok {
- v[i] = ignoreValue{}
- }
- }
- }
-
- if expect.Error(
- stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
- k.FailNow()
- }
-}
-
-func (k *kstub) Verbosef(format string, v ...any) {
- k.Helper()
- if k.Expects("verbosef").Error(
- stub.CheckArg(k.Stub, "format", format, 0),
- stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
- k.FailNow()
- }
-}
-
-func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
-func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
-func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }
-
-// stubOsFile partially implements osFile.
-type stubOsFile struct {
- closeErr error
-
- io.Reader
- io.Writer
-}
-
-func (f *stubOsFile) Close() error { return f.closeErr }
-func (f *stubOsFile) Name() string { panic("unreachable") }
-func (f *stubOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
-
-// stubFi partially implements [os.FileInfo]. Can be passed as nil to assert all methods unreachable.
-type stubFi struct {
- size int64
- mode os.FileMode
- isDir bool
-}
-
-func (fi *stubFi) Name() string { panic("unreachable") }
-func (fi *stubFi) ModTime() time.Time { panic("unreachable") }
-func (fi *stubFi) Sys() any { panic("unreachable") }
-func (fi *stubFi) Size() int64 { return fi.size }
-func (fi *stubFi) Mode() os.FileMode { return fi.mode }
-func (fi *stubFi) IsDir() bool { return fi.isDir }
-
-// stubDir returns a slice of [os.DirEntry] with only their Name method implemented.
-func stubDir(names ...string) []os.DirEntry {
- d := make([]os.DirEntry, len(names))
- for i, name := range names {
- d[i] = nameDentry(name)
- }
- return d
-}
-
-// nameDentry implements the Name method on [os.DirEntry].
-type nameDentry string
-
-func (e nameDentry) Name() string { return string(e) }
-func (nameDentry) IsDir() bool { panic("unreachable") }
-func (nameDentry) Type() fs.FileMode { panic("unreachable") }
-func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
-
-// errorReader implements [io.Reader] that unconditionally returns -1, val.
-type errorReader struct{ val error }
-
-func (r errorReader) Read([]byte) (int, error) { return -1, r.val }
-
-// panicMsgContext implements [message.Msg] and [context.Context] with methods wrapping panic.
-// This should be assigned to test cases to be checked against.
-type panicMsgContext struct{}
-
-func (panicMsgContext) GetLogger() *log.Logger { panic("unreachable") }
-func (panicMsgContext) IsVerbose() bool { panic("unreachable") }
-func (panicMsgContext) SwapVerbose(bool) bool { panic("unreachable") }
-func (panicMsgContext) Verbose(...any) { panic("unreachable") }
-func (panicMsgContext) Verbosef(string, ...any) { panic("unreachable") }
-func (panicMsgContext) Suspend() bool { panic("unreachable") }
-func (panicMsgContext) Resume() bool { panic("unreachable") }
-func (panicMsgContext) BeforeExit() { panic("unreachable") }
-
-func (panicMsgContext) Deadline() (time.Time, bool) { panic("unreachable") }
-func (panicMsgContext) Done() <-chan struct{} { panic("unreachable") }
-func (panicMsgContext) Err() error { panic("unreachable") }
-func (panicMsgContext) Value(any) any { panic("unreachable") }
-
-// panicDispatcher implements syscallDispatcher with methods wrapping panic.
-// This type is meant to be embedded in partial syscallDispatcher implementations.
-type panicDispatcher struct{}
-
-func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
-func (panicDispatcher) getpid() int { panic("unreachable") }
-func (panicDispatcher) getuid() int { panic("unreachable") }
-func (panicDispatcher) getgid() int { panic("unreachable") }
-func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
-func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
-func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
-func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
-func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
-func (panicDispatcher) tempdir() string { panic("unreachable") }
-func (panicDispatcher) exit(int) { panic("unreachable") }
-func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
-func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
-func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
-func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
-func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
-func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
-func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
-func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") }
-func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
-func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
-func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
-func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
-func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
-func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
-func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
-func (panicDispatcher) fatal(...any) { panic("unreachable") }
-func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
-
-func (panicDispatcher) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {
- panic("unreachable")
-}
-func (panicDispatcher) seccompLoad([]seccomp.NativeRule, seccomp.ExportFlag) error {
- panic("unreachable")
-}
diff --git a/internal/app/finalise.go b/internal/app/finalise.go
deleted file mode 100644
index 6cb4f586..00000000
--- a/internal/app/finalise.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package app
-
-import (
- "context"
- "errors"
- "fmt"
- "os"
- "os/user"
- "sync/atomic"
-
- "hakurei.app/hst"
- "hakurei.app/message"
- "hakurei.app/system"
-)
-
-func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
-func newWithMessageError(msg string, err error) error {
- return &hst.AppError{Step: "finalise", Err: err, Msg: msg}
-}
-
-// An outcome is the runnable state of a hakurei container via [hst.Config].
-type outcome struct {
- // Supplementary group ids. Populated during finalise.
- supp []string
- // Resolved priv side operating system interactions. Populated during finalise.
- sys *system.I
- // Transmitted to shim. Populated during finalise.
- state *outcomeState
- // Kept for saving to [state].
- config *hst.Config
-
- // Whether the current process is in outcome.main.
- active atomic.Bool
-
- ctx context.Context
- syscallDispatcher
-}
-
-func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
- if ctx == nil || id == nil {
- // unreachable
- panic("invalid call to finalise")
- }
- if k.ctx != nil || k.sys != nil || k.state != nil {
- // unreachable
- panic("attempting to finalise twice")
- }
- k.ctx = ctx
-
- if err := config.Validate(); err != nil {
- return err
- }
-
- // hsu expects numerical group ids
- supp := make([]string, len(config.Groups))
- for i, name := range config.Groups {
- 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)
- } else {
- return &hst.AppError{Step: "look up group by name", Err: err}
- }
- } else {
- supp[i] = gid
- }
- }
-
- // early validation complete at this point
- s := newOutcomeState(k.syscallDispatcher, msg, id, config, &Hsu{k: k})
- if err := s.populateLocal(k.syscallDispatcher, msg); err != nil {
- return err
- }
-
- sys := system.New(k.ctx, msg, s.uid.unwrap())
- if err := s.newSys(config, sys).toSystem(); err != nil {
- return err
- }
-
- k.sys = sys
- k.supp = supp
- k.state = s
- k.config = config
- return nil
-}
diff --git a/internal/app/hsu.go b/internal/app/hsu.go
deleted file mode 100644
index 1286249a..00000000
--- a/internal/app/hsu.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package app
-
-import (
- "errors"
- "fmt"
- "log"
- "os"
- "os/exec"
- "strconv"
- "sync"
-
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/message"
-)
-
-// Hsu caches responses from cmd/hsu.
-type Hsu struct {
- idOnce sync.Once
- idErr error
- id int
-
- kOnce sync.Once
-
- // msg is not populated
- 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 := h.k.mustHsuPath().String()
-
- cmd := exec.Command(hsuPath)
- cmd.Path = hsuPath
- cmd.Stderr = os.Stderr // pass through fatal messages
- cmd.Env = make([]string, 0)
- cmd.Dir = fhs.Root
- var (
- p []byte
- exitError *exec.ExitError
- )
-
- const step = "obtain uid from hsu"
- 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"}
- }
- } else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
- // hsu prints an error message in this case
- h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
- } else if errors.Is(h.idErr, os.ErrNotExist) {
- h.idErr = &hst.AppError{Step: step, Err: h.idErr,
- Msg: fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
- }
- })
-
- return h.id, h.idErr
-}
-
-// MustID calls [Hsu.ID] and terminates on error.
-func (h *Hsu) MustID(msg message.Msg) int {
- id, err := h.ID()
- if err == nil {
- return id
- }
-
- const fallback = "cannot retrieve user id from setuid wrapper:"
- if errors.Is(err, ErrHsuAccess) {
- if msg != nil {
- msg.Verbose("*"+fallback, err)
- }
- os.Exit(1)
- return -0xdeadbeef // not reached
- } else if m, ok := message.GetMessage(err); ok {
- log.Fatal(m)
- return -0xdeadbeef // not reached
- } else {
- log.Fatalln(fallback, err)
- return -0xdeadbeef // not reached
- }
-}
-
-// HsuUid returns target uid for the stable hsu uid format.
-// No bounds check is performed, a value retrieved by [Hsu] is expected.
-func HsuUid(id, identity int) int { return 1000000 + id*10000 + identity }
diff --git a/internal/app/hsu_test.go b/internal/app/hsu_test.go
deleted file mode 100644
index fad724da..00000000
--- a/internal/app/hsu_test.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package app
-
-import (
- "os"
- "os/exec"
- "reflect"
- "strconv"
- "syscall"
- "testing"
- "unsafe"
-
- "hakurei.app/container/stub"
- "hakurei.app/hst"
-)
-
-func TestHsu(t *testing.T) {
- t.Parallel()
-
- t.Run("ensure dispatcher", func(t *testing.T) {
- hsu := new(Hsu)
- hsu.ensureDispatcher()
-
- k := direct{}
- if !reflect.DeepEqual(hsu.k, k) {
- t.Errorf("ensureDispatcher: k = %#v, want %#v", hsu.k, k)
- }
- })
-
- fCheckID := func(k *kstub) error {
- hsu := &Hsu{k: k}
- id, err := hsu.ID()
- k.Verbose(id)
- if id0, err0 := hsu.ID(); id0 != id || !reflect.DeepEqual(err0, err) {
- t.Fatalf("ID: id0 = %d, err0 = %#v, id = %d, err = %#v", id0, err0, id, err)
- }
- return err
- }
-
- checkSimple(t, "Hsu.ID", []simpleTestCase{
- {"hsu nonexistent", fCheckID, stub.Expect{Calls: []stub.Call{
- call("mustHsuPath", stub.ExpectArgs{}, m("/run/wrappers/bin/hsu"), nil),
- call("cmdOutput", stub.ExpectArgs{"/run/wrappers/bin/hsu", os.Stderr, []string{}, "/"}, ([]byte)(nil), os.ErrNotExist),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{
- Step: "obtain uid from hsu",
- Err: os.ErrNotExist,
- Msg: "the setuid helper is missing: /run/wrappers/bin/hsu",
- }},
-
- {"access", fCheckID, stub.Expect{Calls: []stub.Call{
- call("mustHsuPath", stub.ExpectArgs{}, m("/run/wrappers/bin/hsu"), nil),
- call("cmdOutput", stub.ExpectArgs{"/run/wrappers/bin/hsu", os.Stderr, []string{}, "/"}, ([]byte)(nil), makeExitError(1<<8)),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{
- Step: "obtain uid from hsu",
- Err: ErrHsuAccess,
- }},
-
- {"invalid output", fCheckID, stub.Expect{Calls: []stub.Call{
- call("mustHsuPath", stub.ExpectArgs{}, m("/run/wrappers/bin/hsu"), nil),
- call("cmdOutput", stub.ExpectArgs{"/run/wrappers/bin/hsu", os.Stderr, []string{}, "/"}, []byte{0}, nil),
- call("verbose", stub.ExpectArgs{[]any{0}}, nil, nil),
- }}, &hst.AppError{
- Step: "obtain uid from hsu",
- Err: &strconv.NumError{Func: "Atoi", Num: "\x00", Err: strconv.ErrSyntax},
- Msg: "invalid uid string from hsu",
- }},
-
- {"success", fCheckID, stub.Expect{Calls: []stub.Call{
- call("mustHsuPath", stub.ExpectArgs{}, m("/run/wrappers/bin/hsu"), nil),
- call("cmdOutput", stub.ExpectArgs{"/run/wrappers/bin/hsu", os.Stderr, []string{}, "/"}, []byte{'0'}, nil),
- call("verbose", stub.ExpectArgs{[]any{0}}, nil, nil),
- }}, nil},
- })
-}
-
-// makeExitError populates syscall.WaitStatus in an [exec.ExitError].
-// Do not reuse this function in a cross-platform package.
-func makeExitError(status syscall.WaitStatus) error {
- ps := new(os.ProcessState)
- statusV := reflect.ValueOf(ps).Elem().FieldByName("status")
- *reflect.NewAt(statusV.Type(), unsafe.Pointer(statusV.UnsafeAddr())).Interface().(*syscall.WaitStatus) = status
- return &exec.ExitError{ProcessState: ps}
-}
diff --git a/internal/app/outcome.go b/internal/app/outcome.go
deleted file mode 100644
index 59cdfbd8..00000000
--- a/internal/app/outcome.go
+++ /dev/null
@@ -1,302 +0,0 @@
-package app
-
-import (
- "errors"
- "maps"
- "strconv"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/internal/env"
- "hakurei.app/message"
- "hakurei.app/system"
- "hakurei.app/system/acl"
-)
-
-// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
-// It should be large enough to fit all insertions by outcomeOp.toContainer.
-const envAllocSize = 1 << 6
-
-func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
-
-// stringPair stores a value and its string representation.
-type stringPair[T comparable] struct {
- v T
- s string
-}
-
-func (s *stringPair[T]) unwrap() T { return s.v }
-func (s *stringPair[T]) String() string { return s.s }
-
-// outcomeState is copied to the shim process and available while applying outcomeOp.
-// This is transmitted from the priv side to the shim, so exported fields should be kept to a minimum.
-type outcomeState struct {
- // Params only used by the shim process. Populated by populateEarly.
- Shim *shimParams
-
- // Generated and accounted for by the caller.
- ID *hst.ID
- // Copied from ID.
- id *stringPair[hst.ID]
-
- // Copied from the [hst.Config] field of the same name.
- Identity int
- // Copied from Identity.
- identity *stringPair[int]
- // Returned by [Hsu.MustID].
- UserID int
- // Target init namespace uid resolved from UserID and identity.
- uid *stringPair[int]
-
- // Included as part of [hst.Config], transmitted as-is unless permissive defaults.
- Container *hst.ContainerConfig
-
- // Mapped credentials within container user namespace.
- Mapuid, Mapgid int
- // Copied from their respective exported values.
- mapuid, mapgid *stringPair[int]
-
- // Copied from [EnvPaths] per-process.
- sc hst.Paths
- *env.Paths
-
- // Copied via populateLocal.
- k syscallDispatcher
- // Copied via populateLocal.
- msg message.Msg
-}
-
-// valid checks outcomeState to be safe for use with outcomeOp.
-func (s *outcomeState) valid() bool {
- return s != nil &&
- s.Shim.valid() &&
- s.ID != nil &&
- s.Container != nil &&
- s.Paths != nil
-}
-
-// newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher.
-func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *hst.Config, hsu *Hsu) *outcomeState {
- s := outcomeState{
- Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()},
- ID: id,
- Identity: config.Identity,
- UserID: hsu.MustID(msg),
- Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }),
- Container: config.Container,
- }
-
- // enforce bounds and default early
- if s.Container.WaitDelay < 0 {
- s.Shim.WaitDelay = 0
- } else if s.Container.WaitDelay == 0 {
- s.Shim.WaitDelay = hst.WaitDelayDefault
- } else if s.Container.WaitDelay > hst.WaitDelayMax {
- s.Shim.WaitDelay = hst.WaitDelayMax
- } else {
- s.Shim.WaitDelay = s.Container.WaitDelay
- }
-
- if s.Container.Flags&hst.FMapRealUID != 0 {
- s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
- } else {
- s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
- }
-
- return &s
-}
-
-// populateLocal populates unexported fields from transmitted exported fields.
-// These fields are cheaper to recompute per-process.
-func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error {
- if !s.valid() || k == nil || msg == nil {
- return newWithMessage("impossible outcome state reached")
- }
-
- if s.k != nil || s.msg != nil {
- panic("attempting to call populateLocal twice")
- }
- s.k = k
- s.msg = msg
-
- s.id = &stringPair[hst.ID]{*s.ID, s.ID.String()}
-
- s.Copy(&s.sc, s.UserID)
- msg.Verbosef("process share directory at %q, runtime directory at %q", s.sc.SharePath, s.sc.RunDirPath)
-
- s.identity = newInt(s.Identity)
- s.mapuid, s.mapgid = newInt(s.Mapuid), newInt(s.Mapgid)
- s.uid = newInt(HsuUid(s.UserID, s.identity.unwrap()))
-
- return nil
-}
-
-// instancePath returns a path formatted for outcomeStateSys.instance.
-// This method must only be called from outcomeOp.toContainer if
-// outcomeOp.toSystem has already called outcomeStateSys.instance.
-func (s *outcomeState) instancePath() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) }
-
-// runtimePath returns a path formatted for outcomeStateSys.runtime.
-// This method must only be called from outcomeOp.toContainer if
-// outcomeOp.toSystem has already called outcomeStateSys.runtime.
-func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) }
-
-// outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only.
-// Implementations of outcomeOp must not access fields other than sys unless explicitly stated.
-type outcomeStateSys struct {
- // Whether XDG_RUNTIME_DIR is used post hsu.
- useRuntimeDir bool
- // Process-specific directory in TMPDIR, nil if unused.
- sharePath *check.Absolute
- // Process-specific directory in XDG_RUNTIME_DIR, nil if unused.
- runtimeSharePath *check.Absolute
-
- // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
- appId string
- // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
- et hst.Enablement
-
- // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
- directWayland bool
- // Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
- extraPerms []hst.ExtraPermConfig
- // Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
- sessionBus, systemBus *hst.BusConfig
-
- sys *system.I
- *outcomeState
-}
-
-// newSys returns the address of a new outcomeStateSys embedding the current outcomeState.
-func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
- return &outcomeStateSys{
- appId: config.ID, et: config.Enablements.Unwrap(),
- directWayland: config.DirectWayland, extraPerms: config.ExtraPerms,
- sessionBus: config.SessionBus, systemBus: config.SystemBus,
- sys: sys, outcomeState: s,
- }
-}
-
-// newParams returns the address of a new outcomeStateParams embedding the current outcomeState.
-func (s *outcomeState) newParams() *outcomeStateParams {
- stateParams := outcomeStateParams{params: new(container.Params), outcomeState: s}
- if s.Container.Env == nil {
- stateParams.env = make(map[string]string, envAllocSize)
- } else {
- stateParams.env = maps.Clone(s.Container.Env)
- }
- return &stateParams
-}
-
-// ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required.
-func (state *outcomeStateSys) ensureRuntimeDir() {
- if state.useRuntimeDir {
- return
- }
- state.useRuntimeDir = true
- state.sys.Ensure(state.sc.RunDirPath, 0700)
- state.sys.UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
- state.sys.Ensure(state.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
- state.sys.UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute)
-}
-
-// instance returns the pathname to a process-specific directory within TMPDIR.
-// This directory must only hold entries bound to [system.Process].
-func (state *outcomeStateSys) instance() *check.Absolute {
- if state.sharePath != nil {
- return state.sharePath
- }
- state.sharePath = state.instancePath()
- state.sys.Ephemeral(system.Process, state.sharePath, 0711)
- return state.sharePath
-}
-
-// runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR.
-// This directory must only hold entries bound to [system.Process].
-func (state *outcomeStateSys) runtime() *check.Absolute {
- if state.runtimeSharePath != nil {
- return state.runtimeSharePath
- }
- state.ensureRuntimeDir()
- state.runtimeSharePath = state.runtimePath()
- state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700)
- state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute)
- return state.runtimeSharePath
-}
-
-// outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only.
-type outcomeStateParams struct {
- // Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used.
- params *container.Params
- // Collapsed into the Env slice in [container.Params] by the final outcomeOp.
- env map[string]string
-
- // Filesystems with the optional root sliced off if present. Populated by spParamsOp.
- // Safe for use by spFilesystemOp.
- filesystem []hst.FilesystemConfigJSON
-
- // Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid.
- // Populated by spRuntimeOp.
- runtimeDir *check.Absolute
-
- as hst.ApplyState
- *outcomeState
-}
-
-// errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission.
-var errNotEnabled = errors.New("op not enabled in the configuration")
-
-// An outcomeOp inflicts an outcome on [system.I] and contains enough information to
-// inflict it on [container.Params] in a separate process.
-// An implementation of outcomeOp must store cross-process states in exported fields only.
-type outcomeOp interface {
- // toSystem inflicts the current outcome on [system.I] in the priv side process.
- toSystem(state *outcomeStateSys) error
-
- // toContainer inflicts the current outcome on [container.Params] in the shim process.
- // The implementation must not write to the Env field of [container.Params] as it will be overwritten
- // by flattened env map.
- toContainer(state *outcomeStateParams) error
-}
-
-// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops.
-// This function assumes the caller has already called the Validate method on [hst.Config]
-// and checked that it returns nil.
-func (state *outcomeStateSys) toSystem() error {
- if state.Shim == nil || state.Shim.Ops != nil {
- return newWithMessage("invalid ops state reached")
- }
-
- ops := [...]outcomeOp{
- // must run first
- &spParamsOp{},
-
- &spRuntimeOp{},
- spTmpdirOp{},
- spAccountOp{},
-
- // optional via enablements
- &spWaylandOp{},
- &spX11Op{},
- &spPulseOp{},
- &spDBusOp{},
-
- // must run last
- &spFilesystemOp{},
- }
-
- state.Shim.Ops = make([]outcomeOp, 0, len(ops))
- for _, op := range ops {
- if err := op.toSystem(state); err != nil {
- // this error is used internally to exclude this outcomeOp from transmission
- if errors.Is(err, errNotEnabled) {
- continue
- }
-
- return err
- }
- state.Shim.Ops = append(state.Shim.Ops, op)
- }
- return nil
-}
diff --git a/internal/app/outcome_test.go b/internal/app/outcome_test.go
deleted file mode 100644
index 5ea3ff99..00000000
--- a/internal/app/outcome_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package app
-
-import (
- "testing"
-
- "hakurei.app/hst"
- "hakurei.app/internal/env"
-)
-
-func TestOutcomeStateValid(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- s *outcomeState
- want bool
- }{
- {"nil", nil, false},
- {"zero", new(outcomeState), false},
- {"shim", &outcomeState{Shim: &shimParams{PrivPID: -1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, false},
- {"id", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, false},
- {"container", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Paths: new(env.Paths)}, false},
- {"envpaths", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig)}, false},
- {"valid", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, true},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- if got := tc.s.valid(); got != tc.want {
- t.Errorf("valid: %v, want %v", got, tc.want)
- }
- })
- }
-}
diff --git a/internal/app/process.go b/internal/app/process.go
deleted file mode 100644
index 7873109a..00000000
--- a/internal/app/process.go
+++ /dev/null
@@ -1,321 +0,0 @@
-package app
-
-import (
- "context"
- "encoding/gob"
- "errors"
- "log"
- "os"
- "os/exec"
- "strconv"
- "strings"
- "syscall"
- "time"
-
- "hakurei.app/container"
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/internal"
- "hakurei.app/internal/app/state"
- "hakurei.app/message"
- "hakurei.app/system"
-)
-
-// Duration to wait for shim to exit on top of container WaitDelay.
-const shimWaitTimeout = 5 * time.Second
-
-// mainState holds persistent state bound to outcome.main.
-type mainState struct {
- // done is whether beforeExit has been called already.
- done bool
-
- // Time is the exact point in time where the process was created.
- // Location must be set to UTC.
- //
- // Time is nil if no process was ever created.
- Time *time.Time
-
- store state.Store
- cancel context.CancelFunc
- cmd *exec.Cmd
- cmdWait chan error
-
- k *outcome
- message.Msg
- uintptr
-}
-
-const (
- // mainNeedsRevert indicates the call to Commit has succeeded.
- mainNeedsRevert uintptr = 1 << iota
- // mainNeedsDestroy indicates the instance state entry is present in the store.
- mainNeedsDestroy
-)
-
-// beforeExit must be called immediately before a call to [os.Exit].
-func (ms mainState) beforeExit(isFault bool) {
- if ms.done {
- panic("attempting to call beforeExit twice")
- }
- ms.done = true
- defer ms.BeforeExit()
-
- if isFault && ms.cancel != nil {
- ms.cancel()
- }
-
- var hasErr bool
- // updates hasErr but does not terminate
- perror := func(err error, message string) {
- hasErr = true
- printMessageError("cannot "+message+":", err)
- }
- exitCode := 1
- defer func() {
- if hasErr {
- os.Exit(exitCode)
- }
- }()
-
- // this also handles wait for a non-fault termination
- if ms.cmd != nil && ms.cmdWait != nil {
- waitDone := make(chan struct{})
-
- // this ties waitDone to ctx with the additional compensated timeout duration
- go func() { <-ms.k.ctx.Done(); time.Sleep(ms.k.state.Shim.WaitDelay + shimWaitTimeout); close(waitDone) }()
-
- select {
- case err := <-ms.cmdWait:
- wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
- if ok {
- if v := wstatus.ExitStatus(); v != 0 {
- hasErr = true
- exitCode = v
- }
- }
-
- if ms.IsVerbose() {
- if !ok {
- if err != nil {
- ms.Verbosef("wait: %v", err)
- }
- } else {
- switch {
- case wstatus.Exited():
- ms.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
-
- case wstatus.CoreDump():
- ms.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
-
- case wstatus.Signaled():
- ms.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
-
- default:
- ms.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
- }
- }
- }
-
- case <-waitDone:
- ms.Resume()
- // this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
- // This is different from the container failing to terminate within its timeout period, as that is enforced
- // by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
- log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
- }
-
- ms.Resume()
- }
-
- if ms.uintptr&mainNeedsRevert != 0 {
- if ok, err := ms.store.Do(ms.k.state.identity.unwrap(), func(c state.Cursor) {
- if ms.uintptr&mainNeedsDestroy != 0 {
- if err := c.Destroy(ms.k.state.id.unwrap()); err != nil {
- perror(err, "destroy state entry")
- }
- }
-
- var rt hst.Enablement
- if states, err := c.Load(); err != nil {
- // it is impossible to continue from this point;
- // revert per-process state here to limit damage
- ec := system.Process
- if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
- var joinError interface {
- Unwrap() []error
- error
- }
- if !errors.As(revertErr, &joinError) || joinError == nil {
- perror(revertErr, "revert system setup")
- } else {
- for _, v := range joinError.Unwrap() {
- perror(v, "revert system setup step")
- }
- }
- }
- perror(err, "load instance states")
- } else {
- ec := system.Process
- if l := len(states); l == 0 {
- ec |= system.User
- } else {
- ms.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
- }
-
- // accumulate enablements of remaining launchers
- for i, s := range states {
- if s.Config != nil {
- rt |= s.Config.Enablements.Unwrap()
- } else {
- log.Printf("state entry %d does not contain config", i)
- }
- }
-
- ec |= rt ^ (hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse)
- if ms.IsVerbose() {
- if ec > 0 {
- ms.Verbose("reverting operations scope", system.TypeString(ec))
- }
- }
-
- if err = ms.k.sys.Revert((*system.Criteria)(&ec)); err != nil {
- perror(err, "revert system setup")
- }
- }
- }); err != nil {
- if ok {
- perror(err, "unlock state store")
- } else {
- perror(err, "open state store")
- }
- }
- } else if ms.uintptr&mainNeedsDestroy != 0 {
- panic("unreachable")
- }
-}
-
-// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
-func (ms mainState) fatal(fallback string, ferr error) {
- printMessageError(fallback, ferr)
- ms.beforeExit(true)
- os.Exit(1)
-}
-
-// main carries out outcome and terminates. main does not return.
-func (k *outcome) main(msg message.Msg) {
- if !k.active.CompareAndSwap(false, true) {
- panic("outcome: attempted to run twice")
- }
-
- if k.ctx == nil || k.sys == nil || k.state == nil {
- panic("outcome: did not finalise")
- }
-
- // read comp value early for early failure
- hsuPath := internal.MustHsuPath()
-
- // ms.beforeExit required beyond this point
- ms := &mainState{Msg: msg, k: k}
-
- if err := k.sys.Commit(); err != nil {
- ms.fatal("cannot commit system setup:", err)
- }
- ms.uintptr |= mainNeedsRevert
- ms.store = state.NewMulti(msg, k.state.sc.RunDirPath)
-
- ctx, cancel := context.WithCancel(k.ctx)
- defer cancel()
- ms.cancel = cancel
-
- ms.cmd = exec.CommandContext(ctx, hsuPath.String())
- ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
- ms.cmd.Dir = fhs.Root // container init enters final working directory
- // shim runs in the same session as monitor; see shim.go for behaviour
- ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
-
- var e *gob.Encoder
- if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
- ms.fatal("cannot create shim setup pipe:", err)
- } else {
- e = encoder
- ms.cmd.Env = []string{
- // passed through to shim by hsu
- shimEnv + "=" + strconv.Itoa(fd),
- // interpreted by hsu
- "HAKUREI_IDENTITY=" + k.state.identity.String(),
- }
- }
-
- if len(k.supp) > 0 {
- msg.Verbosef("attaching supplementary group ids %s", k.supp)
- // interpreted by hsu
- ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.supp, " "))
- }
-
- msg.Verbosef("setuid helper at %s", hsuPath)
- msg.Suspend()
- if err := ms.cmd.Start(); err != nil {
- ms.fatal("cannot start setuid wrapper:", err)
- }
- startTime := time.Now().UTC()
- ms.cmdWait = make(chan error, 1)
- // this ties context back to the life of the process
- go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
- ms.Time = &startTime
-
- // unfortunately the I/O here cannot be directly canceled;
- // the cancellation path leads to fatal in this case so that is fine
- select {
- case err := <-func() (setupErr chan error) {
- setupErr = make(chan error, 1)
- go func() { setupErr <- e.Encode(k.state) }()
- return
- }():
- if err != nil {
- msg.Resume()
- ms.fatal("cannot transmit shim config:", err)
- }
-
- case <-ctx.Done():
- msg.Resume()
- ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
- }
-
- // shim accepted setup payload, create process state
- if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c state.Cursor) {
- if err := c.Save(&hst.State{
- ID: k.state.id.unwrap(),
- PID: os.Getpid(),
- ShimPID: ms.cmd.Process.Pid,
- Config: k.config,
- Time: *ms.Time,
- }); err != nil {
- ms.fatal("cannot save state entry:", err)
- }
- }); err != nil {
- if ok {
- ms.uintptr |= mainNeedsDestroy
- ms.fatal("cannot unlock state store:", err)
- } else {
- ms.fatal("cannot open state store:", err)
- }
- }
- // state in store at this point, destroy defunct state entry on termination
- ms.uintptr |= mainNeedsDestroy
-
- // beforeExit ties shim process to context
- ms.beforeExit(false)
- os.Exit(0)
-}
-
-// printMessageError prints the error message according to [message.GetMessage],
-// or fallback prepended to err if an error message is not available.
-func printMessageError(fallback string, err error) {
- m, ok := message.GetMessage(err)
- if !ok {
- log.Println(fallback, err)
- return
- }
-
- log.Print(m)
-}
diff --git a/internal/app/shim-signal.c b/internal/app/shim-signal.c
deleted file mode 100644
index fc005347..00000000
--- a/internal/app/shim-signal.c
+++ /dev/null
@@ -1,65 +0,0 @@
-#include "shim-signal.h"
-#include <errno.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-
-static pid_t hakurei_shim_param_ppid = -1;
-static int hakurei_shim_fd = -1;
-
-static ssize_t hakurei_shim_write(const void *buf, size_t count) {
- int savedErrno = errno;
- ssize_t ret = write(hakurei_shim_fd, buf, count);
- if (ret == -1 && errno != EAGAIN)
- exit(EXIT_FAILURE);
- errno = savedErrno;
- return ret;
-}
-
-/* see shim_linux.go for handling of the value */
-static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
- if (sig != SIGCONT || si == NULL) {
- /* unreachable */
- hakurei_shim_write("\2", 1);
- return;
- }
-
- if (si->si_pid == hakurei_shim_param_ppid) {
- /* monitor requests shim exit */
- hakurei_shim_write("\0", 1);
- return;
- }
-
- /* unexpected si_pid */
- hakurei_shim_write("\3", 1);
-
- if (getppid() != hakurei_shim_param_ppid)
- /* shim orphaned before monitor delivers a signal */
- hakurei_shim_write("\1", 1);
-}
-
-void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
- if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
- *(int *)NULL = 0; /* unreachable */
-
- struct sigaction new_action = {0}, old_action = {0};
- if (sigaction(SIGCONT, NULL, &old_action) != 0)
- return;
- if (old_action.sa_handler != SIG_DFL) {
- errno = ENOTRECOVERABLE;
- return;
- }
-
- new_action.sa_sigaction = hakurei_shim_sigaction;
- if (sigemptyset(&new_action.sa_mask) != 0)
- return;
- new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
-
- if (sigaction(SIGCONT, &new_action, NULL) != 0)
- return;
-
- errno = 0;
- hakurei_shim_param_ppid = ppid;
- hakurei_shim_fd = fd;
-}
diff --git a/internal/app/shim-signal.h b/internal/app/shim-signal.h
deleted file mode 100644
index bdeae3b0..00000000
--- a/internal/app/shim-signal.h
+++ /dev/null
@@ -1,3 +0,0 @@
-#include <signal.h>
-
-void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);
diff --git a/internal/app/shim.go b/internal/app/shim.go
deleted file mode 100644
index 11affddd..00000000
--- a/internal/app/shim.go
+++ /dev/null
@@ -1,235 +0,0 @@
-package app
-
-import (
- "context"
- "errors"
- "io"
- "log"
- "os"
- "os/exec"
- "runtime"
- "sync/atomic"
- "syscall"
- "time"
-
- "hakurei.app/container"
- "hakurei.app/container/comp"
- "hakurei.app/container/seccomp"
- "hakurei.app/hst"
- "hakurei.app/message"
-)
-
-//#include "shim-signal.h"
-import "C"
-
-// setupContSignal sets up the SIGCONT signal handler for the cross-uid shim exit hack.
-// The signal handler is implemented in C, signals can be processed by reading from the returned reader.
-// The returned function must be called after all signal processing concludes.
-func setupContSignal(pid int) (io.ReadCloser, func(), error) {
- if r, w, err := os.Pipe(); err != nil {
- return nil, nil, err
- } else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(pid), C.int(w.Fd())); err != nil {
- _, _ = r.Close(), w.Close()
- return nil, nil, err
- } else {
- return r, func() { runtime.KeepAlive(w) }, nil
- }
-}
-
-// shimEnv is the name of the environment variable storing decimal representation of
-// setup pipe fd for [container.Receive].
-const shimEnv = "HAKUREI_SHIM"
-
-// shimParams is embedded in outcomeState and transmitted from priv side to shim.
-type shimParams struct {
- // Priv side pid, checked against ppid in signal handler for the syscall.SIGCONT hack.
- PrivPID int
-
- // Duration to wait for after the initial process receives os.Interrupt before the container is killed.
- // Limits are enforced on the priv side.
- WaitDelay time.Duration
-
- // Verbosity pass through from [message.Msg].
- Verbose bool
-
- // Outcome setup ops, contains setup state. Populated by outcome.finalise.
- Ops []outcomeOp
-}
-
-// valid checks shimParams to be safe for use.
-func (p *shimParams) valid() bool { return p != nil && p.PrivPID > 0 }
-
-// shimName is the prefix used by log.std in the shim process.
-const shimName = "shim"
-
-// Shim is called by the main function of the shim process and runs as the unconstrained target user.
-// Shim does not return.
-func Shim(msg message.Msg) {
- if msg == nil {
- msg = message.NewMsg(log.Default())
- }
- shimEntrypoint(direct{msg})
-}
-
-func shimEntrypoint(k syscallDispatcher) {
- msg := k.getMsg()
- if msg == nil {
- panic("attempting to call shimEntrypoint with nil msg")
- } else if logger := msg.GetLogger(); logger != nil {
- logger.SetPrefix(shimName + ": ")
- logger.SetFlags(0)
- }
-
- if err := k.setDumpable(container.SUID_DUMP_DISABLE); err != nil {
- k.fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
- }
-
- var (
- state outcomeState
- closeSetup func() error
- )
- if f, err := k.receive(shimEnv, &state, nil); err != nil {
- if errors.Is(err, syscall.EBADF) {
- k.fatal("invalid config descriptor")
- }
- if errors.Is(err, container.ErrReceiveEnv) {
- k.fatal(shimEnv + " not set")
- }
-
- k.fatalf("cannot receive shim setup params: %v", err)
- } else {
- msg.SwapVerbose(state.Shim.Verbose)
- closeSetup = f
-
- if err = state.populateLocal(k, msg); err != nil {
- if m, ok := message.GetMessage(err); ok {
- k.fatal(m)
- } else {
- k.fatalf("cannot populate local state: %v", err)
- }
- }
- }
-
- // the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
- var signalPipe io.ReadCloser
- if r, wKeepAlive, err := k.setupContSignal(state.Shim.PrivPID); err != nil {
- switch {
- case errors.As(err, new(*os.SyscallError)): // returned by os.Pipe
- k.fatal(err.Error())
- return
-
- case errors.As(err, new(syscall.Errno)): // returned by hakurei_shim_setup_cont_signal
- k.fatalf("cannot install SIGCONT handler: %v", err)
- return
-
- default: // unreachable
- k.fatalf("cannot set up exit request: %v", err)
- return
- }
-
- } else {
- defer wKeepAlive()
- signalPipe = r
- }
-
- // pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
- if err := k.prctl(syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); err != nil {
- k.fatalf("cannot set parent-death signal: %v", err)
- }
-
- stateParams := state.newParams()
- for _, op := range state.Shim.Ops {
- if err := op.toContainer(stateParams); err != nil {
- if m, ok := message.GetMessage(err); ok {
- k.fatal(m)
- } else {
- k.fatalf("cannot create container state: %v", err)
- }
- }
- }
-
- // shim exit outcomes
- var cancelContainer atomic.Pointer[context.CancelFunc]
- k.new(func(k syscallDispatcher, msg message.Msg) {
- buf := make([]byte, 1)
- for {
- if _, err := signalPipe.Read(buf); err != nil {
- k.fatalf("cannot read from signal pipe: %v", err)
- }
-
- switch buf[0] {
- case 0: // got SIGCONT from monitor: shim exit requested
- if fp := cancelContainer.Load(); stateParams.params.ForwardCancel && fp != nil && *fp != nil {
- (*fp)()
- // shim now bound by ShimWaitDelay, implemented below
- continue
- }
-
- // setup has not completed, terminate immediately
- msg.Resume()
- k.exit(hst.ExitRequest)
- return
-
- case 1: // got SIGCONT after adoption: monitor died before delivering signal
- msg.BeforeExit()
- k.exit(hst.ExitOrphan)
- return
-
- case 2: // unreachable
- msg.Verbose("sa_sigaction got invalid siginfo")
-
- case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
- msg.Verbose("got SIGCONT from unexpected process")
-
- default: // unreachable
- k.fatalf("got invalid message %d from signal handler", buf[0])
- }
- }
- })
-
- if stateParams.params.Ops == nil {
- k.fatal("invalid container params")
- }
-
- // close setup socket
- if err := closeSetup(); err != nil {
- msg.Verbosef("cannot close setup pipe: %v", err)
- // not fatal
- }
-
- ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
- cancelContainer.Store(&stop)
- z := container.New(ctx, msg)
- z.Params = *stateParams.params
- z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
-
- // bounds and default enforced in finalise.go
- z.WaitDelay = state.Shim.WaitDelay
-
- if err := k.containerStart(z); err != nil {
- printMessageError("cannot start container:", err)
- k.exit(hst.ExitFailure)
- }
- if err := k.containerServe(z); err != nil {
- printMessageError("cannot configure container:", err)
- }
-
- if err := k.seccompLoad(
- seccomp.Preset(comp.PresetStrict, seccomp.AllowMultiarch),
- seccomp.AllowMultiarch,
- ); err != nil {
- k.fatalf("cannot load syscall filter: %v", err)
- }
-
- if err := k.containerWait(z); err != nil {
- var exitError *exec.ExitError
- if !errors.As(err, &exitError) {
- if errors.Is(err, context.Canceled) {
- k.exit(hst.ExitCancel)
- }
- msg.Verbosef("cannot wait: %v", err)
- k.exit(127)
- }
- k.exit(exitError.ExitCode())
- }
-}
diff --git a/internal/app/shim_test.go b/internal/app/shim_test.go
deleted file mode 100644
index 19dedd6f..00000000
--- a/internal/app/shim_test.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package app
-
-import (
- "bytes"
- "context"
- "log"
- "os"
- "syscall"
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/container/seccomp"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/internal/env"
-)
-
-func TestShimEntrypoint(t *testing.T) {
- t.Parallel()
- shimPreset := seccomp.Preset(comp.PresetStrict, seccomp.AllowMultiarch)
- templateParams := &container.Params{
- Dir: m("/data/data/org.chromium.Chromium"),
- Env: []string{
- "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus",
- "DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
- "GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
- "GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
- "GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
- "HOME=/data/data/org.chromium.Chromium",
- "PULSE_COOKIE=/.hakurei/pulse-cookie",
- "PULSE_SERVER=unix:/run/user/1000/pulse/native",
- "SHELL=/run/current-system/sw/bin/zsh",
- "TERM=xterm-256color",
- "USER=chronos",
- "WAYLAND_DISPLAY=wayland-0",
- "XDG_RUNTIME_DIR=/run/user/1000",
- "XDG_SESSION_CLASS=user",
- "XDG_SESSION_TYPE=wayland",
- },
-
- // spParamsOp
- Hostname: "localhost",
- RetainSession: true,
- HostNet: true,
- HostAbstract: true,
- ForwardCancel: true,
- Path: m("/run/current-system/sw/bin/chromium"),
- Args: []string{
- "chromium",
- "--ignore-gpu-blocklist",
- "--disable-smooth-scrolling",
- "--enable-features=UseOzonePlatform",
- "--ozone-platform=wayland",
- },
- SeccompFlags: seccomp.AllowMultiarch,
- Uid: 1000,
- Gid: 100,
-
- Ops: new(container.Ops).
- // resolveRoot
- Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
- // spParamsOp
- Proc(fhs.AbsProc).
- Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
- Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice).
- Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777).
-
- // spRuntimeOp
- Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
- Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), comp.BindWritable).
-
- // spTmpdirOp
- Bind(m("/tmp/hakurei.10/tmpdir/9999"), fhs.AbsTmp, comp.BindWritable).
-
- // spAccountOp
- Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
- Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
-
- // spWaylandOp
- Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1000/wayland-0"), 0).
-
- // spPulseOp
- Bind(m("/run/user/1000/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1000/pulse/native"), 0).
- Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
-
- // spDBusOp
- Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1000/bus"), 0).
- Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
-
- // spFilesystemOp
- Etc(fhs.AbsEtc, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
- Tmpfs(fhs.AbsTmp, 0, 0755).
- Overlay(m("/nix/store"),
- fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
- fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
- Link(m("/run/current-system"), "/run/current-system", true).
- Link(m("/run/opengl-driver"), "/run/opengl-driver", true).
- Bind(fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
- m("/data/data/org.chromium.Chromium"),
- comp.BindWritable|comp.BindEnsure).
- Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"),
- comp.BindOptional|comp.BindWritable|comp.BindDevice).
- Remount(fhs.AbsRoot, syscall.MS_RDONLY),
- }
-
- checkSimple(t, "shimEntrypoint", []simpleTestCase{
- {"success", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{
- call("getMsg", stub.ExpectArgs{}, nil, nil),
- call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil),
- call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil),
- call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{
- Shim: &shimParams{PrivPID: 0xbad, WaitDelay: 0xf, Verbose: true, Ops: []outcomeOp{
- &spParamsOp{"xterm-256color", true},
- &spRuntimeOp{sessionTypeWayland},
- spTmpdirOp{},
- spAccountOp{},
- &spWaylandOp{},
- &spPulseOp{(*[256]byte)(bytes.Repeat([]byte{0}, pulseCookieSizeMax)), pulseCookieSizeMax},
- &spDBusOp{true},
- &spFilesystemOp{},
- }},
-
- ID: &checkExpectInstanceId,
- Identity: hst.IdentityMax,
- UserID: 10,
- Container: hst.Template().Container,
- Mapuid: 1000,
- Mapgid: 100,
- Paths: &env.Paths{TempDir: fhs.AbsTmp, RuntimePath: fhs.AbsRunUser.Append("1000")},
- }, nil}, nil, nil),
- call("swapVerbose", stub.ExpectArgs{true}, false, nil),
- call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil),
- call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
- call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil),
- call("New", stub.ExpectArgs{}, nil, nil),
- call("closeReceive", stub.ExpectArgs{}, nil, nil),
- call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, nil, nil),
- call("containerStart", stub.ExpectArgs{templateParams}, nil, nil),
- call("containerServe", stub.ExpectArgs{templateParams}, nil, nil),
- call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil),
- call("containerWait", stub.ExpectArgs{templateParams}, nil, nil),
-
- // deferred
- call("wKeepAlive", stub.ExpectArgs{}, nil, nil),
- }, Tracks: []stub.Expect{{Calls: []stub.Call{
- call("rcRead", stub.ExpectArgs{}, []byte{2}, nil),
- call("verbose", stub.ExpectArgs{[]any{"sa_sigaction got invalid siginfo"}}, nil, nil),
- call("rcRead", stub.ExpectArgs{}, []byte{3}, nil),
- call("verbose", stub.ExpectArgs{[]any{"got SIGCONT from unexpected process"}}, nil, nil),
- call("rcRead", stub.ExpectArgs{}, nil, nil), // stub terminates this goroutine
- }}}}, nil},
- })
-}
diff --git a/internal/app/spaccount.go b/internal/app/spaccount.go
deleted file mode 100644
index c9f08fe9..00000000
--- a/internal/app/spaccount.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package app
-
-import (
- "encoding/gob"
- "fmt"
- "syscall"
-
- "hakurei.app/container/fhs"
- "hakurei.app/internal/validate"
-)
-
-func init() { gob.Register(spAccountOp{}) }
-
-// spAccountOp sets up user account emulation inside the container.
-type spAccountOp struct{}
-
-func (s spAccountOp) toSystem(state *outcomeStateSys) error {
- // do checks here to fail before fork/exec
- if state.Container == nil || state.Container.Home == nil || state.Container.Shell == nil {
- // unreachable
- return syscall.ENOTRECOVERABLE
- }
-
- // default is applied in toContainer
- if state.Container.Username != "" && !validate.IsValidUsername(state.Container.Username) {
- return newWithMessage(fmt.Sprintf("invalid user name %q", state.Container.Username))
- }
- return nil
-}
-
-func (s spAccountOp) toContainer(state *outcomeStateParams) error {
- const fallbackUsername = "chronos"
-
- username := state.Container.Username
- if username == "" {
- username = fallbackUsername
- }
-
- state.params.Dir = state.Container.Home
- state.env["HOME"] = state.Container.Home.String()
- state.env["USER"] = username
- state.env["SHELL"] = state.Container.Shell.String()
-
- state.params.
- Place(fhs.AbsEtc.Append("passwd"),
- []byte(username+":x:"+
- state.mapuid.String()+":"+
- state.mapgid.String()+
- ":Hakurei:"+
- state.Container.Home.String()+":"+
- state.Container.Shell.String()+"\n")).
- Place(fhs.AbsEtc.Append("group"),
- []byte("hakurei:x:"+state.mapgid.String()+":\n"))
-
- return nil
-}
diff --git a/internal/app/spaccount_test.go b/internal/app/spaccount_test.go
deleted file mode 100644
index c5fb7b5b..00000000
--- a/internal/app/spaccount_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package app
-
-import (
- "os"
- "syscall"
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
-)
-
-func TestSpAccountOp(t *testing.T) {
- t.Parallel()
- config := hst.Template()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"invalid state", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
- c := hst.Template()
- c.Container.Shell = nil
- return c
- }, nil, []stub.Call{
- // this op performs basic validation and does not make calls during toSystem
- }, nil, nil, syscall.ENOTRECOVERABLE, nil, nil, nil, nil, nil},
-
- {"invalid user name", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
- c := hst.Template()
- c.Container.Username = "9"
- return c
- }, nil, []stub.Call{
- // this op performs basic validation and does not make calls during toSystem
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: `invalid user name "9"`,
- }, nil, nil, nil, nil, nil},
-
- {"success fallback username", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
- c := hst.Template()
- c.Container.Username = ""
- return c
- }, nil, []stub.Call{
- // this op performs basic validation and does not make calls during toSystem
- }, newI(), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Dir: config.Container.Home,
- Ops: new(container.Ops).
- Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
- Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
- }, paramsWantEnv(config, map[string]string{
- "HOME": config.Container.Home.String(),
- "USER": config.Container.Username,
- "SHELL": config.Container.Shell.String(),
- }, nil), nil},
-
- {"success", func(bool, bool) outcomeOp { return spAccountOp{} }, hst.Template, nil, []stub.Call{
- // this op performs basic validation and does not make calls during toSystem
- }, newI(), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Dir: config.Container.Home,
- Ops: new(container.Ops).
- Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
- Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
- }, paramsWantEnv(config, map[string]string{
- "HOME": config.Container.Home.String(),
- "USER": config.Container.Username,
- "SHELL": config.Container.Shell.String(),
- }, nil), nil},
- })
-}
diff --git a/internal/app/spcontainer.go b/internal/app/spcontainer.go
deleted file mode 100644
index d411cb9f..00000000
--- a/internal/app/spcontainer.go
+++ /dev/null
@@ -1,391 +0,0 @@
-package app
-
-import (
- "encoding/gob"
- "errors"
- "io/fs"
- "os"
- "path"
- "slices"
- "strconv"
- "syscall"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/container/seccomp"
- "hakurei.app/hst"
- "hakurei.app/internal/validate"
- "hakurei.app/message"
- "hakurei.app/system"
- "hakurei.app/system/acl"
- "hakurei.app/system/dbus"
-)
-
-const varRunNscd = fhs.Var + "run/nscd"
-
-func init() { gob.Register(new(spParamsOp)) }
-
-// spParamsOp initialises unordered fields of [container.Params] and the optional root filesystem.
-// This outcomeOp is hardcoded to always run first.
-type spParamsOp struct {
- // Value of $TERM, stored during toSystem.
- Term string
- // Whether $TERM is set, stored during toSystem.
- TermSet bool
-}
-
-func (s *spParamsOp) toSystem(state *outcomeStateSys) error {
- s.Term, s.TermSet = state.k.lookupEnv("TERM")
- state.sys.Ensure(state.sc.SharePath, 0711)
- return nil
-}
-
-func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
- // pass $TERM for proper terminal I/O in initial process
- if s.TermSet {
- state.env["TERM"] = s.Term
- }
-
- // in practice there should be less than 30 system mount points
- const preallocateOpsCount = 1 << 5
-
- state.params.Hostname = state.Container.Hostname
- state.params.RetainSession = state.Container.Flags&hst.FTty != 0
- state.params.HostNet = state.Container.Flags&hst.FHostNet != 0
- state.params.HostAbstract = state.Container.Flags&hst.FHostAbstract != 0
-
- if state.Container.Path == nil {
- return newWithMessage("invalid program path")
- }
- state.params.Path = state.Container.Path
-
- if len(state.Container.Args) == 0 {
- state.params.Args = []string{state.Container.Path.String()}
- } else {
- state.params.Args = state.Container.Args
- }
-
- // the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
- // this behaviour is implemented in the shim
- state.params.ForwardCancel = state.Shim.WaitDelay > 0
-
- if state.Container.Flags&hst.FMultiarch != 0 {
- state.params.SeccompFlags |= seccomp.AllowMultiarch
- }
-
- if state.Container.Flags&hst.FSeccompCompat == 0 {
- state.params.SeccompPresets |= comp.PresetExt
- }
- if state.Container.Flags&hst.FDevel == 0 {
- state.params.SeccompPresets |= comp.PresetDenyDevel
- }
- if state.Container.Flags&hst.FUserns == 0 {
- state.params.SeccompPresets |= comp.PresetDenyNS
- }
- if state.Container.Flags&hst.FTty == 0 {
- state.params.SeccompPresets |= comp.PresetDenyTTY
- }
-
- if state.Container.Flags&hst.FMapRealUID != 0 {
- state.params.Uid = state.Mapuid
- state.params.Gid = state.Mapgid
- }
-
- {
- state.as.AutoEtcPrefix = state.id.String()
- ops := make(container.Ops, 0, preallocateOpsCount+len(state.Container.Filesystem))
- state.params.Ops = &ops
- state.as.Ops = opsAdapter{&ops}
- }
-
- rootfs, filesystem, _ := resolveRoot(state.Container)
- state.filesystem = filesystem
- if rootfs != nil {
- rootfs.Apply(&state.as)
- }
-
- // early mount points
- state.params.
- Proc(fhs.AbsProc).
- Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755)
- if state.Container.Flags&hst.FDevice == 0 {
- state.params.DevWritable(fhs.AbsDev, true)
- } else {
- state.params.Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice)
- }
- // /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
- state.params.Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777)
-
- return nil
-}
-
-func init() { gob.Register(new(spFilesystemOp)) }
-
-// spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem.
-// This outcomeOp is hardcoded to always run last.
-type spFilesystemOp struct {
- // Matched paths to cover. Stored during toSystem.
- HidePaths []*check.Absolute
-}
-
-func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
- /* retrieve paths and hide them if they're made available in the sandbox;
-
- this feature tries to improve user experience of permissive defaults, and
- 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 */
- hidePaths := []string{
- state.sc.RuntimePath.String(),
- state.sc.SharePath.String(),
-
- // this causes emulated passwd database to be bypassed on some /etc/ setups
- varRunNscd,
- }
-
- // dbus.Address does not go through syscallDispatcher
- systemBusAddr := dbus.FallbackSystemBusAddress
- if addr, ok := state.k.lookupEnv(dbus.SystemBusAddress); ok {
- systemBusAddr = addr
- }
-
- if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
- return &hst.AppError{Step: "parse dbus address", Err: err}
- } else {
- // there is usually only one, do not preallocate
- for _, entry := range entries {
- if entry.Method != "unix" {
- continue
- }
- for _, pair := range entry.Values {
- if pair[0] == "path" {
- if path.IsAbs(pair[1]) {
- // get parent dir of socket
- dir := path.Dir(pair[1])
- if dir == "." || dir == fhs.Root {
- state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
- }
- hidePaths = append(hidePaths, dir)
- } else {
- state.msg.Verbosef("dbus socket %q is not absolute", pair[1])
- }
- }
- }
- }
- }
- hidePathMatch := make([]bool, len(hidePaths))
- for i := range hidePaths {
- if err := evalSymlinks(state.msg, state.k, &hidePaths[i]); err != nil {
- return &hst.AppError{Step: "evaluate path hiding target", Err: err}
- }
- }
-
- _, filesystem, autoroot := resolveRoot(state.Container)
-
- var hidePathSourceCount int
- for i, c := range filesystem {
- if !c.Valid() {
- return newWithMessage("invalid filesystem at index " + strconv.Itoa(i))
- }
-
- // fs counter
- hidePathSourceCount += len(c.Host())
- }
-
- // AutoRootOp is a collection of many BindMountOp internally
- var autoRootEntries []fs.DirEntry
- if autoroot != nil {
- if d, err := state.k.readdir(autoroot.Source.String()); err != nil {
- return &hst.AppError{Step: "access autoroot source", Err: err}
- } else {
- // autoroot counter
- hidePathSourceCount += len(d)
- autoRootEntries = d
- }
- }
-
- hidePathSource := make([]*check.Absolute, 0, hidePathSourceCount)
-
- // fs append
- for _, c := range filesystem {
- // all entries already checked above
- hidePathSource = append(hidePathSource, c.Host()...)
- }
-
- // autoroot append
- if autoroot != nil {
- for _, ent := range autoRootEntries {
- name := ent.Name()
- if container.IsAutoRootBindable(state.msg, name) {
- hidePathSource = append(hidePathSource, autoroot.Source.Append(name))
- }
- }
- }
-
- // evaluated path, input path
- hidePathSourceEval := make([][2]string, len(hidePathSource))
- for i, a := range hidePathSource {
- if a == nil {
- // unreachable
- return newWithMessage("impossible path hiding state reached")
- }
-
- hidePathSourceEval[i] = [2]string{a.String(), a.String()}
- if err := evalSymlinks(state.msg, state.k, &hidePathSourceEval[i][0]); err != nil {
- return &hst.AppError{Step: "evaluate path hiding source", Err: err}
- }
- }
-
- for _, p := range hidePathSourceEval {
- for i := range hidePaths {
- // skip matched entries
- if hidePathMatch[i] {
- continue
- }
-
- if ok, err := validate.DeepContainsH(p[0], hidePaths[i]); err != nil {
- return &hst.AppError{Step: "determine path hiding outcome", Err: err}
- } else if ok {
- hidePathMatch[i] = true
- state.msg.Verbosef("hiding path %q from %q", hidePaths[i], p[1])
- }
- }
- }
-
- // copy matched paths for shim
- for i, ok := range hidePathMatch {
- if ok {
- if a, err := check.NewAbs(hidePaths[i]); err != nil {
- return newWithMessage("invalid path hiding candidate " + strconv.Quote(hidePaths[i]))
- } else {
- s.HidePaths = append(s.HidePaths, a)
- }
- }
- }
-
- // append ExtraPerms last
- flattenExtraPerms(state.sys, state.extraPerms)
- return nil
-}
-
-func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
- for i, c := range state.filesystem {
- if !c.Valid() {
- return newWithMessage("invalid filesystem at index " + strconv.Itoa(i))
- }
- c.Apply(&state.as)
- }
-
- for _, a := range s.HidePaths {
- state.params.Tmpfs(a, 1<<13, 0755)
- }
-
- // no more configured paths beyond this point
- if state.Container.Flags&hst.FDevice == 0 {
- state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
- }
- state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
-
- state.params.Env = make([]string, 0, len(state.env))
- for key, value := range state.env {
- // key validated early via hst
- state.params.Env = append(state.params.Env, key+"="+value)
- }
- slices.Sort(state.params.Env)
-
- return nil
-}
-
-// resolveRoot handles the root filesystem special case for [hst.FilesystemConfig] and additionally resolves autoroot
-// as it requires special handling during path hiding.
-func resolveRoot(c *hst.ContainerConfig) (rootfs hst.FilesystemConfig, filesystem []hst.FilesystemConfigJSON, autoroot *hst.FSBind) {
- // root filesystem special case
- filesystem = c.Filesystem
- // valid happens late, so root gets it here
- if len(filesystem) > 0 && filesystem[0].Valid() && filesystem[0].Path().String() == fhs.Root {
- // if the first element targets /, it is inserted early and excluded from path hiding
- rootfs = filesystem[0].FilesystemConfig
- filesystem = filesystem[1:]
-
- // autoroot requires special handling during path hiding
- if b, ok := rootfs.(*hst.FSBind); ok && b.IsAutoRoot() {
- autoroot = b
- }
- }
- return
-}
-
-// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
-func evalSymlinks(msg message.Msg, k syscallDispatcher, v *string) error {
- if p, err := k.evalSymlinks(*v); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- return err
- }
- msg.Verbosef("path %q does not yet exist", *v)
- } else {
- *v = p
- }
- return nil
-}
-
-// flattenExtraPerms expands a slice of [hst.ExtraPermConfig] into [system.I].
-func flattenExtraPerms(sys *system.I, extraPerms []hst.ExtraPermConfig) {
- for i := range extraPerms {
- p := &extraPerms[i]
- if p.Path == nil {
- continue
- }
-
- if p.Ensure {
- sys.Ensure(p.Path, 0700)
- }
-
- perms := make(acl.Perms, 0, 3)
- if p.Read {
- perms = append(perms, acl.Read)
- }
- if p.Write {
- perms = append(perms, acl.Write)
- }
- if p.Execute {
- perms = append(perms, acl.Execute)
- }
- sys.UpdatePermType(system.User, p.Path, perms...)
- }
-}
-
-// opsAdapter implements [hst.Ops] on [container.Ops].
-type opsAdapter struct{ *container.Ops }
-
-func (p opsAdapter) Tmpfs(target *check.Absolute, size int, perm os.FileMode) hst.Ops {
- return opsAdapter{p.Ops.Tmpfs(target, size, perm)}
-}
-
-func (p opsAdapter) Readonly(target *check.Absolute, perm os.FileMode) hst.Ops {
- return opsAdapter{p.Ops.Readonly(target, perm)}
-}
-
-func (p opsAdapter) Bind(source, target *check.Absolute, flags int) hst.Ops {
- return opsAdapter{p.Ops.Bind(source, target, flags)}
-}
-
-func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) hst.Ops {
- return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
-}
-
-func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
- return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
-}
-
-func (p opsAdapter) Link(target *check.Absolute, linkName string, dereference bool) hst.Ops {
- return opsAdapter{p.Ops.Link(target, linkName, dereference)}
-}
-
-func (p opsAdapter) Root(host *check.Absolute, flags int) hst.Ops {
- return opsAdapter{p.Ops.Root(host, flags)}
-}
-
-func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
- return opsAdapter{p.Ops.Etc(host, prefix)}
-}
diff --git a/internal/app/spcontainer_test.go b/internal/app/spcontainer_test.go
deleted file mode 100644
index 154b9297..00000000
--- a/internal/app/spcontainer_test.go
+++ /dev/null
@@ -1,477 +0,0 @@
-package app
-
-import (
- "errors"
- "os"
- "reflect"
- "syscall"
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/container/seccomp"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
- "hakurei.app/system/dbus"
-)
-
-func TestSpParamsOp(t *testing.T) {
- t.Parallel()
- config := hst.Template()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"invalid program path", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spParamsOp)
- }
- return &spParamsOp{Term: "xterm", TermSet: true}
- }, func() *hst.Config {
- c := hst.Template()
- c.Container.Path = nil
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
- }, newI().
- Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: "invalid program path",
- }},
-
- {"success defaultargs secure", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spParamsOp)
- }
- return &spParamsOp{Term: "xterm", TermSet: true}
- }, func() *hst.Config {
- c := hst.Template()
- c.Container.Args = nil
- c.Container.Flags = hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
- }, newI().
- Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Hostname: config.Container.Hostname,
- HostNet: true,
- HostAbstract: true,
- Path: config.Container.Path,
- Args: []string{config.Container.Path.String()},
- SeccompPresets: comp.PresetExt | comp.PresetDenyDevel | comp.PresetDenyNS | comp.PresetDenyTTY,
- Uid: 1000,
- Gid: 100,
- Ops: new(container.Ops).
- Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
- Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
- DevWritable(fhs.AbsDev, true).
- Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
- }, paramsWantEnv(config, map[string]string{
- "TERM": "xterm",
- }, func(t *testing.T, state *outcomeStateParams) {
- if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
- t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
- }
-
- wantFilesystems := config.Container.Filesystem[1:]
- if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
- t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
- }
- }), nil},
-
- {"success", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spParamsOp)
- }
- return &spParamsOp{Term: "xterm", TermSet: true}
- }, hst.Template, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
- }, newI().
- Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Hostname: config.Container.Hostname,
- RetainSession: true,
- HostNet: true,
- HostAbstract: true,
- Path: config.Container.Path,
- Args: config.Container.Args,
- SeccompFlags: seccomp.AllowMultiarch,
- Uid: 1000,
- Gid: 100,
- Ops: new(container.Ops).
- Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
- Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
- Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice).
- Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
- }, paramsWantEnv(config, map[string]string{
- "TERM": "xterm",
- }, func(t *testing.T, state *outcomeStateParams) {
- if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
- t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
- }
-
- wantFilesystems := config.Container.Filesystem[1:]
- if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
- t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
- }
- }), nil},
- })
-}
-
-func TestSpFilesystemOp(t *testing.T) {
- const nePrefix = container.Nonexistent + "/eval"
- var stubDebianRoot = stubDir("bin", "dev", "etc", "home", "lib64", "lost+found",
- "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
- config := hst.Template()
-
- newConfigSmall := func() *hst.Config {
- c := hst.Template()
- c.Container.Filesystem = []hst.FilesystemConfigJSON{
- {FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: fhs.AbsEtc, Special: true}},
- {FilesystemConfig: &hst.FSOverlay{Target: m("/nix/store"), Lower: []*check.Absolute{
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/.ro-store"),
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium"),
- }}},
- {FilesystemConfig: &hst.FSEphemeral{Target: hst.AbsPrivateTmp}},
- }
- c.Container.Flags &= ^hst.FDevice
- return c
- }
- configSmall := newConfigSmall()
-
- needsApplyState := func(next pStateContainerFunc) pStateContainerFunc {
- return func(state *outcomeStateParams) {
- state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
-
- if next != nil {
- next(state)
- }
- }
- }
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"readdir", func(bool, bool) outcomeOp {
- return new(spFilesystemOp)
- }, hst.Template, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/dbus"}, nePrefix+"/run/dbus", nil),
- call("readdir", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian"}, []os.DirEntry{}, stub.UniqueError(2)),
- }, nil, nil, &hst.AppError{
- Step: "access autoroot source",
- Err: stub.UniqueError(2),
- }, nil, nil, nil, nil, nil},
-
- {"invalid dbus address", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
- c := newConfigSmall()
- c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(false)})
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid", nil),
- }, nil, nil, &hst.AppError{
- Step: "parse dbus address",
- Err: &dbus.BadAddressError{
- Type: dbus.ErrNoColon,
- EntryVal: []byte("invalid"),
- PairPos: -1,
- },
- }, nil, nil, nil, nil, nil},
-
- {"invalid fs early", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
- c := newConfigSmall()
- c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(false)})
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: "invalid filesystem at index 3",
- }, nil, nil, nil, nil, nil},
-
- {"evalSymlinks early", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, "", stub.UniqueError(0)),
- }, nil, nil, &hst.AppError{
- Step: "evaluate path hiding target",
- Err: stub.UniqueError(0),
- }, nil, nil, nil, nil, nil},
-
- {"host nil abs", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
- c := newConfigSmall()
- c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(true)})
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: "impossible path hiding state reached",
- }, nil, nil, nil, nil, nil},
-
- {"evalSymlinks late", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", stub.UniqueError(1)),
- }, nil, nil, &hst.AppError{
- Step: "evaluate path hiding source",
- Err: stub.UniqueError(1),
- }, nil, nil, nil, nil, nil},
-
- {"invalid contains", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
- call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
- }, nil, nil, &hst.AppError{
- Step: "determine path hiding outcome",
- Err: errors.New("Rel: can't make /proc/nonexistent/eval/xdg_runtime_dir relative to var/lib/hakurei/base/org.nixos/org.chromium.Chromium"),
- }, nil, nil, nil, nil, nil},
-
- {"invalid hide", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, "xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, "tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "nscd", nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, "nonexistent/dbus", nil),
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, "nonexistent", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, ".ro-store", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "org.chromium.Chromium", nil),
- call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"nonexistent/dbus", "/etc/"}}, nil, nil),
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: `invalid path hiding candidate "nonexistent/dbus"`,
- }, nil, nil, nil, nil, nil},
-
- {"invalid fs", func(isShim, clearUnexported bool) outcomeOp {
- if !isShim {
- return new(spFilesystemOp)
- }
- return &spFilesystemOp{HidePaths: []*check.Absolute{m("/proc/nonexistent/eval/etc/dbus")}}
- }, newConfigSmall, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
- call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
- }, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
- acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
- state.filesystem = append(configSmall.Container.Filesystem, hst.FilesystemConfigJSON{})
- })), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: "invalid filesystem at index 3",
- }},
-
- {"success noroot nodev envdbus strangedbus dbusnotabs hide", func(isShim, clearUnexported bool) outcomeOp {
- if !isShim {
- return new(spFilesystemOp)
- }
- return &spFilesystemOp{HidePaths: []*check.Absolute{m("/proc/nonexistent/eval/etc/dbus")}}
- }, newConfigSmall, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
- call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
- call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
- }, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
- acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
- state.filesystem = configSmall.Container.Filesystem
- })), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Env: []string{
- "GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
- "GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
- "GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
- },
-
- Ops: new(container.Ops).
- Etc(fhs.AbsEtc, wantAutoEtcPrefix).
- OverlayReadonly(
- check.MustAbs("/nix/store"),
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/.ro-store"),
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium")).
- Readonly(hst.AbsPrivateTmp, 0755).
- Tmpfs(m("/proc/nonexistent/eval/etc/dbus"), 1<<13, 0755).
- Remount(fhs.AbsDev, syscall.MS_RDONLY).
- Remount(fhs.AbsRoot, syscall.MS_RDONLY),
- }, nil, nil},
-
- {"success", func(bool, bool) outcomeOp {
- return new(spFilesystemOp)
- }, hst.Template, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
- call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
- call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/run/dbus"}, nePrefix+"/run/dbus", nil),
- call("readdir", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian"}, stubDebianRoot, nil),
- call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"}, nePrefix+"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"}, nePrefix+"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/ro-store", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/u0/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/u0/org.chromium.Chromium", nil),
- call("evalSymlinks", stub.ExpectArgs{"/dev/dri"}, nePrefix+"/dev/dri", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/bin"}, nePrefix+"/var/lib/hakurei/base/org.debian/bin", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/home"}, nePrefix+"/var/lib/hakurei/base/org.debian/home", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/lib64"}, nePrefix+"/var/lib/hakurei/base/org.debian/lib64", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/lost+found"}, nePrefix+"/var/lib/hakurei/base/org.debian/lost+found", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/nix"}, nePrefix+"/var/lib/hakurei/base/org.debian/nix", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/root"}, nePrefix+"/var/lib/hakurei/base/org.debian/root", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/run"}, nePrefix+"/var/lib/hakurei/base/org.debian/run", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/srv"}, nePrefix+"/var/lib/hakurei/base/org.debian/srv", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/sys"}, nePrefix+"/var/lib/hakurei/base/org.debian/sys", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/usr"}, nePrefix+"/var/lib/hakurei/base/org.debian/usr", nil),
- call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/var"}, nePrefix+"/var/lib/hakurei/base/org.debian/var", nil),
- }, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
- acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
- state.filesystem = config.Container.Filesystem[1:]
- })), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Env: []string{
- "GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
- "GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
- "GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
- },
-
- Ops: new(container.Ops).
- Etc(fhs.AbsEtc, wantAutoEtcPrefix).
- Tmpfs(fhs.AbsTmp, 0, 0755).
- Overlay(
- check.MustAbs("/nix/store"),
- fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
- fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
- fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
- Link(fhs.AbsRun.Append("current-system"), "/run/current-system", true).
- Link(fhs.AbsRun.Append("opengl-driver"), "/run/opengl-driver", true).
- Bind(
- fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
- check.MustAbs("/data/data/org.chromium.Chromium"),
- comp.BindWritable|comp.BindEnsure).
- Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"), comp.BindDevice|comp.BindWritable|comp.BindOptional).
- Remount(fhs.AbsRoot, syscall.MS_RDONLY),
- }, nil, nil},
- })
-}
-
-func TestFlattenExtraPerms(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- perms []hst.ExtraPermConfig
- want *system.I
- }{
- {"path nil check", append(hst.Template().ExtraPerms, hst.ExtraPermConfig{}), newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
- acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute)},
-
- {"template", hst.Template().ExtraPerms, newI().
- Ensure(m("/var/lib/hakurei/u0"), 0700).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
- acl.Execute).
- UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
- acl.Read, acl.Write, acl.Execute)},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- got := newI()
- flattenExtraPerms(got, tc.perms)
- if !reflect.DeepEqual(got, tc.want) {
- t.Errorf("flattenExtraPerms: sys = %#v, want %#v", got, tc.want)
- }
- })
- }
-}
-
-// invalidFSHost implements the Host method of [hst.FilesystemConfig] with an invalid response.
-type invalidFSHost bool
-
-func (f invalidFSHost) Valid() bool { return bool(f) }
-func (invalidFSHost) Path() *check.Absolute { panic("unreachable") }
-func (invalidFSHost) Host() []*check.Absolute { return []*check.Absolute{nil} }
-func (invalidFSHost) Apply(*hst.ApplyState) { panic("unreachable") }
-func (invalidFSHost) String() string { panic("unreachable") }
diff --git a/internal/app/spdbus.go b/internal/app/spdbus.go
deleted file mode 100644
index 429674d0..00000000
--- a/internal/app/spdbus.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package app
-
-import (
- "encoding/gob"
-
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/system/acl"
- "hakurei.app/system/dbus"
-)
-
-func init() { gob.Register(new(spDBusOp)) }
-
-// spDBusOp maintains an xdg-dbus-proxy instance for the container.
-// Runs after spRuntimeOp.
-type spDBusOp struct {
- // Whether to bind the system bus socket. Populated during toSystem.
- ProxySystem bool
-}
-
-func (s *spDBusOp) toSystem(state *outcomeStateSys) error {
- if state.et&hst.EDBus == 0 {
- return errNotEnabled
- }
-
- if state.sessionBus == nil {
- state.sessionBus = dbus.NewConfig(state.appId, true, true)
- }
-
- // downstream socket paths
- sessionPath, systemPath := state.instance().Append("bus"), state.instance().Append("system_bus_socket")
-
- var sessionBus, systemBus dbus.ProxyPair
- sessionBus[0], systemBus[0] = state.k.dbusAddress()
- sessionBus[1], systemBus[1] = sessionPath.String(), systemPath.String()
- if err := state.sys.ProxyDBus(state.sessionBus, state.systemBus, sessionBus, systemBus); err != nil {
- return err
- }
-
- state.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
- if state.systemBus != nil {
- s.ProxySystem = true
- state.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
- }
- return nil
-}
-
-func (s *spDBusOp) toContainer(state *outcomeStateParams) error {
- sessionInner := state.runtimeDir.Append("bus")
- state.env["DBUS_SESSION_BUS_ADDRESS"] = "unix:path=" + sessionInner.String()
- state.params.Bind(state.instancePath().Append("bus"), sessionInner, 0)
- if s.ProxySystem {
- systemInner := fhs.AbsVar.Append("run/dbus/system_bus_socket")
- state.env["DBUS_SYSTEM_BUS_ADDRESS"] = "unix:path=" + systemInner.String()
- state.params.Bind(state.instancePath().Append("system_bus_socket"), systemInner, 0)
- }
- return nil
-}
diff --git a/internal/app/spdbus_test.go b/internal/app/spdbus_test.go
deleted file mode 100644
index 8dcf102c..00000000
--- a/internal/app/spdbus_test.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package app
-
-import (
- "syscall"
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/stub"
- "hakurei.app/helper"
- "hakurei.app/hst"
- "hakurei.app/message"
- "hakurei.app/system"
- "hakurei.app/system/acl"
- "hakurei.app/system/dbus"
-)
-
-func TestSpDBusOp(t *testing.T) {
- config := hst.Template()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"not enabled", func(bool, bool) outcomeOp {
- return new(spDBusOp)
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements = 0
- return c
- }, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
-
- {"invalid", func(bool, bool) outcomeOp {
- return new(spDBusOp)
- }, func() *hst.Config {
- c := hst.Template()
- c.SessionBus.Talk[0] += "\x00"
- c.SystemBus = nil
- return c
- }, nil, []stub.Call{
- call("dbusAddress", stub.ExpectArgs{}, [2]string{
- "unix:path=/run/user/1000/bus",
- "unix:path=/var/run/dbus/system_bus_socket",
- }, nil),
- }, nil, sysUsesInstance(nil), &system.OpError{
- Op: "dbus",
- Err: syscall.EINVAL,
- Msg: "message bus proxy configuration contains NUL byte",
- Revert: false,
- }, nil, nil, nil, nil, nil},
-
- {"success default", func(bool, bool) outcomeOp {
- return new(spDBusOp)
- }, func() *hst.Config {
- c := hst.Template()
- c.SessionBus, c.SystemBus = nil, nil
- return c
- }, nil, []stub.Call{
- call("dbusAddress", stub.ExpectArgs{}, [2]string{
- "unix:path=/run/user/1000/bus",
- "unix:path=/var/run/dbus/system_bus_socket",
- }, nil),
- call("isVerbose", stub.ExpectArgs{}, true, nil),
- call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
- "unix:path=/run/user/1000/bus",
- wantInstancePrefix + "/bus",
- "--filter",
- "--talk=org.freedesktop.DBus",
- "--talk=org.freedesktop.Notifications",
- "--own=org.chromium.Chromium.*",
- "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "--call=org.freedesktop.portal.*=*",
- "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
- }}}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
- "unix:path=/run/user/1000/bus",
- wantInstancePrefix+"/bus",
- "--filter",
- "--talk=org.freedesktop.DBus",
- "--talk=org.freedesktop.Notifications",
- "--own=org.chromium.Chromium.*",
- "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "--call=org.freedesktop.portal.*=*",
- "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
- )}}, nil, nil),
- }, func() *system.I {
- sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
- sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
- if err := sys.ProxyDBus(
- dbus.NewConfig(config.ID, true, true), nil,
- dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"},
- dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", wantInstancePrefix + "/system_bus_socket"},
- ); err != nil {
- t.Fatalf("cannot prepare sys: %v", err)
- }
- sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write)
- return sys
- }(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantInstancePrefix+"/bus"),
- m("/run/user/1000/bus"), 0),
- }, paramsWantEnv(config, map[string]string{
- "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
- }, nil), nil},
-
- {"success", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spDBusOp)
- }
- return &spDBusOp{ProxySystem: true}
- }, hst.Template, nil, []stub.Call{
- call("dbusAddress", stub.ExpectArgs{}, [2]string{
- "unix:path=/run/user/1000/bus",
- "unix:path=/var/run/dbus/system_bus_socket",
- }, nil),
- call("isVerbose", stub.ExpectArgs{}, true, nil),
- call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
- "unix:path=/run/user/1000/bus",
- wantInstancePrefix + "/bus",
- "--filter",
- "--talk=org.freedesktop.Notifications",
- "--talk=org.freedesktop.FileManager1",
- "--talk=org.freedesktop.ScreenSaver",
- "--talk=org.freedesktop.secrets",
- "--talk=org.kde.kwalletd5",
- "--talk=org.kde.kwalletd6",
- "--talk=org.gnome.SessionManager",
- "--own=org.chromium.Chromium.*",
- "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "--own=org.mpris.MediaPlayer2.chromium.*",
- "--call=org.freedesktop.portal.*=*",
- "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
- }}}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{
- "unix:path=/var/run/dbus/system_bus_socket",
- wantInstancePrefix + "/system_bus_socket",
- "--filter",
- "--talk=org.bluez",
- "--talk=org.freedesktop.Avahi",
- "--talk=org.freedesktop.UPower",
- }}}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
- "unix:path=/run/user/1000/bus",
- wantInstancePrefix+"/bus",
- "--filter",
- "--talk=org.freedesktop.Notifications",
- "--talk=org.freedesktop.FileManager1",
- "--talk=org.freedesktop.ScreenSaver",
- "--talk=org.freedesktop.secrets",
- "--talk=org.kde.kwalletd5",
- "--talk=org.kde.kwalletd6",
- "--talk=org.gnome.SessionManager",
- "--own=org.chromium.Chromium.*",
- "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
- "--own=org.mpris.MediaPlayer2.chromium.*",
- "--call=org.freedesktop.portal.*=*",
- "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
-
- "unix:path=/var/run/dbus/system_bus_socket",
- wantInstancePrefix+"/system_bus_socket",
- "--filter",
- "--talk=org.bluez",
- "--talk=org.freedesktop.Avahi",
- "--talk=org.freedesktop.UPower",
- )}}, nil, nil),
- }, func() *system.I {
- sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
- sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
- if err := sys.ProxyDBus(
- config.SessionBus, config.SystemBus,
- dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"},
- dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", wantInstancePrefix + "/system_bus_socket"},
- ); err != nil {
- t.Fatalf("cannot prepare sys: %v", err)
- }
- sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write).
- UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read, acl.Write)
- return sys
- }(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantInstancePrefix+"/bus"),
- m("/run/user/1000/bus"), 0).
- Bind(m(wantInstancePrefix+"/system_bus_socket"),
- m("/var/run/dbus/system_bus_socket"), 0),
- }, paramsWantEnv(config, map[string]string{
- "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
- "DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/var/run/dbus/system_bus_socket",
- }, nil), nil},
- })
-}
diff --git a/internal/app/sppulse.go b/internal/app/sppulse.go
deleted file mode 100644
index fe63f366..00000000
--- a/internal/app/sppulse.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package app
-
-import (
- "encoding/gob"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "strconv"
- "syscall"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/message"
-)
-
-const pulseCookieSizeMax = 1 << 8
-
-func init() { gob.Register(new(spPulseOp)) }
-
-// spPulseOp exports the PulseAudio server to the container.
-// Runs after spRuntimeOp.
-type spPulseOp struct {
- // PulseAudio cookie data, populated during toSystem if a cookie is present.
- Cookie *[pulseCookieSizeMax]byte
- // PulseAudio cookie size, populated during toSystem if a cookie is present.
- CookieSize int
-}
-
-func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
- if state.et&hst.EPulse == 0 {
- return errNotEnabled
- }
-
- pulseRuntimeDir, pulseSocket := s.commonPaths(state.outcomeState)
-
- if _, err := state.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 newWithMessageError(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir), err)
- }
-
- if fi, err := state.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}
- }
- return newWithMessageError(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir), err)
- } else {
- if m := fi.Mode(); m&0o006 != 0o006 {
- return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
- }
- }
-
- // pulse socket is world writable and its parent directory DAC permissions prevents access;
- // hard link to target-executable share directory to grant access
- state.sys.Link(pulseSocket, state.runtime().Append("pulse"))
-
- // load up to pulseCookieSizeMax bytes of pulse cookie for transmission to shim
- if a, err := discoverPulseCookie(state.k); err != nil {
- return err
- } else if a != nil {
- s.Cookie = new([pulseCookieSizeMax]byte)
- if s.CookieSize, err = loadFile(state.msg, state.k, "PulseAudio cookie", a.String(), s.Cookie[:]); err != nil {
- return err
- }
- } else {
- state.msg.Verbose("cannot locate PulseAudio cookie (tried " +
- "$PULSE_COOKIE, " +
- "$XDG_CONFIG_HOME/pulse/cookie, " +
- "$HOME/.pulse-cookie)")
- }
-
- return nil
-}
-
-func (s *spPulseOp) toContainer(state *outcomeStateParams) error {
- innerPulseSocket := state.runtimeDir.Append("pulse", "native")
- state.params.Bind(state.runtimePath().Append("pulse"), innerPulseSocket, 0)
- state.env["PULSE_SERVER"] = "unix:" + innerPulseSocket.String()
-
- if s.Cookie != nil {
- innerDst := hst.AbsPrivateTmp.Append("/pulse-cookie")
-
- if s.CookieSize < 0 || s.CookieSize > pulseCookieSizeMax {
- return newWithMessage("unexpected PulseAudio cookie size")
- }
- state.env["PULSE_COOKIE"] = innerDst.String()
- state.params.Place(innerDst, s.Cookie[:s.CookieSize])
- }
-
- return nil
-}
-
-func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSocket *check.Absolute) {
- // PulseAudio runtime directory (usually `/run/user/%d/pulse`)
- pulseRuntimeDir = state.sc.RuntimePath.Append("pulse")
- // PulseAudio socket (usually `/run/user/%d/pulse/native`)
- pulseSocket = pulseRuntimeDir.Append("native")
- return
-}
-
-// discoverPulseCookie attempts to discover the pathname of the PulseAudio cookie of the current user.
-// If both returned pathname and error are nil, the cookie is likely unavailable and can be silently skipped.
-func discoverPulseCookie(k syscallDispatcher) (*check.Absolute, error) {
- const paLocateStep = "locate PulseAudio cookie"
-
- // from environment
- if p, ok := k.lookupEnv("PULSE_COOKIE"); ok {
- if a, err := check.NewAbs(p); err != nil {
- return nil, &hst.AppError{Step: paLocateStep, Err: err}
- } else {
- // this takes precedence, do not verify whether the file is accessible
- return a, nil
- }
- }
-
- // $HOME/.pulse-cookie
- if p, ok := k.lookupEnv("HOME"); ok {
- var pulseCookiePath *check.Absolute
- if a, err := check.NewAbs(p); err != nil {
- return nil, &hst.AppError{Step: paLocateStep, Err: err}
- } else {
- pulseCookiePath = a.Append(".pulse-cookie")
- }
-
- if fi, err := k.stat(pulseCookiePath.String()); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
- }
- // fallthrough
- } else if fi.IsDir() {
- // fallthrough
- } else {
- return pulseCookiePath, nil
- }
- }
-
- // $XDG_CONFIG_HOME/pulse/cookie
- if p, ok := k.lookupEnv("XDG_CONFIG_HOME"); ok {
- var pulseCookiePath *check.Absolute
- if a, err := check.NewAbs(p); err != nil {
- return nil, &hst.AppError{Step: paLocateStep, Err: err}
- } else {
- pulseCookiePath = a.Append("pulse", "cookie")
- }
-
- if fi, err := k.stat(pulseCookiePath.String()); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
- }
- // fallthrough
- } else if fi.IsDir() {
- // fallthrough
- } else {
- return pulseCookiePath, nil
- }
- }
-
- // cookie not present
- // not fatal: authentication is disabled
- return nil, nil
-}
-
-// loadFile reads up to len(buf) bytes from the file at pathname.
-func loadFile(
- msg message.Msg, k syscallDispatcher,
- description, pathname string, buf []byte,
-) (int, error) {
- n := len(buf)
- if n == 0 {
- return -1, errors.New("invalid buffer")
- }
-
- if fi, err := k.stat(pathname); err != nil {
- return -1, &hst.AppError{Step: "access " + description, Err: err}
- } else {
- if fi.IsDir() {
- return -1, &hst.AppError{Step: "read " + description,
- Err: &os.PathError{Op: "stat", Path: pathname, Err: syscall.EISDIR}}
- }
- if s := fi.Size(); s > int64(n) {
- return -1, newWithMessageError(
- description+" at "+strconv.Quote(pathname)+" exceeds expected size",
- &os.PathError{Op: "stat", Path: pathname, Err: syscall.ENOMEM},
- )
- } else if s < int64(n) {
- msg.Verbosef("%s at %q is %d bytes shorter than expected", description, pathname, int64(n)-s)
- } else {
- msg.Verbosef("loading %d bytes from %q", n, pathname)
- }
- }
-
- if f, err := k.open(pathname); err != nil {
- return -1, &hst.AppError{Step: "open " + description, Err: err}
- } else {
- if n, err = f.Read(buf); err != nil {
- if !errors.Is(err, io.EOF) {
- _ = f.Close()
- return n, &hst.AppError{Step: "read " + description, Err: err}
- }
- }
-
- if err = f.Close(); err != nil {
- return n, &hst.AppError{Step: "close " + description, Err: err}
- }
- return n, nil
- }
-}
diff --git a/internal/app/sppulse_test.go b/internal/app/sppulse_test.go
deleted file mode 100644
index c6220731..00000000
--- a/internal/app/sppulse_test.go
+++ /dev/null
@@ -1,460 +0,0 @@
-package app
-
-import (
- "bytes"
- "errors"
- "os"
- "syscall"
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/check"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
-)
-
-func TestSpPulseOp(t *testing.T) {
- t.Parallel()
-
- config := hst.Template()
- sampleCookie := bytes.Repeat([]byte{0xfc}, pulseCookieSizeMax)
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"not enabled", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements = 0
- return c
- }, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
-
- {"socketDir stat", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spPulseOp)
- }
- return &spPulseOp{Cookie: (*[256]byte)(sampleCookie)}
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), stub.UniqueError(2)),
- }, nil, nil, &hst.AppError{
- Step: `access PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse"`,
- Err: stub.UniqueError(2),
- }, nil, nil, nil, nil, nil},
-
- {"socketDir nonexistent", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), os.ErrNotExist),
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrNotExist,
- Msg: `PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse" not found`,
- }, nil, nil, nil, nil, nil},
-
- {"socket stat", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), stub.UniqueError(1)),
- }, nil, nil, &hst.AppError{
- Step: `access PulseAudio socket "/proc/nonexistent/xdg_runtime_dir/pulse/native"`,
- Err: stub.UniqueError(1),
- }, nil, nil, nil, nil, nil},
-
- {"socket nonexistent", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, (*stubFi)(nil), os.ErrNotExist),
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrNotExist,
- Msg: `PulseAudio directory "/proc/nonexistent/xdg_runtime_dir/pulse" found but socket does not exist`,
- }, nil, nil, nil, nil, nil},
-
- {"socket mode", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0660}, nil),
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: `unexpected permissions on "/proc/nonexistent/xdg_runtime_dir/pulse/native": -rw-rw----`,
- }, nil, nil, nil, nil, nil},
-
- {"cookie notAbs", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "proc/nonexistent/cookie", nil),
- }, nil, nil, &hst.AppError{
- Step: "locate PulseAudio cookie",
- Err: &check.AbsoluteError{Pathname: "proc/nonexistent/cookie"},
- }, nil, nil, nil, nil, nil},
-
- {"cookie loadFile", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubFi{isDir: false, size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"loading %d bytes from %q", []any{1 << 8, "/proc/nonexistent/cookie"}}, nil, nil),
- call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, (*stubOsFile)(nil), stub.UniqueError(0)),
- }, nil, nil, &hst.AppError{
- Step: "open PulseAudio cookie",
- Err: stub.UniqueError(0),
- }, nil, nil, nil, nil, nil},
-
- {"cookie bad shim size", func(isShim, clearUnexported bool) outcomeOp {
- if !isShim {
- return new(spPulseOp)
- }
- op := &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookie), CookieSize: pulseCookieSizeMax}
- if clearUnexported {
- op.CookieSize += +0xfd
- }
- return op
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubFi{isDir: false, size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"loading %d bytes from %q", []any{1 << 8, "/proc/nonexistent/cookie"}}, nil, nil),
- call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
- }, newI().
- // state.ensureRuntimeDir
- Ensure(m(wantRunDirPath), 0700).
- UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
- Ensure(m(wantRuntimePath), 0700).
- UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- // state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
- UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
- // toSystem
- Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: "unexpected PulseAudio cookie size",
- }},
-
- {"success cookie short", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spPulseOp)
- }
- sampleCookieTrunc := make([]byte, pulseCookieSizeMax)
- copy(sampleCookieTrunc, sampleCookie[:len(sampleCookie)-0xe])
- return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookieTrunc), CookieSize: pulseCookieSizeMax - 0xe}
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubFi{isDir: false, size: pulseCookieSizeMax - 0xe}, nil),
- call("verbosef", stub.ExpectArgs{"%s at %q is %d bytes shorter than expected", []any{"PulseAudio cookie", "/proc/nonexistent/cookie", int64(0xe)}}, nil, nil),
- call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie[:len(sampleCookie)-0xe])}, nil),
- }, newI().
- // state.ensureRuntimeDir
- Ensure(m(wantRunDirPath), 0700).
- UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
- Ensure(m(wantRuntimePath), 0700).
- UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- // state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
- UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
- // toSystem
- Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantRuntimeSharePath+"/pulse"), m("/run/user/1000/pulse/native"), 0).
- Place(m("/.hakurei/pulse-cookie"), sampleCookie[:len(sampleCookie)-0xe]),
- }, paramsWantEnv(config, map[string]string{
- "PULSE_SERVER": "unix:/run/user/1000/pulse/native",
- "PULSE_COOKIE": "/.hakurei/pulse-cookie",
- }, nil), nil},
-
- {"success cookie", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spPulseOp)
- }
- return &spPulseOp{Cookie: (*[pulseCookieSizeMax]byte)(sampleCookie), CookieSize: pulseCookieSizeMax}
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/cookie", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubFi{isDir: false, size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"loading %d bytes from %q", []any{1 << 8, "/proc/nonexistent/cookie"}}, nil, nil),
- call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
- }, newI().
- // state.ensureRuntimeDir
- Ensure(m(wantRunDirPath), 0700).
- UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
- Ensure(m(wantRuntimePath), 0700).
- UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- // state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
- UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
- // toSystem
- Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantRuntimeSharePath+"/pulse"), m("/run/user/1000/pulse/native"), 0).
- Place(m("/.hakurei/pulse-cookie"), sampleCookie),
- }, paramsWantEnv(config, map[string]string{
- "PULSE_SERVER": "unix:/run/user/1000/pulse/native",
- "PULSE_COOKIE": "/.hakurei/pulse-cookie",
- }, nil), nil},
-
- {"success", func(bool, bool) outcomeOp {
- return new(spPulseOp)
- }, hst.Template, nil, []stub.Call{
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse"}, (*stubFi)(nil), nil),
- call("stat", stub.ExpectArgs{wantRuntimePath + "/pulse/native"}, &stubFi{mode: 0666}, nil),
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"cannot locate PulseAudio cookie (tried $PULSE_COOKIE, $XDG_CONFIG_HOME/pulse/cookie, $HOME/.pulse-cookie)"}}, nil, nil),
- }, newI().
- // state.ensureRuntimeDir
- Ensure(m(wantRunDirPath), 0700).
- UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
- Ensure(m(wantRuntimePath), 0700).
- UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- // state.runtime
- Ephemeral(system.Process, m(wantRuntimeSharePath), 0700).
- UpdatePerm(m(wantRuntimeSharePath), acl.Execute).
- // toSystem
- Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantRuntimeSharePath+"/pulse"), m("/run/user/1000/pulse/native"), 0),
- }, paramsWantEnv(config, map[string]string{
- "PULSE_SERVER": "unix:/run/user/1000/pulse/native",
- }, nil), nil},
- })
-}
-
-func TestDiscoverPulseCookie(t *testing.T) {
- t.Parallel()
-
- fCheckPathname := func(k *kstub) error {
- a, err := discoverPulseCookie(k)
- k.Verbose(a)
- return err
- }
-
- checkSimple(t, "discoverPulseCookie", []simpleTestCase{
- {"override notAbs", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "proc/nonexistent/pulse-cookie", nil),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, &hst.AppError{
- Step: "locate PulseAudio cookie",
- Err: &check.AbsoluteError{Pathname: "proc/nonexistent/pulse-cookie"},
- }},
-
- {"success override", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, "/proc/nonexistent/pulse-cookie", nil),
- call("verbose", stub.ExpectArgs{[]any{m("/proc/nonexistent/pulse-cookie")}}, nil, nil),
- }}, nil},
-
- {"home notAbs", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, "proc/nonexistent/home", nil),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, &hst.AppError{
- Step: "locate PulseAudio cookie",
- Err: &check.AbsoluteError{Pathname: "proc/nonexistent/home"},
- }},
-
- {"home stat", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, "/proc/nonexistent/home", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/home/.pulse-cookie"}, (*stubFi)(nil), stub.UniqueError(1)),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, &hst.AppError{
- Step: "access PulseAudio cookie",
- Err: stub.UniqueError(1),
- }},
-
- {"home nonexistent", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, "/proc/nonexistent/home", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/home/.pulse-cookie"}, (*stubFi)(nil), os.ErrNotExist),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, nil},
-
- {"success home", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, "/proc/nonexistent/home", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/home/.pulse-cookie"}, &stubFi{}, nil),
- call("verbose", stub.ExpectArgs{[]any{m("/proc/nonexistent/home/.pulse-cookie")}}, nil, nil),
- }}, nil},
-
- {"xdg notAbs", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, "proc/nonexistent/xdg", nil),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, &hst.AppError{
- Step: "locate PulseAudio cookie",
- Err: &check.AbsoluteError{Pathname: "proc/nonexistent/xdg"},
- }},
-
- {"xdg stat", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, "/proc/nonexistent/xdg", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/xdg/pulse/cookie"}, (*stubFi)(nil), stub.UniqueError(0)),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, &hst.AppError{
- Step: "access PulseAudio cookie",
- Err: stub.UniqueError(0),
- }},
-
- {"xdg dir", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, "/proc/nonexistent/xdg", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/xdg/pulse/cookie"}, &stubFi{isDir: true}, nil),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, nil},
-
- {"success home dir xdg nonexistent", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, "/proc/nonexistent/home", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/home/.pulse-cookie"}, &stubFi{isDir: true}, nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, "/proc/nonexistent/xdg", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/xdg/pulse/cookie"}, (*stubFi)(nil), os.ErrNotExist),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, nil},
-
- {"success home nonexistent xdg", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, "/proc/nonexistent/home", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/home/.pulse-cookie"}, (*stubFi)(nil), os.ErrNotExist),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, "/proc/nonexistent/xdg", nil),
- call("stat", stub.ExpectArgs{"/proc/nonexistent/xdg/pulse/cookie"}, &stubFi{}, nil),
- call("verbose", stub.ExpectArgs{[]any{m("/proc/nonexistent/xdg/pulse/cookie")}}, nil, nil),
- }}, nil},
-
- {"success empty environ", fCheckPathname, stub.Expect{Calls: []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"PULSE_COOKIE"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"HOME"}, nil, nil),
- call("lookupEnv", stub.ExpectArgs{"XDG_CONFIG_HOME"}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{(*check.Absolute)(nil)}}, nil, nil),
- }}, nil},
- })
-}
-
-func TestLoadFile(t *testing.T) {
- t.Parallel()
-
- fAfterWriteExact := func(k *kstub) error {
- buf := make([]byte, 1<<8)
- n, err := loadFile(k, k,
- "simulated PulseAudio cookie",
- "/home/ophestra/xdg/config/pulse/cookie",
- buf)
- k.Verbose(buf[:n])
- return err
- }
-
- fAfterWrite := func(k *kstub) error {
- buf := make([]byte, 1<<8+0xfd)
- n, err := loadFile(k, k,
- "simulated PulseAudio cookie",
- "/home/ophestra/xdg/config/pulse/cookie",
- buf)
- k.Verbose(buf[:n])
- return err
- }
-
- fBeforeWrite := func(k *kstub) error {
- buf := make([]byte, 1<<8+0xfd)
- n, err := loadFile(k, k,
- "simulated PulseAudio cookie",
- "/home/ophestra/xdg/config/pulse/cookie",
- buf)
- k.Verbose(n)
-
- if !bytes.Equal(buf, make([]byte, len(buf))) {
- t.Errorf("loadFile: buf = %#v", buf)
- }
- return err
- }
-
- sampleCookie := bytes.Repeat([]byte{0xfc}, pulseCookieSizeMax)
- checkSimple(t, "loadFile", []simpleTestCase{
- {"buf", func(k *kstub) error {
- n, err := loadFile(k, k,
- "simulated PulseAudio cookie",
- "/home/ophestra/xdg/config/pulse/cookie",
- nil)
- k.Verbose(n)
- return err
- }, stub.Expect{Calls: []stub.Call{
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, errors.New("invalid buffer")},
-
- {"stat", fBeforeWrite, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, (*stubFi)(nil), stub.UniqueError(3)),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{
- Step: "access simulated PulseAudio cookie",
- Err: stub.UniqueError(3),
- }},
-
- {"dir", fBeforeWrite, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubFi{isDir: true}, nil),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{
- Step: "read simulated PulseAudio cookie",
- Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.EISDIR},
- }},
-
- {"oob", fBeforeWrite, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubFi{size: 1<<8 + 0xff}, nil),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{
- Step: "finalise",
- Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.ENOMEM},
- Msg: `simulated PulseAudio cookie at "/home/ophestra/xdg/config/pulse/cookie" exceeds expected size`,
- }},
-
- {"open", fBeforeWrite, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubFi{size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"%s at %q is %d bytes shorter than expected", []any{"simulated PulseAudio cookie", "/home/ophestra/xdg/config/pulse/cookie", int64(0xfd)}}, nil, nil),
- call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, (*stubOsFile)(nil), stub.UniqueError(2)),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{Step: "open simulated PulseAudio cookie", Err: stub.UniqueError(2)}},
-
- {"read", fBeforeWrite, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubFi{size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"%s at %q is %d bytes shorter than expected", []any{"simulated PulseAudio cookie", "/home/ophestra/xdg/config/pulse/cookie", int64(0xfd)}}, nil, nil),
- call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubOsFile{Reader: errorReader{stub.UniqueError(1)}}, nil),
- call("verbose", stub.ExpectArgs{[]any{-1}}, nil, nil),
- }}, &hst.AppError{Step: "read simulated PulseAudio cookie", Err: stub.UniqueError(1)}},
-
- {"short close", fAfterWrite, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubFi{size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"%s at %q is %d bytes shorter than expected", []any{"simulated PulseAudio cookie", "/home/ophestra/xdg/config/pulse/cookie", int64(0xfd)}}, nil, nil),
- call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubOsFile{closeErr: stub.UniqueError(0), Reader: bytes.NewReader(sampleCookie)}, nil),
- call("verbose", stub.ExpectArgs{[]any{sampleCookie}}, nil, nil),
- }}, &hst.AppError{Step: "close simulated PulseAudio cookie", Err: stub.UniqueError(0)}},
-
- {"success", fAfterWriteExact, stub.Expect{Calls: []stub.Call{
- call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubFi{size: 1 << 8}, nil),
- call("verbosef", stub.ExpectArgs{"loading %d bytes from %q", []any{1 << 8, "/home/ophestra/xdg/config/pulse/cookie"}}, nil, nil),
- call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil),
- call("verbose", stub.ExpectArgs{[]any{sampleCookie}}, nil, nil),
- }}, nil},
- })
-}
diff --git a/internal/app/spruntime.go b/internal/app/spruntime.go
deleted file mode 100644
index 25c04b83..00000000
--- a/internal/app/spruntime.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package app
-
-import (
- "encoding/gob"
-
- "hakurei.app/container/check"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
-)
-
-const (
- /*
- Path to a user-private user-writable directory that is bound
- to the user login time on the machine. It is automatically
- created the first time a user logs in and removed on the
- user's final logout. If a user logs in twice at the same time,
- both sessions will see the same $XDG_RUNTIME_DIR and the same
- contents. If a user logs in once, then logs out again, and
- logs in again, the directory contents will have been lost in
- between, but applications should not rely on this behavior and
- must be able to deal with stale files. To store
- session-private data in this directory, the user should
- include the value of $XDG_SESSION_ID in the filename. This
- directory shall be used for runtime file system objects such
- as AF_UNIX sockets, FIFOs, PID files and similar. It is
- guaranteed that this directory is local and offers the
- greatest possible file system feature set the operating system
- provides. For further details, see the XDG Base Directory
- Specification[3]. $XDG_RUNTIME_DIR is not set if the current
- user is not the original user of the session.
- */
- envXDGRuntimeDir = "XDG_RUNTIME_DIR"
-
- /*
- The session class. This may be used instead of class= on the
- module parameter line, and is usually preferred.
- */
- envXDGSessionClass = "XDG_SESSION_CLASS"
-
- /*
- A regular interactive user session. This is the default class
- for sessions for which a TTY or X display is known at session
- registration time.
- */
- xdgSessionClassUser = "user"
-
- /*
- The session type. This may be used instead of type= on the
- module parameter line, and is usually preferred.
-
- One of "unspecified", "tty", "x11", "wayland", "mir", or "web".
- */
- envXDGSessionType = "XDG_SESSION_TYPE"
-)
-
-func init() { gob.Register(new(spRuntimeOp)) }
-
-const (
- sessionTypeUnspec = iota
- sessionTypeTTY
- sessionTypeX11
- sessionTypeWayland
-)
-
-// spRuntimeOp sets up XDG_RUNTIME_DIR inside the container.
-type spRuntimeOp struct {
- // SessionType determines the value of envXDGSessionType. Populated during toSystem.
- SessionType uintptr
-}
-
-func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error {
- if state.Container.Flags&hst.FShareRuntime != 0 {
- runtimeDir, runtimeDirInst := s.commonPaths(state.outcomeState)
- state.sys.Ensure(runtimeDir, 0700)
- state.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
- state.sys.Ensure(runtimeDirInst, 0700)
- state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
- }
-
- if state.et&hst.EWayland != 0 {
- s.SessionType = sessionTypeWayland
- } else if state.et&hst.EX11 != 0 {
- s.SessionType = sessionTypeX11
- } else {
- s.SessionType = sessionTypeTTY
- }
-
- return nil
-}
-
-func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
- state.runtimeDir = fhs.AbsRunUser.Append(state.mapuid.String())
- state.env[envXDGRuntimeDir] = state.runtimeDir.String()
- state.env[envXDGSessionClass] = xdgSessionClassUser
-
- switch s.SessionType {
- case sessionTypeUnspec:
- state.env[envXDGSessionType] = "unspecified"
- case sessionTypeTTY:
- state.env[envXDGSessionType] = "tty"
- case sessionTypeX11:
- state.env[envXDGSessionType] = "x11"
- case sessionTypeWayland:
- state.env[envXDGSessionType] = "wayland"
-
- }
-
- state.params.Tmpfs(fhs.AbsRunUser, 1<<12, 0755)
- if state.Container.Flags&hst.FShareRuntime != 0 {
- _, runtimeDirInst := s.commonPaths(state.outcomeState)
- state.params.Bind(runtimeDirInst, state.runtimeDir, comp.BindWritable)
- } else {
- state.params.Mkdir(state.runtimeDir, 0700)
- }
- return nil
-}
-
-func (s *spRuntimeOp) commonPaths(state *outcomeState) (runtimeDir, runtimeDirInst *check.Absolute) {
- runtimeDir = state.sc.SharePath.Append("runtime")
- runtimeDirInst = runtimeDir.Append(state.identity.String())
- return
-}
diff --git a/internal/app/spruntime_test.go b/internal/app/spruntime_test.go
deleted file mode 100644
index 1675a7e5..00000000
--- a/internal/app/spruntime_test.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package app
-
-import (
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
-)
-
-func TestSpRuntimeOp(t *testing.T) {
- t.Parallel()
- config := hst.Template()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"success zero", func(isShim bool, clearUnexported bool) outcomeOp {
- if !isShim {
- return new(spRuntimeOp)
- }
- op := &spRuntimeOp{sessionTypeTTY}
- if clearUnexported {
- op.SessionType = sessionTypeUnspec
- }
- return op
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements = 0
- return c
- }, nil, []stub.Call{
- // this op configures the system state and does not make calls during toSystem
- }, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
- Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
- }, paramsWantEnv(config, map[string]string{
- "XDG_RUNTIME_DIR": "/run/user/1000",
- "XDG_SESSION_CLASS": "user",
- "XDG_SESSION_TYPE": "unspecified",
- }, nil), nil},
-
- {"success tty", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spRuntimeOp)
- }
- return &spRuntimeOp{sessionTypeTTY}
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements = 0
- return c
- }, nil, []stub.Call{
- // this op configures the system state and does not make calls during toSystem
- }, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
- Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
- }, paramsWantEnv(config, map[string]string{
- "XDG_RUNTIME_DIR": "/run/user/1000",
- "XDG_SESSION_CLASS": "user",
- "XDG_SESSION_TYPE": "tty",
- }, nil), nil},
-
- {"success x11", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spRuntimeOp)
- }
- return &spRuntimeOp{sessionTypeX11}
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements = hst.Enablements(hst.EX11)
- return c
- }, nil, []stub.Call{
- // this op configures the system state and does not make calls during toSystem
- }, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
- Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
- }, paramsWantEnv(config, map[string]string{
- "XDG_RUNTIME_DIR": "/run/user/1000",
- "XDG_SESSION_CLASS": "user",
- "XDG_SESSION_TYPE": "x11",
- }, nil), nil},
-
- {"success", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spRuntimeOp)
- }
- return &spRuntimeOp{sessionTypeWayland}
- }, hst.Template, nil, []stub.Call{
- // this op configures the system state and does not make calls during toSystem
- }, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
- Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
- }, paramsWantEnv(config, map[string]string{
- "XDG_RUNTIME_DIR": "/run/user/1000",
- "XDG_SESSION_CLASS": "user",
- "XDG_SESSION_TYPE": "wayland",
- }, nil), nil},
- })
-}
diff --git a/internal/app/sptmpdir.go b/internal/app/sptmpdir.go
deleted file mode 100644
index 5b7b54b9..00000000
--- a/internal/app/sptmpdir.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package app
-
-import (
- "encoding/gob"
-
- "hakurei.app/container/check"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
-)
-
-func init() { gob.Register(spTmpdirOp{}) }
-
-// spTmpdirOp sets up TMPDIR inside the container.
-type spTmpdirOp struct{}
-
-func (s spTmpdirOp) toSystem(state *outcomeStateSys) error {
- if state.Container.Flags&hst.FShareTmpdir != 0 {
- tmpdir, tmpdirInst := s.commonPaths(state.outcomeState)
- state.sys.Ensure(tmpdir, 0700)
- state.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
- state.sys.Ensure(tmpdirInst, 01700)
- state.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
- }
- return nil
-}
-
-func (s spTmpdirOp) toContainer(state *outcomeStateParams) error {
- if state.Container.Flags&hst.FShareTmpdir != 0 {
- _, tmpdirInst := s.commonPaths(state.outcomeState)
- state.params.Bind(tmpdirInst, fhs.AbsTmp, comp.BindWritable)
- } else {
- state.params.Tmpfs(fhs.AbsTmp, 0, 01777)
- }
- return nil
-}
-
-func (s spTmpdirOp) commonPaths(state *outcomeState) (tmpdir, tmpdirInst *check.Absolute) {
- tmpdir = state.sc.SharePath.Append("tmpdir")
- tmpdirInst = tmpdir.Append(state.identity.String())
- return
-}
diff --git a/internal/app/sptmpdir_test.go b/internal/app/sptmpdir_test.go
deleted file mode 100644
index 53b44fbe..00000000
--- a/internal/app/sptmpdir_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package app
-
-import (
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/comp"
- "hakurei.app/container/fhs"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
-)
-
-func TestSpTmpdirOp(t *testing.T) {
- t.Parallel()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"success", func(bool, bool) outcomeOp {
- return spTmpdirOp{}
- }, hst.Template, nil, []stub.Call{
- // this op configures the system state and does not make calls during toSystem
- }, newI().
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), 0700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), acl.Execute).
- Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), 01700).
- UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), fhs.AbsTmp, comp.BindWritable),
- }, nil, nil},
- })
-}
diff --git a/internal/app/spwayland.go b/internal/app/spwayland.go
deleted file mode 100644
index d8c18386..00000000
--- a/internal/app/spwayland.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package app
-
-import (
- "encoding/gob"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/system/acl"
- "hakurei.app/system/wayland"
-)
-
-func init() { gob.Register(new(spWaylandOp)) }
-
-// spWaylandOp exports the Wayland display server to the container.
-// Runs after spRuntimeOp.
-type spWaylandOp struct {
- // Path to host wayland socket. Populated during toSystem if DirectWayland is true.
- SocketPath *check.Absolute
-}
-
-func (s *spWaylandOp) toSystem(state *outcomeStateSys) error {
- if state.et&hst.EWayland == 0 {
- return errNotEnabled
- }
-
- // outer wayland socket (usually `/run/user/%d/wayland-%d`)
- var socketPath *check.Absolute
- if name, ok := state.k.lookupEnv(wayland.WaylandDisplay); !ok {
- state.msg.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
- socketPath = state.sc.RuntimePath.Append(wayland.FallbackName)
- } else if a, err := check.NewAbs(name); err != nil {
- socketPath = state.sc.RuntimePath.Append(name)
- } else {
- socketPath = a
- }
-
- if !state.directWayland { // set up security-context-v1
- appId := state.appId
- if appId == "" {
- // use instance ID in case app id is not set
- appId = "app.hakurei." + state.id.String()
- }
- // downstream socket paths
- state.sys.Wayland(state.instance().Append("wayland"), socketPath, appId, state.id.String())
- } else { // bind mount wayland socket (insecure)
- state.msg.Verbose("direct wayland access, PROCEED WITH CAUTION")
- state.ensureRuntimeDir()
- s.SocketPath = socketPath
- state.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
- }
- return nil
-}
-
-func (s *spWaylandOp) toContainer(state *outcomeStateParams) error {
- innerPath := state.runtimeDir.Append(wayland.FallbackName)
- state.env[wayland.WaylandDisplay] = wayland.FallbackName
- if s.SocketPath == nil {
- state.params.Bind(state.instancePath().Append("wayland"), innerPath, 0)
- } else {
- state.params.Bind(s.SocketPath, innerPath, 0)
- }
- return nil
-}
diff --git a/internal/app/spwayland_test.go b/internal/app/spwayland_test.go
deleted file mode 100644
index 89c81473..00000000
--- a/internal/app/spwayland_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package app
-
-import (
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/system"
- "hakurei.app/system/acl"
- "hakurei.app/system/wayland"
-)
-
-func TestSpWaylandOp(t *testing.T) {
- t.Parallel()
- config := hst.Template()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"not enabled", func(bool, bool) outcomeOp {
- return new(spWaylandOp)
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements = 0
- return c
- }, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
-
- {"success notAbs defaultAppId", func(bool, bool) outcomeOp {
- return new(spWaylandOp)
- }, func() *hst.Config {
- c := hst.Template()
- c.ID = ""
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"WAYLAND_DISPLAY"}, "wayland-1", nil),
- }, newI().
- // state.instance
- Ephemeral(system.Process, m(wantInstancePrefix), 0711).
- // toSystem
- Wayland(
- m(wantInstancePrefix+"/wayland"),
- m(wantRuntimePath+"/wayland-1"),
- "app.hakurei."+wantAutoEtcPrefix,
- wantAutoEtcPrefix,
- ), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantInstancePrefix+"/wayland"), m("/run/user/1000/wayland-0"), 0),
- }, paramsWantEnv(config, map[string]string{
- wayland.WaylandDisplay: wayland.FallbackName,
- }, nil), nil},
-
- {"success direct", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spWaylandOp)
- }
- return &spWaylandOp{SocketPath: m("/proc/nonexistent/wayland")}
- }, func() *hst.Config {
- c := hst.Template()
- c.DirectWayland = true
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"WAYLAND_DISPLAY"}, "/proc/nonexistent/wayland", nil),
- call("verbose", stub.ExpectArgs{[]any{"direct wayland access, PROCEED WITH CAUTION"}}, nil, nil),
- }, newI().
- // state.ensureRuntimeDir
- Ensure(m(wantRunDirPath), 0700).
- UpdatePermType(system.User, m(wantRunDirPath), acl.Execute).
- Ensure(m(wantRuntimePath), 0700).
- UpdatePermType(system.User, m(wantRuntimePath), acl.Execute).
- // toSystem
- UpdatePermType(hst.EWayland, m("/proc/nonexistent/wayland"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m("/proc/nonexistent/wayland"), m("/run/user/1000/wayland-0"), 0),
- }, paramsWantEnv(config, map[string]string{
- wayland.WaylandDisplay: wayland.FallbackName,
- }, nil), nil},
-
- {"success", func(bool, bool) outcomeOp {
- return new(spWaylandOp)
- }, hst.Template, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"WAYLAND_DISPLAY"}, nil, nil),
- call("verbose", stub.ExpectArgs{[]any{"WAYLAND_DISPLAY is not set, assuming wayland-0"}}, nil, nil),
- }, newI().
- // state.instance
- Ephemeral(system.Process, m(wantInstancePrefix), 0711).
- // toSystem
- Wayland(
- m(wantInstancePrefix+"/wayland"),
- m(wantRuntimePath+"/"+wayland.FallbackName),
- "org.chromium.Chromium",
- wantAutoEtcPrefix,
- ), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(m(wantInstancePrefix+"/wayland"), m("/run/user/1000/wayland-0"), 0),
- }, paramsWantEnv(config, map[string]string{
- wayland.WaylandDisplay: wayland.FallbackName,
- }, nil), nil},
- })
-}
diff --git a/internal/app/spx11.go b/internal/app/spx11.go
deleted file mode 100644
index 109cf0c0..00000000
--- a/internal/app/spx11.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package app
-
-import (
- "encoding/gob"
- "errors"
- "fmt"
- "io/fs"
- "strconv"
- "strings"
-
- "hakurei.app/container/check"
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/system/acl"
-)
-
-var absX11SocketDir = fhs.AbsTmp.Append(".X11-unix")
-
-func init() { gob.Register(new(spX11Op)) }
-
-// spX11Op exports the X11 display server to the container.
-type spX11Op struct {
- // Value of $DISPLAY, stored during toSystem
- Display string
-}
-
-func (s *spX11Op) toSystem(state *outcomeStateSys) error {
- if state.et&hst.EX11 == 0 {
- return errNotEnabled
- }
-
- if d, ok := state.k.lookupEnv("DISPLAY"); !ok {
- return newWithMessage("DISPLAY is not set")
- } else {
- s.Display = d
- }
-
- // the socket file at `/tmp/.X11-unix/X%d` is typically owned by the priv user
- // and not accessible by the target user
- var socketPath *check.Absolute
- if len(s.Display) > 1 && s.Display[0] == ':' { // `:%d`
- if n, err := strconv.Atoi(s.Display[1:]); err == nil && n >= 0 {
- socketPath = absX11SocketDir.Append("X" + strconv.Itoa(n))
- }
- } else if len(s.Display) > 5 && strings.HasPrefix(s.Display, "unix:") { // `unix:%s`
- if a, err := check.NewAbs(s.Display[5:]); err == nil {
- socketPath = a
- }
- }
- if socketPath != nil {
- if _, err := state.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 {
- state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute)
- if state.Container.Flags&hst.FHostAbstract == 0 {
- s.Display = "unix:" + socketPath.String()
- }
- }
- }
-
- state.sys.ChangeHosts("#" + state.uid.String())
- return nil
-}
-
-func (s *spX11Op) toContainer(state *outcomeStateParams) error {
- state.env["DISPLAY"] = s.Display
- state.params.Bind(absX11SocketDir, absX11SocketDir, 0)
- return nil
-}
diff --git a/internal/app/spx11_test.go b/internal/app/spx11_test.go
deleted file mode 100644
index 0486a1e0..00000000
--- a/internal/app/spx11_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package app
-
-import (
- "os"
- "testing"
-
- "hakurei.app/container"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/system/acl"
-)
-
-func TestSpX11Op(t *testing.T) {
- t.Parallel()
- config := hst.Template()
-
- checkOpBehaviour(t, []opBehaviourTestCase{
- {"not enabled", func(bool, bool) outcomeOp {
- return new(spX11Op)
- }, hst.Template, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
-
- {"lookupEnv", func(bool, bool) outcomeOp {
- return new(spX11Op)
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements |= hst.Enablements(hst.EX11)
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, nil, nil),
- }, nil, nil, &hst.AppError{
- Step: "finalise",
- Err: os.ErrInvalid,
- Msg: "DISPLAY is not set",
- }, nil, nil, nil, nil, nil},
-
- {"abs stat", func(bool, bool) outcomeOp {
- return new(spX11Op)
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements |= hst.Enablements(hst.EX11)
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, "unix:/tmp/.X11-unix/X0", nil),
- call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), stub.UniqueError(0)),
- }, nil, nil, &hst.AppError{
- Step: `access X11 socket "/tmp/.X11-unix/X0"`,
- Err: stub.UniqueError(0),
- }, nil, nil, nil, nil, nil},
-
- {"success abs nonexistent", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spX11Op)
- }
- return &spX11Op{Display: "unix:/tmp/.X11-unix/X0"}
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements |= hst.Enablements(hst.EX11)
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, "unix:/tmp/.X11-unix/X0", nil),
- call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), os.ErrNotExist),
- }, newI().
- ChangeHosts("#1000009"), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(absX11SocketDir, absX11SocketDir, 0),
- }, paramsWantEnv(config, map[string]string{
- "DISPLAY": "unix:/tmp/.X11-unix/X0",
- }, nil), nil},
-
- {"success abs abstract", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spX11Op)
- }
- return &spX11Op{Display: "unix:/tmp/.X11-unix/X0"}
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements |= hst.Enablements(hst.EX11)
- c.Container.Flags &= ^hst.FHostAbstract
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, "unix:/tmp/.X11-unix/X0", nil),
- call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), nil),
- }, newI().
- UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read, acl.Write, acl.Execute).
- ChangeHosts("#1000009"), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(absX11SocketDir, absX11SocketDir, 0),
- }, paramsWantEnv(config, map[string]string{
- "DISPLAY": "unix:/tmp/.X11-unix/X0",
- }, nil), nil},
-
- {"success", func(isShim, _ bool) outcomeOp {
- if !isShim {
- return new(spX11Op)
- }
- return &spX11Op{Display: ":0"}
- }, func() *hst.Config {
- c := hst.Template()
- *c.Enablements |= hst.Enablements(hst.EX11)
- return c
- }, nil, []stub.Call{
- call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, ":0", nil),
- call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), nil),
- }, newI().
- UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read, acl.Write, acl.Execute).
- ChangeHosts("#1000009"), nil, nil, insertsOps(nil), []stub.Call{
- // this op configures the container state and does not make calls during toContainer
- }, &container.Params{
- Ops: new(container.Ops).
- Bind(absX11SocketDir, absX11SocketDir, 0),
- }, paramsWantEnv(config, map[string]string{
- "DISPLAY": ":0",
- }, nil), nil},
- })
-}
diff --git a/internal/app/state/data.go b/internal/app/state/data.go
deleted file mode 100644
index a680c96b..00000000
--- a/internal/app/state/data.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package state
-
-import (
- "encoding/gob"
- "fmt"
- "io"
- "os"
-
- "hakurei.app/hst"
-)
-
-// entryEncode encodes [hst.State] into [io.Writer] with the state entry header.
-// entryEncode does not validate the embedded [hst.Config] value.
-//
-// A non-nil error returned by entryEncode is of type [hst.AppError].
-func entryEncode(w io.Writer, s *hst.State) error {
- if err := entryWriteHeader(w, s.Enablements.Unwrap()); err != nil {
- return &hst.AppError{Step: "encode state header", Err: err}
- } else if err = gob.NewEncoder(w).Encode(s); err != nil {
- return &hst.AppError{Step: "encode state body", Err: err}
- } else {
- return nil
- }
-}
-
-// entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error.
-func entryDecodeHeader(r io.Reader) (hst.Enablement, error) {
- if et, err := entryReadHeader(r); err != nil {
- return 0, &hst.AppError{Step: "decode state header", Err: err}
- } else {
- return et, nil
- }
-}
-
-// entryDecode decodes [hst.State] from [io.Reader] and stores the result in the value pointed to by p.
-// entryDecode validates the embedded [hst.Config] value.
-//
-// A non-nil error returned by entryDecode is of type [hst.AppError].
-func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) {
- if et, err := entryDecodeHeader(r); err != nil {
- return et, err
- } else if err = gob.NewDecoder(r).Decode(&p); err != nil {
- return et, &hst.AppError{Step: "decode state body", Err: err}
- } else if err = p.Config.Validate(); err != nil {
- return et, err
- } else if p.Enablements.Unwrap() != et {
- return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid,
- Msg: fmt.Sprintf("state entry %s has unexpected enablement byte %#x, %#x", p.ID.String(), byte(p.Enablements.Unwrap()), byte(et))}
- } else {
- return et, nil
- }
-}
diff --git a/internal/app/state/data_test.go b/internal/app/state/data_test.go
deleted file mode 100644
index 6ad91e53..00000000
--- a/internal/app/state/data_test.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package state
-
-import (
- "bytes"
- "encoding/gob"
- "errors"
- "io"
- "os"
- "reflect"
- "strings"
- "testing"
- "time"
-
- "hakurei.app/container/stub"
- "hakurei.app/hst"
-)
-
-func TestEntryData(t *testing.T) {
- t.Parallel()
-
- mustEncodeGob := func(e any) string {
- var buf bytes.Buffer
- if err := gob.NewEncoder(&buf).Encode(e); err != nil {
- t.Fatalf("cannot encode invalid state: %v", err)
- return "\x00" // not reached
- } else {
- return buf.String()
- }
- }
- templateStateGob := mustEncodeGob(newTemplateState())
-
- testCases := []struct {
- name string
- data string
- s *hst.State
- err error
- }{
- {"invalid header", "\x00\xff\xca\xfe\xff\xff\xff\x00", nil, &hst.AppError{
- Step: "decode state header", Err: errors.New("unexpected revision ffff")}},
-
- {"invalid gob", "\x00\xff\xca\xfe\x00\x00\xff\x00", nil, &hst.AppError{
- Step: "decode state body", Err: io.EOF}},
-
- {"invalid config", "\x00\xff\xca\xfe\x00\x00\xff\x00" + mustEncodeGob(new(hst.State)), new(hst.State), &hst.AppError{
- Step: "validate configuration", Err: hst.ErrConfigNull,
- Msg: "invalid configuration"}},
-
- {"inconsistent enablement", "\x00\xff\xca\xfe\x00\x00\xff\x00" + templateStateGob, newTemplateState(), &hst.AppError{
- Step: "validate state enablement", Err: os.ErrInvalid,
- Msg: "state entry aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa has unexpected enablement byte 0xd, 0xff"}},
-
- {"template", "\x00\xff\xca\xfe\x00\x00\x0d\xf2" + templateStateGob, newTemplateState(), nil},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- t.Run("encode", func(t *testing.T) {
- if tc.s == nil || tc.s.Config == nil {
- return
- }
- t.Parallel()
-
- var buf bytes.Buffer
- if err := entryEncode(&buf, tc.s); err != nil {
- t.Fatalf("entryEncode: error = %v", err)
- }
-
- if tc.err == nil {
- // Gob encoding is not guaranteed to be deterministic.
- // While the current implementation mostly is, it has randomised order
- // for iterating over maps, and hst.Config holds a map for environ.
- var got hst.State
- if et, err := entryDecode(&buf, &got); err != nil {
- t.Fatalf("entryDecode: error = %v", err)
- } else if stateEt := got.Enablements.Unwrap(); et != stateEt {
- t.Fatalf("entryDecode: et = %x, state %x", et, stateEt)
- }
- if !reflect.DeepEqual(&got, tc.s) {
- t.Errorf("entryEncode: %x", buf.Bytes())
- }
- } else if testing.Verbose() {
- t.Logf("%x", buf.String())
- }
- })
-
- t.Run("decode", func(t *testing.T) {
- t.Parallel()
-
- var got hst.State
- if et, err := entryDecode(strings.NewReader(tc.data), &got); !reflect.DeepEqual(err, tc.err) {
- t.Fatalf("entryDecode: error = %#v, want %#v", err, tc.err)
- } else if err != nil {
- return
- } else if stateEt := got.Enablements.Unwrap(); et != stateEt {
- t.Fatalf("entryDecode: et = %x, state %x", et, stateEt)
- }
-
- if !reflect.DeepEqual(&got, tc.s) {
- t.Errorf("entryDecode: %#v, want %#v", &got, tc.s)
- }
- })
- })
- }
-
- t.Run("encode fault", func(t *testing.T) {
- t.Parallel()
- s := newTemplateState()
-
- t.Run("gob", func(t *testing.T) {
- var want = &hst.AppError{Step: "encode state body", Err: stub.UniqueError(0xcafe)}
- if err := entryEncode(stubNErrorWriter(entryHeaderSize), s); !reflect.DeepEqual(err, want) {
- t.Errorf("entryEncode: error = %#v, want %#v", err, want)
- }
- })
-
- t.Run("header", func(t *testing.T) {
- var want = &hst.AppError{Step: "encode state header", Err: stub.UniqueError(0xcafe)}
- if err := entryEncode(stubNErrorWriter(entryHeaderSize-1), s); !reflect.DeepEqual(err, want) {
- t.Errorf("entryEncode: error = %#v, want %#v", err, want)
- }
- })
- })
-}
-
-// newTemplateState returns the address of a new template [hst.State] struct.
-func newTemplateState() *hst.State {
- return &hst.State{
- ID: hst.ID(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))),
- PID: 0xcafebabe,
- ShimPID: 0xdeadbeef,
- Config: hst.Template(),
- Time: time.Unix(0, 0),
- }
-}
-
-// stubNErrorWriter returns an error for writes above a certain size.
-type stubNErrorWriter int
-
-func (w stubNErrorWriter) Write(p []byte) (n int, err error) {
- if len(p) > int(w) {
- return int(w), stub.UniqueError(0xcafe)
- }
- return io.Discard.Write(p)
-}
diff --git a/internal/app/state/header.go b/internal/app/state/header.go
deleted file mode 100644
index 25b1e6af..00000000
--- a/internal/app/state/header.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package state
-
-import (
- "encoding/hex"
- "errors"
- "io"
- "os"
- "strconv"
- "syscall"
-
- "hakurei.app/hst"
-)
-
-const (
- // entryHeaderMagic are magic bytes at the beginning of the state entry file.
- entryHeaderMagic = "\x00\xff\xca\xfe"
- // entryHeaderRevision follows entryHeaderMagic and is incremented for revisions of the format.
- entryHeaderRevision = "\x00\x00"
- // entryHeaderSize is the fixed size of the header in bytes, including the enablement byte and its complement.
- entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2
-)
-
-// entryHeaderEncode encodes a state entry header for a [hst.Enablement] byte.
-func entryHeaderEncode(et hst.Enablement) *[entryHeaderSize]byte {
- data := [entryHeaderSize]byte([]byte(
- entryHeaderMagic + entryHeaderRevision + string([]hst.Enablement{et, ^et}),
- ))
- return &data
-}
-
-// entryHeaderDecode validates a state entry header and returns the [hst.Enablement] byte.
-func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
- if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic {
- return 0, errors.New("invalid header " + hex.EncodeToString(magic))
- }
- if revision := data[len(entryHeaderMagic):len(entryHeaderMagic+entryHeaderRevision)]; string(revision) != entryHeaderRevision {
- return 0, errors.New("unexpected revision " + hex.EncodeToString(revision))
- }
-
- et := data[len(entryHeaderMagic+entryHeaderRevision)]
- if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] {
- return 0, errors.New("header enablement value is inconsistent")
- }
- return hst.Enablement(et), nil
-}
-
-// EntrySizeError is returned for a file too small to hold a state entry header.
-type EntrySizeError struct {
- Name string
- Size int64
-}
-
-func (e *EntrySizeError) Error() string {
- if e.Name == "" {
- return "state entry file is too short"
- }
- return "state entry file " + strconv.Quote(e.Name) + " is too short"
-}
-
-// entryCheckFile checks whether [os.FileInfo] refers to a file that might hold [hst.State].
-func entryCheckFile(fi os.FileInfo) error {
- if fi.IsDir() {
- return syscall.EISDIR
- }
- if s := fi.Size(); s <= int64(entryHeaderSize) {
- return &EntrySizeError{Name: fi.Name(), Size: s}
- }
- return nil
-}
-
-// entryReadHeader reads [hst.Enablement] from an [io.Reader].
-func entryReadHeader(r io.Reader) (hst.Enablement, error) {
- var data [entryHeaderSize]byte
- if n, err := r.Read(data[:]); err != nil {
- return 0, err
- } else if n != entryHeaderSize {
- return 0, &EntrySizeError{Size: int64(n)}
- }
- return entryHeaderDecode(&data)
-}
-
-// entryWriteHeader writes [hst.Enablement] header to an [io.Writer].
-func entryWriteHeader(w io.Writer, et hst.Enablement) error {
- _, err := w.Write(entryHeaderEncode(et)[:])
- return err
-}
diff --git a/internal/app/state/header_test.go b/internal/app/state/header_test.go
deleted file mode 100644
index 18a620c2..00000000
--- a/internal/app/state/header_test.go
+++ /dev/null
@@ -1,184 +0,0 @@
-package state
-
-import (
- "bytes"
- "errors"
- "io"
- "io/fs"
- "os"
- "reflect"
- "syscall"
- "testing"
- "time"
-
- "hakurei.app/hst"
-)
-
-func TestEntryHeader(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- data [entryHeaderSize]byte
- et hst.Enablement
- err error
- }{
- {"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
- 0x0a, 0xf6}, 0,
- errors.New("header enablement value is inconsistent")},
- {"unexpected revision", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0xff, 0xff}, 0,
- errors.New("unexpected revision ffff")},
- {"invalid header", [entryHeaderSize]byte{0x00, 0xfe, 0xca, 0xfe}, 0,
- errors.New("invalid header 00fecafe")},
-
- {"success high", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
- 0xff, 0x00}, 0xff, nil},
- {"success", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
- 0x09, 0xf6}, hst.EWayland | hst.EPulse, nil},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- t.Run("encode", func(t *testing.T) {
- if tc.err != nil {
- return
- }
- t.Parallel()
-
- if got := entryHeaderEncode(tc.et); *got != tc.data {
- t.Errorf("entryHeaderEncode: %x, want %x", *got, tc.data)
- }
-
- t.Run("write", func(t *testing.T) {
- var buf bytes.Buffer
- if err := entryWriteHeader(&buf, tc.et); err != nil {
- t.Fatalf("entryWriteHeader: error = %v", err)
- }
- if got := ([entryHeaderSize]byte)(buf.Bytes()); got != tc.data {
- t.Errorf("entryWriteHeader: %x, want %x", got, tc.data)
- }
- })
- })
-
- t.Run("decode", func(t *testing.T) {
- t.Parallel()
-
- got, err := entryHeaderDecode(&tc.data)
- if !reflect.DeepEqual(err, tc.err) {
- t.Fatalf("entryHeaderDecode: error = %#v, want %#v", err, tc.err)
- }
- if err != nil {
- return
- }
- if got != tc.et {
- t.Errorf("entryHeaderDecode: et = %q, want %q", got, tc.et)
- }
-
- if got, err = entryReadHeader(bytes.NewReader(tc.data[:])); err != nil {
- t.Fatalf("entryReadHeader: error = %#v", err)
- } else if got != tc.et {
- t.Errorf("entryReadHeader: et = %q, want %q", got, tc.et)
- }
- })
- })
- }
-}
-
-func TestEntrySizeError(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- err error
- want string
- }{
- {"size only", &EntrySizeError{Size: 0xdeadbeef},
- `state entry file is too short`},
- {"full", &EntrySizeError{Name: "nonexistent", Size: 0xdeadbeef},
- `state entry file "nonexistent" is too short`},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- if got := tc.err.Error(); got != tc.want {
- t.Errorf("Error: %s, want %s", got, tc.want)
- }
- })
- }
-}
-
-func TestEntryCheckFile(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- fi os.FileInfo
- err error
- }{
- {"dir", &stubFi{name: "dir", isDir: true},
- syscall.EISDIR},
- {"short", stubFi{name: "short", size: 8},
- &EntrySizeError{Name: "short", Size: 8}},
- {"success", stubFi{size: 9}, nil},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- if err := entryCheckFile(tc.fi); !reflect.DeepEqual(err, tc.err) {
- t.Errorf("entryCheckFile: error = %#v, want %#v", err, tc.err)
- }
- })
- }
-}
-
-func TestEntryReadHeader(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- newR func() io.Reader
- err error
- }{
- {"eof", func() io.Reader { return bytes.NewReader([]byte{}) }, io.EOF},
- {"short", func() io.Reader { return bytes.NewReader([]byte{0}) }, &EntrySizeError{Size: 1}},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- if _, err := entryReadHeader(tc.newR()); !reflect.DeepEqual(err, tc.err) {
- t.Errorf("entryReadHeader: error = %#v, want %#v", err, tc.err)
- }
- })
- }
-}
-
-// stubFi partially implements [os.FileInfo] using hardcoded values.
-type stubFi struct {
- name string
- size int64
- isDir bool
-}
-
-func (fi stubFi) Name() string {
- if fi.name == "" {
- panic("unreachable")
- }
- return fi.name
-}
-
-func (fi stubFi) Size() int64 {
- if fi.size < 0 {
- panic("unreachable")
- }
- return fi.size
-}
-
-func (fi stubFi) IsDir() bool { return fi.isDir }
-
-func (fi stubFi) Mode() fs.FileMode { panic("unreachable") }
-func (fi stubFi) ModTime() time.Time { panic("unreachable") }
-func (fi stubFi) Sys() any { panic("unreachable") }
diff --git a/internal/app/state/join.go b/internal/app/state/join.go
deleted file mode 100644
index 43a19b1d..00000000
--- a/internal/app/state/join.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package state
-
-import (
- "errors"
- "maps"
-
- "hakurei.app/hst"
-)
-
-var (
- ErrDuplicate = errors.New("store contains duplicates")
-)
-
-/*
-Joiner is the interface that wraps the Join method.
-
-The Join function uses Joiner if available.
-*/
-type Joiner interface {
- Join() (map[hst.ID]*hst.State, error)
-}
-
-// Join returns joined state entries of all active identities.
-func Join(s Store) (map[hst.ID]*hst.State, error) {
- if j, ok := s.(Joiner); ok {
- return j.Join()
- }
-
- var (
- aids []int
- entries = make(map[hst.ID]*hst.State)
-
- el int
- res map[hst.ID]*hst.State
- loadErr error
- )
-
- if ln, err := s.List(); err != nil {
- return nil, err
- } else {
- aids = ln
- }
-
- for _, aid := range aids {
- if _, err := s.Do(aid, func(c Cursor) {
- res, loadErr = c.Load()
- }); err != nil {
- return nil, err
- }
-
- if loadErr != nil {
- return nil, loadErr
- }
-
- // save expected length
- el = len(entries) + len(res)
- maps.Copy(entries, res)
- if len(entries) != el {
- return nil, ErrDuplicate
- }
- }
-
- return entries, nil
-}
diff --git a/internal/app/state/segment.go b/internal/app/state/segment.go
deleted file mode 100644
index c8ab8c13..00000000
--- a/internal/app/state/segment.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package state
-
-import (
- "errors"
- "fmt"
- "iter"
- "os"
- "strconv"
- "sync"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/internal/lockedfile"
-)
-
-// stateEntryHandle is a handle on a state entry retrieved from a storeHandle.
-// Must only be used while its parent storeHandle.fileMu is held.
-type stateEntryHandle struct {
- // Error returned while decoding pathname.
- // A non-nil value disables stateEntryHandle.
- decodeErr error
-
- // Checked path to entry file.
- pathname *check.Absolute
-
- hst.ID
-}
-
-// open opens the underlying state entry file, returning [hst.AppError] for a non-nil error.
-func (eh *stateEntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
- if eh.decodeErr != nil {
- return nil, eh.decodeErr
- }
-
- if f, err := os.OpenFile(eh.pathname.String(), flag, perm); err != nil {
- return nil, &hst.AppError{Step: "open state entry", Err: err}
- } else {
- return f, nil
- }
-}
-
-// destroy removes the underlying state entry file, returning [hst.AppError] for a non-nil error.
-func (eh *stateEntryHandle) destroy() error {
- // destroy does not go through open
- if eh.decodeErr != nil {
- return eh.decodeErr
- }
-
- if err := os.Remove(eh.pathname.String()); err != nil {
- return &hst.AppError{Step: "destroy state entry", Err: err}
- }
- return nil
-}
-
-// save encodes [hst.State] and writes it to the underlying file.
-// An error is returned if a file already exists with the same identifier.
-// save does not validate the embedded [hst.Config].
-func (eh *stateEntryHandle) save(state *hst.State) error {
- f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
- if err != nil {
- return err
- }
-
- err = entryEncode(f, state)
- if closeErr := f.Close(); closeErr != nil && err == nil {
- err = &hst.AppError{Step: "close state file", Err: closeErr}
- }
- return err
-}
-
-// load loads and validates the state entry header, and returns the [hst.Enablement] byte.
-// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
-// load validates the embedded hst.Config value.
-func (eh *stateEntryHandle) load(v *hst.State) (hst.Enablement, error) {
- f, err := eh.open(os.O_RDONLY, 0)
- if err != nil {
- return 0, err
- }
-
- var et hst.Enablement
- if v != nil {
- et, err = entryDecode(f, v)
- if err == nil && v.ID != eh.ID {
- err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
- Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
- }
- } else {
- et, err = entryDecodeHeader(f)
- }
-
- if closeErr := f.Close(); closeErr != nil && err == nil {
- err = &hst.AppError{Step: "close state file", Err: closeErr}
- }
- return et, err
-}
-
-// storeHandle is a handle on a stateStore segment.
-// Initialised by stateStore.identityHandle.
-type storeHandle struct {
- // Identity of instances tracked by this segment.
- identity int
- // Pathname of directory that the segment referred to by storeHandle is rooted in.
- path *check.Absolute
- // Inter-process mutex to synchronise operations against resources in this segment.
- fileMu *lockedfile.Mutex
-
- // Must be held alongside fileMu.
- mu sync.Mutex
-}
-
-// entries returns an iterator over all stateEntryHandle held in this segment.
-// Must be called while holding a lock on mu and fileMu.
-// A non-nil error attached to a stateEntryHandle indicates a malformed identifier and is of type [hst.AppError].
-// A non-nil error returned by entries is of type [hst.AppError].
-func (h *storeHandle) entries() (iter.Seq[*stateEntryHandle], int, error) {
- // for error reporting
- const step = "read store segment entries"
-
- // read directory contents, should only contain storeMutexName and identifier
- var entries []os.DirEntry
- if pl, err := os.ReadDir(h.path.String()); err != nil {
- return nil, -1, &hst.AppError{Step: step, Err: err}
- } else {
- entries = pl
- }
-
- // expects lock file
- l := len(entries)
- if l > 0 {
- l--
- }
-
- return func(yield func(*stateEntryHandle) bool) {
- for _, ent := range entries {
- var eh = stateEntryHandle{pathname: h.path.Append(ent.Name())}
-
- // this should never happen
- if ent.IsDir() {
- eh.decodeErr = &hst.AppError{Step: step,
- Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
- goto out
- }
-
- // silently skip lock file
- if ent.Name() == storeMutexName {
- continue
- }
-
- // this either indicates a serious bug or external interference
- if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
- eh.decodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
- goto out
- }
-
- out:
- if !yield(&eh) {
- break
- }
- }
- }, l, nil
-}
diff --git a/internal/app/state/segment_test.go b/internal/app/state/segment_test.go
deleted file mode 100644
index 834cb99a..00000000
--- a/internal/app/state/segment_test.go
+++ /dev/null
@@ -1,259 +0,0 @@
-package state
-
-import (
- "errors"
- "iter"
- "os"
- "reflect"
- "slices"
- "strings"
- "syscall"
- "testing"
-
- "hakurei.app/container/check"
- "hakurei.app/container/stub"
- "hakurei.app/hst"
- "hakurei.app/internal/lockedfile"
-)
-
-func TestStateEntryHandle(t *testing.T) {
- t.Parallel()
-
- t.Run("lockout", func(t *testing.T) {
- t.Parallel()
- wantErr := func() error { return stub.UniqueError(0) }
- eh := stateEntryHandle{decodeErr: wantErr(), pathname: check.MustAbs("/proc/nonexistent")}
-
- if _, err := eh.open(-1, 0); !reflect.DeepEqual(err, wantErr()) {
- t.Errorf("open: error = %v, want %v", err, wantErr())
- }
- if err := eh.destroy(); !reflect.DeepEqual(err, wantErr()) {
- t.Errorf("destroy: error = %v, want %v", err, wantErr())
- }
- if err := eh.save(nil); !reflect.DeepEqual(err, wantErr()) {
- t.Errorf("save: error = %v, want %v", err, wantErr())
- }
- if _, err := eh.load(nil); !reflect.DeepEqual(err, wantErr()) {
- t.Errorf("load: error = %v, want %v", err, wantErr())
- }
- })
-
- t.Run("od", func(t *testing.T) {
- t.Parallel()
-
- {
- eh := stateEntryHandle{pathname: check.MustAbs(t.TempDir()).Append("entry")}
- if f, err := eh.open(os.O_CREATE|syscall.O_EXCL, 0); err != nil {
- t.Fatalf("open: error = %v", err)
- } else if err = f.Close(); err != nil {
- t.Errorf("Close: error = %v", err)
- }
- if err := eh.destroy(); err != nil {
- t.Fatalf("destroy: error = %v", err)
- }
- }
-
- t.Run("nonexistent", func(t *testing.T) {
- t.Parallel()
- eh := stateEntryHandle{pathname: check.MustAbs("/proc/nonexistent")}
-
- wantErrOpen := &hst.AppError{Step: "open state entry",
- Err: &os.PathError{Op: "open", Path: "/proc/nonexistent", Err: syscall.ENOENT}}
- if _, err := eh.open(os.O_CREATE|syscall.O_EXCL, 0); !reflect.DeepEqual(err, wantErrOpen) {
- t.Errorf("open: error = %#v, want %#v", err, wantErrOpen)
- }
-
- wantErrDestroy := &hst.AppError{Step: "destroy state entry",
- Err: &os.PathError{Op: "remove", Path: "/proc/nonexistent", Err: syscall.ENOENT}}
- if err := eh.destroy(); !reflect.DeepEqual(err, wantErrDestroy) {
- t.Errorf("destroy: error = %#v, want %#v", err, wantErrDestroy)
- }
- })
- })
-
- t.Run("saveload", func(t *testing.T) {
- t.Parallel()
- eh := stateEntryHandle{pathname: check.MustAbs(t.TempDir()).Append("entry"),
- ID: newTemplateState().ID}
-
- if err := eh.save(newTemplateState()); err != nil {
- t.Fatalf("save: error = %v", err)
- }
-
- t.Run("validate", func(t *testing.T) {
- t.Parallel()
-
- t.Run("internal", func(t *testing.T) {
- t.Parallel()
-
- var got hst.State
- if f, err := os.Open(eh.pathname.String()); err != nil {
- t.Fatal(err.Error())
- } else if _, err = entryDecode(f, &got); err != nil {
- t.Fatalf("entryDecode: error = %v", err)
- } else if err = f.Close(); err != nil {
- t.Fatal(f.Close())
- }
-
- if want := newTemplateState(); !reflect.DeepEqual(&got, want) {
- t.Errorf("entryDecode: %#v, want %#v", &got, want)
- }
- })
-
- t.Run("load header only", func(t *testing.T) {
- t.Parallel()
-
- if et, err := eh.load(nil); err != nil {
- t.Fatalf("load: error = %v", err)
- } else if want := newTemplateState().Enablements.Unwrap(); et != want {
- t.Errorf("load: et = %x, want %x", et, want)
- }
- })
-
- t.Run("load", func(t *testing.T) {
- t.Parallel()
-
- var got hst.State
- if _, err := eh.load(&got); err != nil {
- t.Fatalf("load: error = %v", err)
- } else if want := newTemplateState(); !reflect.DeepEqual(&got, want) {
- t.Errorf("load: %#v, want %#v", &got, want)
- }
- })
-
- t.Run("load inconsistent", func(t *testing.T) {
- t.Parallel()
- wantErr := &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
- Msg: "state entry 00000000000000000000000000000000 has unexpected id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
-
- ehi := stateEntryHandle{pathname: eh.pathname}
- if _, err := ehi.load(new(hst.State)); !reflect.DeepEqual(err, wantErr) {
- t.Errorf("load: error = %#v, want %#v", err, wantErr)
- }
- })
- })
- })
-}
-
-func TestStoreHandle(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- ents [2][]string
- want func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle
- ext func(t *testing.T, entries iter.Seq[*stateEntryHandle], n int)
- }{
- {"errors", [2][]string{{
- "e81eb203b4190ac5c3842ef44d429945",
- "lock",
- "f0-invalid",
- }, {
- "f1-directory",
- }}, func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle {
- return []*stateEntryHandle{
- newEh(nil, "e81eb203b4190ac5c3842ef44d429945"),
- newEh(&hst.AppError{Step: "decode store segment entry",
- Err: hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength}}, "f0-invalid"),
- newEh(&hst.AppError{Step: "read store segment entries",
- Err: errors.New(`unexpected directory "f1-directory" in store`)}, "f1-directory"),
- }
- }, nil},
-
- {"success", [2][]string{{
- "e81eb203b4190ac5c3842ef44d429945",
- "7958cfbb9272d9cf9cfd61c85afa13f1",
- "d0b5f7446dd5bd3424ff2f7ac9cace1e",
- "c8c8e2c4aea5c32fe47240ff8caa874e",
- "fa0d30b249d80f155a1f80ceddcc32f2",
- "lock",
- }}, func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle {
- return []*stateEntryHandle{
- newEh(nil, "7958cfbb9272d9cf9cfd61c85afa13f1"),
- newEh(nil, "c8c8e2c4aea5c32fe47240ff8caa874e"),
- newEh(nil, "d0b5f7446dd5bd3424ff2f7ac9cace1e"),
- newEh(nil, "e81eb203b4190ac5c3842ef44d429945"),
- newEh(nil, "fa0d30b249d80f155a1f80ceddcc32f2"),
- }
- }, func(t *testing.T, entries iter.Seq[*stateEntryHandle], n int) {
- if n != 5 {
- t.Fatalf("entries: n = %d", n)
- }
-
- // check partial drain
- for range entries {
- break
- }
- }},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- p := check.MustAbs(t.TempDir()).Append("segment")
- if err := os.Mkdir(p.String(), 0700); err != nil {
- t.Fatal(err.Error())
- }
- createEntries(t, p, tc.ents)
-
- var got []*stateEntryHandle
- if entries, n, err := (&storeHandle{
- identity: -0xbad,
- path: p,
- fileMu: lockedfile.MutexAt(p.Append("lock").String()),
- }).entries(); err != nil {
- t.Fatalf("entries: error = %v", err)
- } else {
- got = slices.AppendSeq(make([]*stateEntryHandle, 0, n), entries)
- if tc.ext != nil {
- tc.ext(t, entries, n)
- }
- }
-
- slices.SortFunc(got, func(a, b *stateEntryHandle) int { return strings.Compare(a.pathname.String(), b.pathname.String()) })
- want := tc.want(func(err error, name string) *stateEntryHandle {
- eh := stateEntryHandle{decodeErr: err, pathname: p.Append(name)}
- if err == nil {
- if err = eh.UnmarshalText([]byte(name)); err != nil {
- t.Fatalf("UnmarshalText: error = %v", err)
- }
- }
- return &eh
- })
-
- if !reflect.DeepEqual(got, want) {
- t.Errorf("entries: %q, want %q", got, want)
- }
- })
- }
-
- t.Run("nonexistent", func(t *testing.T) {
- var wantErr = &hst.AppError{Step: "read store segment entries", Err: &os.PathError{
- Op: "open",
- Path: "/proc/nonexistent",
- Err: syscall.ENOENT,
- }}
- if _, _, err := (&storeHandle{
- identity: -0xbad,
- path: check.MustAbs("/proc/nonexistent"),
- }).entries(); !reflect.DeepEqual(err, wantErr) {
- t.Fatalf("entries: error = %#v, want %#v", err, wantErr)
- }
- })
-}
-
-// createEntries creates file and directory entries in the specified prefix.
-func createEntries(t *testing.T, prefix *check.Absolute, ents [2][]string) {
- for _, s := range ents[0] {
- if f, err := os.OpenFile(prefix.Append(s).String(), os.O_CREATE|os.O_EXCL, 0600); err != nil {
- t.Fatal(err.Error())
- } else if err = f.Close(); err != nil {
- t.Fatal(err.Error())
- }
- }
- for _, s := range ents[1] {
- if err := os.Mkdir(prefix.Append(s).String(), 0700); err != nil {
- t.Fatal(err.Error())
- }
- }
-}
diff --git a/internal/app/state/state.go b/internal/app/state/state.go
deleted file mode 100644
index 62b31865..00000000
--- a/internal/app/state/state.go
+++ /dev/null
@@ -1,131 +0,0 @@
-// Package state provides cross-process state tracking for hakurei container instances.
-package state
-
-import (
- "strconv"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/message"
-)
-
-/* this provides an implementation of Store on top of the improved state tracking to ease in the changes */
-
-type Store interface {
- // Do calls f exactly once and ensures store exclusivity until f returns.
- // Returns whether f is called and any errors during the locking process.
- // Cursor provided to f becomes invalid as soon as f returns.
- Do(identity int, f func(c Cursor)) (ok bool, err error)
-
- // List queries the store and returns a list of identities known to the store.
- // Note that some or all returned identities might not have any active apps.
- List() (identities []int, err error)
-}
-
-func (s *stateStore) Do(identity int, f func(c Cursor)) (bool, error) {
- if h, err := s.identityHandle(identity); err != nil {
- return false, err
- } else {
- return h.do(f)
- }
-}
-
-// storeAdapter satisfies [Store] via stateStore.
-type storeAdapter struct {
- msg message.Msg
- *stateStore
-}
-
-func (s storeAdapter) List() ([]int, error) {
- segments, n, err := s.segments()
- if err != nil {
- return nil, err
- }
-
- identities := make([]int, 0, n)
- for si := range segments {
- if si.err != nil {
- if m, ok := message.GetMessage(err); ok {
- s.msg.Verbose(m)
- } else {
- // unreachable
- return nil, err
- }
- continue
- }
- identities = append(identities, si.identity)
- }
- return identities, nil
-}
-
-// NewMulti returns an instance of the multi-file store.
-func NewMulti(msg message.Msg, prefix *check.Absolute) Store {
- return storeAdapter{msg, newStore(prefix.Append("state"))}
-}
-
-// Cursor provides access to the store of an identity.
-type Cursor interface {
- Save(state *hst.State) error
- Destroy(id hst.ID) error
- Load() (map[hst.ID]*hst.State, error)
- Len() (int, error)
-}
-
-// do implements stateStore.Do on storeHandle.
-func (h *storeHandle) do(f func(c Cursor)) (bool, error) {
- if unlock, err := h.fileMu.Lock(); err != nil {
- return false, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.identity), Err: err}
- } else {
- defer unlock()
- }
-
- f(h)
- return true, nil
-}
-
-/* these compatibility methods must only be called while fileMu is held */
-
-func (h *storeHandle) Save(state *hst.State) error {
- return (&stateEntryHandle{nil, h.path.Append(state.ID.String()), state.ID}).save(state)
-}
-
-func (h *storeHandle) Destroy(id hst.ID) error {
- return (&stateEntryHandle{nil, h.path.Append(id.String()), id}).destroy()
-}
-
-func (h *storeHandle) Load() (map[hst.ID]*hst.State, error) {
- entries, n, err := h.entries()
- if err != nil {
- return nil, err
- }
-
- r := make(map[hst.ID]*hst.State, n)
- for eh := range entries {
- if eh.decodeErr != nil {
- err = eh.decodeErr
- break
- }
- var s hst.State
- if _, err = eh.load(&s); err != nil {
- break
- }
- r[eh.ID] = &s
- }
- return r, err
-}
-
-func (h *storeHandle) Len() (int, error) {
- entries, _, err := h.entries()
- if err != nil {
- return -1, err
- }
-
- var n int
- for eh := range entries {
- if eh.decodeErr != nil {
- err = eh.decodeErr
- }
- n++
- }
- return n, err
-}
diff --git a/internal/app/state/state_test.go b/internal/app/state/state_test.go
deleted file mode 100644
index c387b2ec..00000000
--- a/internal/app/state/state_test.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package state_test
-
-import (
- "log"
- "math/rand"
- "reflect"
- "slices"
- "testing"
- "time"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/internal/app/state"
- "hakurei.app/message"
-)
-
-func TestMulti(t *testing.T) {
- s := state.NewMulti(message.NewMsg(log.New(log.Writer(), "multi: ", 0)), check.MustAbs(t.TempDir()))
-
- t.Run("list empty store", func(t *testing.T) {
- if identities, err := s.List(); err != nil {
- t.Fatalf("List: error = %v", err)
- } else if len(identities) != 0 {
- t.Fatalf("List: identities = %#v", identities)
- }
- })
-
- const (
- insertEntryChecked = iota
- insertEntryNoCheck
- insertEntryOtherApp
-
- tl
- )
-
- var tc [tl]hst.State
- for i := 0; i < tl; i++ {
- if err := hst.NewInstanceID(&tc[i].ID); err != nil {
- t.Fatalf("cannot create dummy state: %v", err)
- }
- tc[i].PID = rand.Int()
- tc[i].Config = hst.Template()
- tc[i].Time = time.Now()
- }
-
- do := func(identity int, f func(c state.Cursor)) {
- if ok, err := s.Do(identity, f); err != nil {
- t.Fatalf("Do: ok = %v, error = %v", ok, err)
- }
- }
-
- insert := func(i, identity int) {
- do(identity, func(c state.Cursor) {
- if err := c.Save(&tc[i]); err != nil {
- t.Fatalf("Save: error = %v", err)
- }
- })
- }
-
- check := func(i, identity int) {
- do(identity, func(c state.Cursor) {
- if entries, err := c.Load(); err != nil {
- t.Fatalf("Load: error = %v", err)
- } else if got, ok := entries[tc[i].ID]; !ok {
- t.Fatalf("Load: entry %s missing", &tc[i].ID)
- } else {
- got.Time = tc[i].Time
- if !reflect.DeepEqual(got, &tc[i]) {
- t.Fatalf("Load: entry %s got %#v, want %#v", &tc[i].ID, got, &tc[i])
- }
- }
- })
- }
-
- // insert entry checked
- insert(insertEntryChecked, 0)
- check(insertEntryChecked, 0)
-
- // insert entry unchecked
- insert(insertEntryNoCheck, 0)
-
- // insert entry different identity
- insert(insertEntryOtherApp, 1)
- check(insertEntryOtherApp, 1)
-
- // check previous insertion
- check(insertEntryNoCheck, 0)
-
- // list identities
- if identities, err := s.List(); err != nil {
- t.Fatalf("List: error = %v", err)
- } else {
- slices.Sort(identities)
- want := []int{0, 1}
- if !slices.Equal(identities, want) {
- t.Fatalf("List() = %#v, want %#v", identities, want)
- }
- }
-
- // join store
- if entries, err := state.Join(s); err != nil {
- t.Fatalf("Join: error = %v", err)
- } else if len(entries) != 3 {
- t.Fatalf("Join(s) = %#v", entries)
- }
-
- // clear identity 1
- do(1, func(c state.Cursor) {
- if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
- t.Fatalf("Destroy: error = %v", err)
- }
- })
- do(1, func(c state.Cursor) {
- if l, err := c.Len(); err != nil {
- t.Fatalf("Len: error = %v", err)
- } else if l != 0 {
- t.Fatalf("Len: %d, want 0", l)
- }
- })
-}
diff --git a/internal/app/state/store.go b/internal/app/state/store.go
deleted file mode 100644
index 565e4ee5..00000000
--- a/internal/app/state/store.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package state
-
-import (
- "errors"
- "io/fs"
- "iter"
- "os"
- "strconv"
- "sync"
- "syscall"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
- "hakurei.app/internal/lockedfile"
-)
-
-// storeMutexName is the pathname of the file backing [lockedfile.Mutex] of a stateStore and storeHandle.
-const storeMutexName = "lock"
-
-// A stateStore keeps track of [hst.State] via a well-known filesystem accessible to all hakurei priv-side processes.
-// Access to store data and related resources are synchronised on a per-segment basis via storeHandle.
-type stateStore struct {
- // Pathname of directory that the store is rooted in.
- base *check.Absolute
-
- // All currently known instances of storeHandle, keyed by their identity.
- handles sync.Map
-
- // Inter-process mutex to synchronise operations against the entire store.
- // Held during List and when initialising previously unknown identities during Do.
- // Must not be accessed directly. Callers should use the bigLock method instead.
- fileMu *lockedfile.Mutex
-
- // For creating the base directory.
- mkdirOnce sync.Once
- // Stored error value via mkdirOnce.
- mkdirErr error
-}
-
-// bigLock acquires fileMu on stateStore.
-// A non-nil error returned by bigLock is of type [hst.AppError].
-func (s *stateStore) bigLock() (unlock func(), err error) {
- s.mkdirOnce.Do(func() { s.mkdirErr = os.MkdirAll(s.base.String(), 0700) })
- if s.mkdirErr != nil {
- return nil, &hst.AppError{Step: "create state store directory", Err: s.mkdirErr}
- }
-
- if unlock, err = s.fileMu.Lock(); err != nil {
- return nil, &hst.AppError{Step: "acquire lock on the state store", Err: err}
- }
- return
-}
-
-// identityHandle loads or initialises a storeHandle for identity.
-// A non-nil error returned by identityHandle is of type [hst.AppError].
-func (s *stateStore) identityHandle(identity int) (*storeHandle, error) {
- h := new(storeHandle)
- h.mu.Lock()
-
- if v, ok := s.handles.LoadOrStore(identity, h); ok {
- h = v.(*storeHandle)
- } else {
- // acquire big lock to initialise previously unknown segment handle
- if unlock, err := s.bigLock(); err != nil {
- return nil, err
- } else {
- defer unlock()
- }
-
- h.identity = identity
- h.path = s.base.Append(strconv.Itoa(identity))
- h.fileMu = lockedfile.MutexAt(h.path.Append(storeMutexName).String())
-
- err := os.MkdirAll(h.path.String(), 0700)
- h.mu.Unlock()
- if err != nil && !errors.Is(err, fs.ErrExist) {
- // handle methods will likely return ENOENT
- s.handles.CompareAndDelete(identity, h)
- return nil, &hst.AppError{Step: "create store segment directory", Err: err}
- }
- }
- return h, nil
-}
-
-// segmentIdentity is produced by the iterator returned by stateStore.segments.
-type segmentIdentity struct {
- // Identity of the current segment.
- identity int
- // Error encountered while processing this segment.
- err error
-}
-
-// segments returns an iterator over all segmentIdentity known to the store.
-// To obtain a storeHandle on a segment, caller must then call identityHandle.
-// A non-nil error returned by segments is of type [hst.AppError].
-func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) {
- // read directory contents, should only contain storeMutexName and identity
- var entries []os.DirEntry
-
- // acquire big lock to read store segment list
- if unlock, err := s.bigLock(); err != nil {
- return nil, -1, err
- } else {
- entries, err = os.ReadDir(s.base.String())
- unlock()
-
- if err != nil && !errors.Is(err, os.ErrNotExist) {
- return nil, -1, &hst.AppError{Step: "read store segments", Err: err}
- }
- }
-
- // expects lock file
- l := len(entries)
- if l > 0 {
- l--
- }
-
- return func(yield func(segmentIdentity) bool) {
- // for error reporting
- const step = "process store segment"
-
- for _, ent := range entries {
- si := segmentIdentity{identity: -1}
-
- // should only be the big lock
- if !ent.IsDir() {
- if ent.Name() == storeMutexName {
- continue
- }
-
- // this should never happen
- si.err = &hst.AppError{Step: step, Err: syscall.EISDIR,
- Msg: "skipped non-directory entry " + strconv.Quote(ent.Name())}
- goto out
- }
-
- // failure paths either indicates a serious bug or external interference
- if v, err := strconv.Atoi(ent.Name()); err != nil {
- si.err = &hst.AppError{Step: step, Err: err,
- Msg: "skipped non-identity entry " + strconv.Quote(ent.Name())}
- goto out
- } else if v < hst.IdentityMin || v > hst.IdentityMax {
- si.err = &hst.AppError{Step: step, Err: syscall.ERANGE,
- Msg: "skipped out of bounds entry " + strconv.Itoa(v)}
- goto out
- } else {
- si.identity = v
- }
-
- out:
- if !yield(si) {
- break
- }
- }
- }, l, nil
-}
-
-// newStore returns the address of a new instance of stateStore.
-// Multiple instances of stateStore rooted in the same directory is supported, but discouraged.
-func newStore(base *check.Absolute) *stateStore {
- return &stateStore{base: base, fileMu: lockedfile.MutexAt(base.Append(storeMutexName).String())}
-}
diff --git a/internal/app/state/store_test.go b/internal/app/state/store_test.go
deleted file mode 100644
index f58be18f..00000000
--- a/internal/app/state/store_test.go
+++ /dev/null
@@ -1,254 +0,0 @@
-package state
-
-import (
- "cmp"
- "iter"
- "os"
- "reflect"
- "slices"
- "strconv"
- "strings"
- "syscall"
- "testing"
-
- "hakurei.app/container/check"
- "hakurei.app/hst"
-)
-
-func TestStateStoreBigLock(t *testing.T) {
- t.Parallel()
-
- {
- s := newStore(check.MustAbs(t.TempDir()).Append("state"))
- for i := 0; i < 2; i++ { // check once behaviour
- if unlock, err := s.bigLock(); err != nil {
- t.Fatalf("bigLock: error = %v", err)
- } else {
- unlock()
- }
- }
- }
-
- t.Run("mkdir", func(t *testing.T) {
- t.Parallel()
-
- wantErr := &hst.AppError{Step: "create state store directory",
- Err: &os.PathError{Op: "mkdir", Path: "/proc/nonexistent", Err: syscall.ENOENT}}
- for i := 0; i < 2; i++ { // check once behaviour
- if _, err := newStore(check.MustAbs("/proc/nonexistent")).bigLock(); !reflect.DeepEqual(err, wantErr) {
- t.Errorf("bigLock: error = %#v, want %#v", err, wantErr)
- }
- }
- })
-
- t.Run("access", func(t *testing.T) {
- t.Parallel()
-
- base := check.MustAbs(t.TempDir()).Append("inaccessible")
- if err := os.MkdirAll(base.String(), 0); err != nil {
- t.Fatal(err.Error())
- }
-
- wantErr := &hst.AppError{Step: "acquire lock on the state store",
- Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}}
- if _, err := newStore(base).bigLock(); !reflect.DeepEqual(err, wantErr) {
- t.Errorf("bigLock: error = %#v, want %#v", err, wantErr)
- }
- })
-}
-
-func TestStateStoreIdentityHandle(t *testing.T) {
- t.Parallel()
-
- t.Run("loadstore", func(t *testing.T) {
- t.Parallel()
-
- s := newStore(check.MustAbs(t.TempDir()).Append("store"))
-
- var handleAddr [8]*storeHandle
- checkHandle := func(identity int, load bool) {
- if h, err := s.identityHandle(identity); err != nil {
- t.Fatalf("identityHandle: error = %v", err)
- } else if load != (handleAddr[identity] != nil) {
- t.Fatalf("identityHandle: load = %v, want %v", load, handleAddr[identity] != nil)
- } else if !load {
- handleAddr[identity] = h
-
- if h.identity != identity {
- t.Errorf("identityHandle: identity = %d, want %d", h.identity, identity)
- }
- } else if h != handleAddr[identity] {
- t.Fatalf("identityHandle: %p, want %p", h, handleAddr[identity])
- }
- }
-
- checkHandle(0, false)
- checkHandle(1, false)
- checkHandle(2, false)
- checkHandle(3, false)
- checkHandle(7, false)
- checkHandle(7, true)
- checkHandle(2, true)
- checkHandle(1, true)
- checkHandle(2, true)
- checkHandle(0, true)
- })
-
- t.Run("access", func(t *testing.T) {
- t.Parallel()
-
- base := check.MustAbs(t.TempDir()).Append("inaccessible")
- if err := os.MkdirAll(base.String(), 0); err != nil {
- t.Fatal(err.Error())
- }
-
- wantErr := &hst.AppError{Step: "acquire lock on the state store",
- Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}}
- if _, err := newStore(base).identityHandle(0); !reflect.DeepEqual(err, wantErr) {
- t.Errorf("identityHandle: error = %#v, want %#v", err, wantErr)
- }
- })
-
- t.Run("access segment", func(t *testing.T) {
- t.Parallel()
-
- base := check.MustAbs(t.TempDir()).Append("inaccessible")
- if err := os.MkdirAll(base.String(), 0700); err != nil {
- t.Fatal(err.Error())
- }
- if f, err := os.Create(base.Append(storeMutexName).String()); err != nil {
- t.Fatal(err.Error())
- } else if err = f.Close(); err != nil {
- t.Fatal(err.Error())
- }
- if err := os.Chmod(base.String(), 0100); err != nil {
- t.Fatal(err.Error())
- }
- t.Cleanup(func() {
- if err := os.Chmod(base.String(), 0700); err != nil {
- t.Fatal(err.Error())
- }
- })
-
- wantErr := &hst.AppError{Step: "create store segment directory",
- Err: &os.PathError{Op: "mkdir", Path: base.Append("0").String(), Err: syscall.EACCES}}
- if _, err := newStore(base).identityHandle(0); !reflect.DeepEqual(err, wantErr) {
- t.Errorf("identityHandle: error = %#v, want %#v", err, wantErr)
- }
- })
-}
-
-func TestStateStoreSegments(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- name string
- ents [2][]string
- want []segmentIdentity
- ext func(t *testing.T, segments iter.Seq[segmentIdentity], n int)
- }{
- {"errors", [2][]string{{
- "f0-invalid-file",
- }, {
- "f1-invalid-syntax",
- "9999",
- "16384",
- }}, []segmentIdentity{
- {-1, &hst.AppError{Step: "process store segment", Err: syscall.EISDIR,
- Msg: `skipped non-directory entry "f0-invalid-file"`}},
- {-1, &hst.AppError{Step: "process store segment", Err: syscall.ERANGE,
- Msg: `skipped out of bounds entry 16384`}},
- {-1, &hst.AppError{Step: "process store segment",
- Err: &strconv.NumError{Func: "Atoi", Num: "f1-invalid-syntax", Err: strconv.ErrSyntax},
- Msg: `skipped non-identity entry "f1-invalid-syntax"`}},
- {9999, nil},
- }, nil},
-
- {"success", [2][]string{{
- "lock",
- }, {
- "0",
- "1",
- "2",
- "3",
- "4",
- "5",
- "6",
- "7",
- "9",
- "13",
- "20",
- "31",
- "197",
- }}, []segmentIdentity{
- {0, nil},
- {1, nil},
- {2, nil},
- {3, nil},
- {4, nil},
- {5, nil},
- {6, nil},
- {7, nil},
- {9, nil},
- {13, nil},
- {20, nil},
- {31, nil},
- {197, nil},
- }, func(t *testing.T, segments iter.Seq[segmentIdentity], n int) {
- if n != 13 {
- t.Fatalf("segments: n = %d", n)
- }
-
- // check partial drain
- for range segments {
- break
- }
- }},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
-
- base := check.MustAbs(t.TempDir()).Append("store")
- if err := os.Mkdir(base.String(), 0700); err != nil {
- t.Fatal(err.Error())
- }
- createEntries(t, base, tc.ents)
-
- var got []segmentIdentity
- if segments, n, err := newStore(base).segments(); err != nil {
- t.Fatalf("segments: error = %v", err)
- } else {
- got = slices.AppendSeq(make([]segmentIdentity, 0, n), segments)
- if tc.ext != nil {
- tc.ext(t, segments, n)
- }
- }
-
- slices.SortFunc(got, func(a, b segmentIdentity) int {
- if a.identity == b.identity {
- return strings.Compare(a.err.Error(), b.err.Error())
- }
- return cmp.Compare(a.identity, b.identity)
- })
- if !reflect.DeepEqual(got, tc.want) {
- t.Errorf("segments: %#v, want %#v", got, tc.want)
- }
- })
- }
-
- t.Run("access", func(t *testing.T) {
- t.Parallel()
-
- base := check.MustAbs(t.TempDir()).Append("inaccessible")
- if err := os.MkdirAll(base.String(), 0); err != nil {
- t.Fatal(err.Error())
- }
-
- wantErr := &hst.AppError{Step: "acquire lock on the state store",
- Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}}
- if _, _, err := newStore(base).segments(); !reflect.DeepEqual(err, wantErr) {
- t.Errorf("segments: error = %#v, want %#v", err, wantErr)
- }
- })
-}