20 Commits

Author SHA1 Message Date
e47aebb7a0 internal/app/outcome: apply configured filesystems late
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m42s
Test / Hakurei (push) Successful in 2m37s
Test / Hpkg (push) Successful in 3m33s
Test / Sandbox (race detector) (push) Successful in 4m10s
Test / Hakurei (race detector) (push) Successful in 4m49s
Test / Flake checks (push) Successful in 1m29s
This enables configured filesystems to cover system mount points.

Closes #8.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 01:41:52 +09:00
543bf69102 internal/app/spx11: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m16s
Test / Hakurei (push) Successful in 3m7s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m47s
Test / Flake checks (push) Successful in 1m29s
This outcomeOp will likely never change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 01:00:12 +09:00
4cfb1fda8f internal/app/spwayland: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m28s
This op is quite clean. Might get slightly more complex at some point passing socket fd.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-19 00:30:56 +09:00
c12183959a internal/app/dispatcher: report correct field
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m30s
This was mistakenly reporting sharePath on inequivalence causing very confusing output.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 23:59:10 +09:00
f5845e312e internal/app/sptmpdir: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 3m59s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Flake checks (push) Successful in 1m27s
Another simple one. This will change when shared tmpdir and xdg runtime dir becomes optional.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 23:46:10 +09:00
a103c4a7c7 internal/app/hsu: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m4s
Test / Hpkg (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hakurei (race detector) (push) Successful in 4m44s
Test / Flake checks (push) Successful in 1m22s
The stub exec.ExitError is hairy as usual, but internal/app is not cross-platform, so this is okay.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 20:45:42 +09:00
67ec82ae1b ldd/exec: raise timeout
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (push) Successful in 3m4s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 6m9s
Test / Flake checks (push) Successful in 1m28s
This mostly helps with tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 18:03:09 +09:00
f6f0cb56ae internal/app/hsu: remove wrapper method
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Sandbox (race detector) (push) Successful in 3m53s
Test / Hpkg (push) Successful in 3m54s
Test / Hakurei (race detector) (push) Successful in 4m43s
Test / Hakurei (push) Successful in 2m13s
Test / Flake checks (push) Successful in 1m27s
This was added to reduce the size of diffs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 17:35:20 +09:00
d4284c109d internal/app/spruntime: emulate pam_systemd type
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Hakurei (push) Successful in 44s
Test / Hakurei (race detector) (push) Successful in 44s
Test / Hpkg (push) Successful in 42s
Test / Sandbox (push) Successful in 1m42s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Flake checks (push) Successful in 1m22s
This sets XDG_SESSION_TYPE to the corresponding values specified in pam_systemd(8) according to enablements.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 04:33:04 +09:00
030ad2a73b internal/app/spruntime: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 38s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m2s
Test / Hpkg (push) Successful in 4m11s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m25s
This one is quite simple and has no state. Needs to emulate pam_systemd behaviour so that will change.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 03:41:49 +09:00
78d7955abd internal/app/sppulse: check cookie discovery
All checks were successful
Test / Create distribution (push) Successful in 48s
Test / Sandbox (push) Successful in 2m22s
Test / Hakurei (push) Successful in 3m17s
Test / Sandbox (race detector) (push) Successful in 4m13s
Test / Hpkg (push) Successful in 4m18s
Test / Hakurei (race detector) (push) Successful in 5m0s
Test / Flake checks (push) Successful in 1m37s
There's quite a bit of code duplication here, but since this is already quite simple it is best to leave it as is for now.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 01:30:33 +09:00
b066495a7d internal/app/sppulse: check buf error injection
All checks were successful
Test / Create distribution (push) Successful in 54s
Test / Hpkg (push) Successful in 4m16s
Test / Sandbox (push) Successful in 1m45s
Test / Hakurei (push) Successful in 2m27s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m53s
Test / Flake checks (push) Successful in 1m38s
The loadFile behaviour does not guarantee the buffer to be zeroed or not clobbered if an error is returned, but for the current implementation it is good to check.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-18 01:01:52 +09:00
82299d34c6 internal/app/sppulse: correctly handle small cookie
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m6s
Test / Sandbox (race detector) (push) Successful in 3m55s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m46s
Test / Flake checks (push) Successful in 1m19s
The trailing zero bytes need to be sliced off, so send cookie size alongside buffer content.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 08:03:03 +09:00
792013cefb internal/app/sppulse: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m5s
Test / Sandbox (race detector) (push) Successful in 3m58s
Test / Hpkg (push) Successful in 4m9s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m27s
Still needs to check the relocated functions separately.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 06:32:21 +09:00
3f39132935 internal/app/dispatcher: reduce check code duplication
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m6s
Test / Hakurei (push) Successful in 3m3s
Test / Sandbox (race detector) (push) Successful in 3m56s
Test / Hpkg (push) Successful in 3m58s
Test / Hakurei (race detector) (push) Successful in 4m42s
Test / Flake checks (push) Successful in 1m28s
This also improves readability of test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-17 05:47:12 +09:00
c922c3f80e internal/app/sppulse: relocate hard to test code
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m15s
Test / Hakurei (push) Successful in 3m1s
Test / Sandbox (race detector) (push) Successful in 3m59s
Test / Hpkg (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m48s
Test / Flake checks (push) Successful in 1m19s
These are better tested separately instead of creating many op test cases.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 05:47:49 +09:00
6cf58ca1b3 internal/app/spfinal: check behaviour
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 2m10s
Test / Hakurei (push) Successful in 3m2s
Test / Hpkg (push) Successful in 3m56s
Test / Sandbox (race detector) (push) Successful in 4m1s
Test / Hakurei (race detector) (push) Successful in 4m45s
Test / Flake checks (push) Successful in 1m25s
This will be merged with spFilesystemOp eventually.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 02:08:31 +09:00
425421d9b1 hst/container: rename constants
All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m4s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 4m50s
Test / Hpkg (push) Successful in 5m4s
Test / Hakurei (race detector) (push) Successful in 5m38s
Test / Flake checks (push) Successful in 1m30s
The shim is an implementation detail and should not be mentioned in the API.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-16 00:27:00 +09:00
5e0f15d76b hst/container: additional shim exit codes
All checks were successful
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 4m26s
Test / Sandbox (race detector) (push) Successful in 6m36s
Test / Hakurei (push) Successful in 6m58s
Test / Hakurei (race detector) (push) Successful in 8m54s
Test / Hpkg (push) Successful in 9m13s
Test / Flake checks (push) Successful in 3m13s
These are now considered stable, defined behaviour and can be used by external programs to determine shim outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-15 22:09:33 +09:00
ae65491223 container/init: use one channel for wait4
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m12s
Test / Hpkg (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 4m6s
Test / Hakurei (race detector) (push) Successful in 4m51s
Test / Flake checks (push) Successful in 1m31s
When using two channels it is possible for the other case to be reached before all pending winfo are consumed, causing incorrect reporting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-15 21:35:19 +09:00
32 changed files with 1618 additions and 380 deletions

View File

@@ -94,7 +94,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
passwd *user.User
passwdOnce sync.Once
passwdFunc = func() {
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustIDMsg(msg), flagIdentity))
us := strconv.Itoa(app.HsuUid(new(app.Hsu).MustID(msg), flagIdentity))
if u, err := user.LookupId(us); err != nil {
msg.Verbosef("cannot look up uid %s", us)
passwd = &user.User{
@@ -302,7 +302,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil))
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath.String()), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")

View File

@@ -89,7 +89,7 @@ func tryShort(msg message.Msg, name string) (config *hst.Config, entry *state.St
msg.Verbose("argument looks like prefix")
var sc hst.Paths
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID())
app.CopyPaths().Copy(&sc, new(app.Hsu).MustID(nil))
s := state.NewMulti(msg, sc.RunDirPath.String())
if entries, err := state.Join(s); err != nil {
log.Printf("cannot join store: %v", err)

View File

@@ -21,7 +21,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output)
defer t.MustFlush()
info := &hst.Info{User: new(app.Hsu).MustID()}
info := &hst.Info{User: new(app.Hsu).MustID(nil)}
app.CopyPaths().Copy(&info.Paths, info.User)
if flagJSON {

View File

@@ -353,10 +353,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
wpid int
wstatus WaitStatus
}
// info is closed as the wait4 thread terminates
// when there are no longer any processes left to reap
info := make(chan winfo, 1)
done := make(chan struct{})
k.new(func(k syscallDispatcher) {
k.lockOSThread()
var (
err error
wpid = -2
@@ -382,7 +386,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
k.printf(msg, "unexpected wait4 response: %v", err)
}
close(done)
close(info)
})
// handle signals to dump withheld messages
@@ -411,7 +415,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
msg.BeforeExit()
k.exit(0)
case w := <-info:
case w, ok := <-info:
if !ok {
msg.BeforeExit()
k.exit(r)
continue // unreachable
}
if w.wpid == cmd.Process.Pid {
// initial process exited, output is most likely available again
msg.Resume()
@@ -433,10 +443,6 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
}
case <-done:
msg.BeforeExit()
k.exit(r)
case <-timeout:
k.printf(msg, "timeout exceeded waiting for lingering processes")
msg.BeforeExit()

View File

@@ -2081,6 +2081,8 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
// magicWait4Signal as args[4] causes this to block until simulated signal is delivered
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil, magicWait4Signal}, 0xbad, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
@@ -2174,6 +2176,8 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, stub.PanicExit}, 0, syscall.ECHILD),
}}},
@@ -2266,6 +2270,8 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xfade01ce), 0, nil}, 0xbad, nil),
// this terminates the goroutine at the call, preventing it from leaking while preserving behaviour
call("wait4", stub.ExpectArgs{-1, nil, 0, nil, 0xdeadbeef}, 0, syscall.ECHILD),
@@ -2358,6 +2364,8 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xdeaf), 0, nil}, 0xbabe, nil),
@@ -2494,6 +2502,8 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xdeaf), 0, nil}, 0xbabe, nil),
@@ -2634,6 +2644,8 @@ func TestInitEntrypoint(t *testing.T) {
/* wait4 */
Tracks: []stub.Expect{{Calls: []stub.Call{
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, nil, 0, nil}, 0, syscall.EINTR),
call("wait4", stub.ExpectArgs{-1, syscall.WaitStatus(0xdeaf), 0, nil}, 0xbabe, nil),

View File

@@ -24,18 +24,25 @@ const (
IdentityMin = 0
// IdentityMax is the maximum value of [Config.Identity]. This is enforced by cmd/hsu.
IdentityMax = 9999
)
// ShimExitRequest is returned when the priv side process requests shim exit.
ShimExitRequest = 254
// ShimExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
ShimExitOrphan = 3
const (
// ExitFailure is returned if the container fails to start.
ExitFailure = iota + 1
// ExitCancel is returned if the container is terminated by a shim-directed signal which cancels its context.
ExitCancel
// ExitOrphan is returned when the shim is orphaned before priv side delivers a signal.
ExitOrphan
// ExitRequest is returned when the priv side process requests shim exit.
ExitRequest = 254
)
const (
// FMultiarch unblocks syscalls required for multiarch to work on applicable targets.
FMultiarch uintptr = 1 << iota
// FSeccompCompat causes emitted seccomp filter programs to be identical to Flatpak.
// FSeccompCompat changes emitted seccomp filter programs to be identical to that of Flatpak.
FSeccompCompat
// FDevel unblocks ptrace and friends.
FDevel

View File

@@ -103,17 +103,17 @@ func TestApp(t *testing.T) {
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"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), bits.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"), bits.BindWritable|bits.BindDevice|bits.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).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), bits.BindWritable).
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: bits.PresetExt | bits.PresetDenyDevel,
HostNet: true,
@@ -268,7 +268,7 @@ func TestApp(t *testing.T) {
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/65534",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
"XDG_SESSION_TYPE=wayland",
},
Ops: new(container.Ops).
Root(m("/"), bits.BindWritable).
@@ -276,13 +276,6 @@ func TestApp(t *testing.T) {
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
DevWritable(m("/dev/"), true).
Tmpfs(m("/dev/shm"), 0, 01777).
Bind(m("/dev/dri"), m("/dev/dri"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), bits.BindWritable|bits.BindDevice|bits.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).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), bits.BindWritable).
@@ -293,6 +286,13 @@ func TestApp(t *testing.T) {
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"), bits.BindWritable|bits.BindDevice|bits.BindOptional).
Bind(m("/dev/kvm"), m("/dev/kvm"), bits.BindWritable|bits.BindDevice|bits.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: bits.PresetExt | bits.PresetDenyDevel,
HostNet: true,
@@ -420,13 +420,23 @@ func TestApp(t *testing.T) {
"WAYLAND_DISPLAY=wayland-0",
"XDG_RUNTIME_DIR=/run/user/1971",
"XDG_SESSION_CLASS=user",
"XDG_SESSION_TYPE=tty",
"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"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), bits.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).
@@ -441,16 +451,6 @@ func TestApp(t *testing.T) {
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), bits.BindWritable|bits.BindEnsure).
Remount(m("/dev/"), syscall.MS_RDONLY).
Tmpfs(m("/run/user/"), 4096, 0755).
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), bits.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), bits.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).
Remount(m("/"), syscall.MS_RDONLY),
SeccompPresets: bits.PresetExt | bits.PresetDenyTTY | bits.PresetDenyDevel,
HostNet: true,

View File

@@ -2,8 +2,10 @@ package app
import (
"bytes"
"io"
"io/fs"
"log"
"maps"
"os"
"os/exec"
"reflect"
@@ -26,33 +28,135 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
}
// checkExpectUid is the uid value used by checkOpBehaviour to initialise [system.I].
const checkExpectUid = 0xcafebabe
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
// wantAutoEtcPrefix is the autoetc prefix corresponding to checkExpectInstanceId.
const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// 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 [state.ID] value used by checkOpBehaviour to initialise outcomeState.
var checkExpectInstanceId = *(*state.ID)(bytes.Repeat([]byte{0xaa}, len(state.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 func(isShim, clearUnexported bool) outcomeOp
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 func(state *outcomeStateSys)
toSystem []stub.Call
wantSys *system.I
extraCheckSys func(t *testing.T, state *outcomeStateSys)
// 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 func(state *outcomeStateParams)
toContainer []stub.Call
wantParams *container.Params
extraCheckParams func(t *testing.T, state *outcomeStateParams)
// 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()
@@ -63,7 +167,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
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"}, container.Nonexistent+"/xdg_runtime_dir", nil),
call("lookupEnv", stub.ExpectArgs{"XDG_RUNTIME_DIR"}, wantRuntimePath, nil),
call("getuid", stub.ExpectArgs{}, 1000, nil),
call("getgid", stub.ExpectArgs{}, 100, nil),
@@ -169,6 +273,40 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
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]
@@ -189,6 +327,18 @@ func (k *kstub) lookupEnv(key string) (string, bool) {
}
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")
@@ -272,6 +422,32 @@ func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bo
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))
@@ -289,6 +465,11 @@ 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{}

View File

@@ -35,7 +35,8 @@ func (h *Hsu) ensureDispatcher() {
})
}
// ID returns the current user hsurc identifier. ErrHsuAccess is returned if the current user is not in hsurc.
// ID returns the current user hsurc identifier.
// [ErrHsuAccess] is returned if the current user is not in hsurc.
func (h *Hsu) ID() (int, error) {
h.ensureDispatcher()
h.idOnce.Do(func() {
@@ -61,8 +62,8 @@ func (h *Hsu) ID() (int, error) {
} 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 os.IsNotExist(h.idErr) {
h.idErr = &hst.AppError{Step: step, Err: os.ErrNotExist,
} 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)}
}
})
@@ -71,10 +72,7 @@ func (h *Hsu) ID() (int, error) {
}
// MustID calls [Hsu.ID] and terminates on error.
func (h *Hsu) MustID() int { return h.MustIDMsg(nil) }
// MustIDMsg implements MustID with a custom [container.Msg].
func (h *Hsu) MustIDMsg(msg message.Msg) int {
func (h *Hsu) MustID(msg message.Msg) int {
id, err := h.ID()
if err == nil {
return id
@@ -86,16 +84,16 @@ func (h *Hsu) MustIDMsg(msg message.Msg) int {
msg.Verbose("*"+fallback, err)
}
os.Exit(1)
return -0xdeadbeef
return -0xdeadbeef // not reached
} else if m, ok := message.GetMessage(err); ok {
log.Fatal(m)
return -0xdeadbeef
return -0xdeadbeef // not reached
} else {
log.Fatalln(fallback, err)
return -0xdeadbeef
return -0xdeadbeef // not reached
}
}
// HsuUid returns target uid for the stable hsu uid format.
// No bounds check is performed, a value retrieved from hsu is expected.
// No bounds check is performed, a value retrieved by [Hsu] is expected.
func HsuUid(id, identity int) int { return 1000000 + id*10000 + identity }

84
internal/app/hsu_test.go Normal file
View File

@@ -0,0 +1,84 @@
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}
}

View File

@@ -14,6 +14,10 @@ import (
"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.
@@ -78,7 +82,7 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *state.ID, config
Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()},
ID: id,
Identity: config.Identity,
UserID: hsu.MustIDMsg(msg),
UserID: hsu.MustID(msg),
EnvPaths: copyPaths(k),
Container: config.Container,
}
@@ -268,10 +272,7 @@ func (state *outcomeStateSys) toSystem() error {
// must run first
&spParamsOp{},
// TODO(ophestra): move this late for #8 and #9
&spFilesystemOp{},
spRuntimeOp{},
&spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
@@ -281,6 +282,7 @@ func (state *outcomeStateSys) toSystem() error {
&spPulseOp{},
&spDBusOp{},
&spFilesystemOp{},
spFinalOp{},
}

View File

@@ -23,14 +23,11 @@ import (
//#include "shim-signal.h"
import "C"
const (
// setup pipe fd for [container.Receive]
shimEnv = "HAKUREI_SHIM"
// only used for a nil configured env map
envAllocSize = 1 << 6
)
// 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
@@ -39,7 +36,7 @@ type shimParams struct {
// Limits are enforced on the priv side.
WaitDelay time.Duration
// Verbosity pass through from [container.Msg].
// Verbosity pass through from [message.Msg].
Verbose bool
// Outcome setup ops, contains setup state. Populated by outcome.finalise.
@@ -131,12 +128,12 @@ func ShimMain() {
// setup has not completed, terminate immediately
msg.Resume()
os.Exit(hst.ShimExitRequest)
os.Exit(hst.ExitRequest)
return
case 1: // got SIGCONT after adoption: monitor died before delivering signal
msg.BeforeExit()
os.Exit(hst.ShimExitOrphan)
os.Exit(hst.ExitOrphan)
return
case 2: // unreachable
@@ -172,7 +169,7 @@ func ShimMain() {
if err := z.Start(); err != nil {
printMessageError("cannot start container:", err)
os.Exit(1)
os.Exit(hst.ExitFailure)
}
if err := z.Serve(); err != nil {
printMessageError("cannot configure container:", err)
@@ -189,7 +186,7 @@ func ShimMain() {
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) {
os.Exit(2)
os.Exit(hst.ExitCancel)
}
log.Printf("wait: %v", err)
os.Exit(127)

View File

@@ -1,7 +1,6 @@
package app
import (
"maps"
"os"
"syscall"
"testing"
@@ -42,48 +41,32 @@ func TestSpAccountOp(t *testing.T) {
return c
}, nil, []stub.Call{
// this op performs basic validation and does not make calls during toSystem
}, newI(), nil, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
}, []stub.Call{
}, 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")),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"HOME": config.Container.Home.String(),
"USER": config.Container.Username,
"SHELL": config.Container.Shell.String(),
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
}, 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, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
}, []stub.Call{
}, 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")),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"HOME": config.Container.Home.String(),
"USER": config.Container.Username,
"SHELL": config.Container.Shell.String(),
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
}, paramsWantEnv(config, map[string]string{
"HOME": config.Container.Home.String(),
"USER": config.Container.Username,
"SHELL": config.Container.Shell.String(),
}, nil), nil},
})
}

View File

@@ -2,7 +2,6 @@ package app
import (
"errors"
"maps"
"os"
"reflect"
"syscall"
@@ -72,16 +71,9 @@ func TestSpParamsOp(t *testing.T) {
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) {
wantEnv := map[string]string{
"TERM": "xterm",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
}
@@ -90,7 +82,7 @@ func TestSpParamsOp(t *testing.T) {
if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
}
}, nil},
}), nil},
{"success", func(isShim, _ bool) outcomeOp {
if !isShim {
@@ -117,15 +109,9 @@ func TestSpParamsOp(t *testing.T) {
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
Bind(fhs.AbsDev, fhs.AbsDev, bits.BindWritable|bits.BindDevice).
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
}, paramsWantEnv(config, map[string]string{
"TERM": "xterm",
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"TERM": "xterm",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
}
@@ -134,7 +120,7 @@ func TestSpParamsOp(t *testing.T) {
if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
}
}, nil},
}), nil},
})
}
@@ -159,6 +145,16 @@ func TestSpFilesystemOp(t *testing.T) {
}
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)
@@ -310,12 +306,9 @@ func TestSpFilesystemOp(t *testing.T) {
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(), nil, nil, func(state *outcomeStateParams) {
state.filesystem = configSmall.Container.Filesystem
state.params.Ops = new(container.Ops)
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
state.filesystem = append(state.filesystem, hst.FilesystemConfigJSON{})
}, []stub.Call{
}, newI(), 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",
@@ -341,11 +334,9 @@ func TestSpFilesystemOp(t *testing.T) {
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(), nil, nil, func(state *outcomeStateParams) {
}, newI(), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
state.filesystem = configSmall.Container.Filesystem
state.params.Ops = new(container.Ops)
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
}, []stub.Call{
})), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
@@ -386,11 +377,9 @@ func TestSpFilesystemOp(t *testing.T) {
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(), nil, nil, func(state *outcomeStateParams) {
}, newI(), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
state.filesystem = config.Container.Filesystem[1:]
state.params.Ops = new(container.Ops)
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
}, []stub.Call{
})), []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).

View File

@@ -12,6 +12,7 @@ import (
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

View File

@@ -1,8 +1,6 @@
package app
import (
"maps"
"reflect"
"syscall"
"testing"
@@ -18,7 +16,6 @@ import (
func TestSpDBusOp(t *testing.T) {
config := hst.Template()
const instancePrefix = container.Nonexistent + "/tmp/hakurei.0/" + wantAutoEtcPrefix
checkOpBehaviour(t, []opBehaviourTestCase{
{"not enabled", func(bool, bool) outcomeOp {
@@ -41,11 +38,7 @@ func TestSpDBusOp(t *testing.T) {
"unix:path=/run/user/1000/bus",
"unix:path=/var/run/dbus/system_bus_socket",
}, nil),
}, nil, func(t *testing.T, state *outcomeStateSys) {
if want := m(instancePrefix); !reflect.DeepEqual(state.sharePath, want) {
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
}
}, &system.OpError{
}, nil, sysUsesInstance(nil), &system.OpError{
Op: "dbus",
Err: syscall.EINVAL,
Msg: "message bus proxy configuration contains NUL byte",
@@ -66,7 +59,7 @@ func TestSpDBusOp(t *testing.T) {
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
"unix:path=/run/user/1000/bus",
instancePrefix + "/bus",
wantInstancePrefix + "/bus",
"--filter",
"--talk=org.freedesktop.DBus",
"--talk=org.freedesktop.Notifications",
@@ -77,7 +70,7 @@ func TestSpDBusOp(t *testing.T) {
}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
"unix:path=/run/user/1000/bus",
instancePrefix+"/bus",
wantInstancePrefix+"/bus",
"--filter",
"--talk=org.freedesktop.DBus",
"--talk=org.freedesktop.Notifications",
@@ -88,40 +81,25 @@ func TestSpDBusOp(t *testing.T) {
)}}, nil, nil),
}, func() *system.I {
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
sys.Ephemeral(system.Process, m(instancePrefix), 0711)
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", instancePrefix + "/bus"},
dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", instancePrefix + "/system_bus_socket"},
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(instancePrefix+"/bus"), acl.Read, acl.Write)
sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write)
return sys
}(), func(t *testing.T, state *outcomeStateSys) {
if want := m(instancePrefix); !reflect.DeepEqual(state.sharePath, want) {
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
}
}, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
// emulates spRuntimeOp
state.runtimeDir = m("/run/user/1000")
}, []stub.Call{
}(), 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(instancePrefix+"/bus"),
Bind(m(wantInstancePrefix+"/bus"),
m("/run/user/1000/bus"), 0),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
}, paramsWantEnv(config, map[string]string{
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
}, nil), nil},
{"success", func(isShim, _ bool) outcomeOp {
if !isShim {
@@ -136,7 +114,7 @@ func TestSpDBusOp(t *testing.T) {
call("isVerbose", stub.ExpectArgs{}, true, nil),
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
"unix:path=/run/user/1000/bus",
instancePrefix + "/bus",
wantInstancePrefix + "/bus",
"--filter",
"--talk=org.freedesktop.Notifications",
"--talk=org.freedesktop.FileManager1",
@@ -153,7 +131,7 @@ func TestSpDBusOp(t *testing.T) {
}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{
"unix:path=/var/run/dbus/system_bus_socket",
instancePrefix + "/system_bus_socket",
wantInstancePrefix + "/system_bus_socket",
"--filter",
"--talk=org.bluez",
"--talk=org.freedesktop.Avahi",
@@ -161,7 +139,7 @@ func TestSpDBusOp(t *testing.T) {
}}}, nil, nil),
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
"unix:path=/run/user/1000/bus",
instancePrefix+"/bus",
wantInstancePrefix+"/bus",
"--filter",
"--talk=org.freedesktop.Notifications",
"--talk=org.freedesktop.FileManager1",
@@ -177,7 +155,7 @@ func TestSpDBusOp(t *testing.T) {
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
"unix:path=/var/run/dbus/system_bus_socket",
instancePrefix+"/system_bus_socket",
wantInstancePrefix+"/system_bus_socket",
"--filter",
"--talk=org.bluez",
"--talk=org.freedesktop.Avahi",
@@ -185,43 +163,28 @@ func TestSpDBusOp(t *testing.T) {
)}}, nil, nil),
}, func() *system.I {
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
sys.Ephemeral(system.Process, m(instancePrefix), 0711)
sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
if err := sys.ProxyDBus(
config.SessionBus, config.SystemBus,
dbus.ProxyPair{"unix:path=/run/user/1000/bus", instancePrefix + "/bus"},
dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", instancePrefix + "/system_bus_socket"},
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(instancePrefix+"/bus"), acl.Read, acl.Write).
UpdatePerm(m(instancePrefix+"/system_bus_socket"), acl.Read, acl.Write)
sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write).
UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read, acl.Write)
return sys
}(), func(t *testing.T, state *outcomeStateSys) {
if want := m(instancePrefix); !reflect.DeepEqual(state.sharePath, want) {
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
}
}, nil, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
// emulates spRuntimeOp
state.runtimeDir = m("/run/user/1000")
}, []stub.Call{
}(), 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(instancePrefix+"/bus"),
Bind(m(wantInstancePrefix+"/bus"),
m("/run/user/1000/bus"), 0).
Bind(m(instancePrefix+"/system_bus_socket"),
Bind(m(wantInstancePrefix+"/system_bus_socket"),
m("/var/run/dbus/system_bus_socket"), 0),
}, func(t *testing.T, state *outcomeStateParams) {
wantEnv := 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",
}
maps.Copy(wantEnv, config.Container.Env)
if !maps.Equal(state.env, wantEnv) {
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
}
}, nil},
}, 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},
})
}

View File

@@ -0,0 +1,65 @@
package app
import (
"syscall"
"testing"
"hakurei.app/container"
"hakurei.app/container/fhs"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/acl"
)
func TestSpFinalOp(t *testing.T) {
checkOpBehaviour(t, []opBehaviourTestCase{
{"nil extra invalid env", func(bool, bool) outcomeOp {
return spFinalOp{}
}, func() *hst.Config {
c := hst.Template()
// verify nil check behaviour
c.ExtraPerms = append(c.ExtraPerms, hst.ExtraPermConfig{})
// verify toContainer behaviour
c.Container.Env["="] = "\x00"
return c
}, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, 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, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
}, []stub.Call{
// this op configures the container state and does not make calls during toContainer
}, nil, nil, &hst.AppError{
Step: "flatten environment",
Err: syscall.EINVAL,
Msg: "invalid environment variable =",
}},
{"success", func(bool, bool) outcomeOp {
return spFinalOp{}
}, hst.Template, nil, []stub.Call{
// this op configures the system state and does not make calls during toSystem
}, 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, func(state *outcomeStateParams) {
state.params.Ops = new(container.Ops)
}, []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).Remount(fhs.AbsRoot, syscall.MS_RDONLY),
}, nil, nil},
})
}

View File

@@ -7,10 +7,12 @@ import (
"io"
"io/fs"
"os"
"strconv"
"syscall"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/message"
)
const pulseCookieSizeMax = 1 << 8
@@ -18,9 +20,12 @@ 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 {
@@ -34,115 +39,31 @@ func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
}
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
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 newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
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))
}
}
// hard link pulse socket into target-executable share
// 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"))
// publish current user's pulse cookie for target user
var paCookiePath *check.Absolute
{
const paLocateStep = "locate PulseAudio cookie"
// from environment
if p, ok := state.k.lookupEnv("PULSE_COOKIE"); ok {
if a, err := check.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
// this takes precedence, do not verify whether the file is accessible
paCookiePath = a
goto out
}
}
// $HOME/.pulse-cookie
if p, ok := state.k.lookupEnv("HOME"); ok {
if a, err := check.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append(".pulse-cookie")
}
if fi, err := state.k.stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if fi.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
// $XDG_CONFIG_HOME/pulse/cookie
if p, ok := state.k.lookupEnv("XDG_CONFIG_HOME"); ok {
if a, err := check.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err}
} else {
paCookiePath = a.Append("pulse", "cookie")
}
if fi, err := state.k.stat(paCookiePath.String()); err != nil {
paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
}
// fallthrough
} else if fi.IsDir() {
paCookiePath = nil
} else {
goto out
}
}
out:
}
if paCookiePath != nil {
if b, err := state.k.stat(paCookiePath.String()); err != nil {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
} else {
if b.IsDir() {
return &hst.AppError{Step: "read PulseAudio cookie", Err: &os.PathError{Op: "stat", Path: paCookiePath.String(), Err: syscall.EISDIR}}
}
if b.Size() > pulseCookieSizeMax {
return newWithMessageError(
fmt.Sprintf("PulseAudio cookie at %q exceeds maximum expected size", paCookiePath),
&os.PathError{Op: "stat", Path: paCookiePath.String(), Err: syscall.ENOMEM},
)
}
}
var r io.ReadCloser
if f, err := state.k.open(paCookiePath.String()); err != nil {
return &hst.AppError{Step: "open PulseAudio cookie", Err: err}
} else {
r = f
}
// 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 n, err := r.Read(s.Cookie[:]); err != nil {
if !errors.Is(err, io.EOF) {
_ = r.Close()
return &hst.AppError{Step: "read PulseAudio cookie", Err: err}
}
state.msg.Verbosef("copied %d bytes from %q", n, paCookiePath)
}
if err := r.Close(); err != nil {
return &hst.AppError{Step: "close PulseAudio cookie", Err: err}
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 " +
@@ -161,8 +82,12 @@ func (s *spPulseOp) toContainer(state *outcomeStateParams) error {
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[:])
state.params.Place(innerDst, s.Cookie[:s.CookieSize])
}
return nil
@@ -175,3 +100,111 @@ func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSock
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
}
}

View File

@@ -0,0 +1,460 @@
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},
})
}

View File

@@ -6,43 +6,114 @@ import (
"hakurei.app/container/bits"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/system"
"hakurei.app/system/acl"
)
func init() { gob.Register(spRuntimeOp{}) }
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{}
type spRuntimeOp struct {
// SessionType determines the value of envXDGSessionType. Populated during toSystem.
SessionType uintptr
}
func (s spRuntimeOp) toSystem(state *outcomeStateSys) error {
func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error {
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 {
const (
xdgRuntimeDir = "XDG_RUNTIME_DIR"
xdgSessionClass = "XDG_SESSION_CLASS"
xdgSessionType = "XDG_SESSION_TYPE"
)
func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
state.runtimeDir = fhs.AbsRunUser.Append(state.mapuid.String())
state.env[xdgRuntimeDir] = state.runtimeDir.String()
state.env[xdgSessionClass] = "user"
state.env[xdgSessionType] = "tty"
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"
}
_, runtimeDirInst := s.commonPaths(state.outcomeState)
state.params.Tmpfs(fhs.AbsRunUser, 1<<12, 0755)
state.params.Bind(runtimeDirInst, state.runtimeDir, bits.BindWritable)
state.params.
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
Bind(runtimeDirInst, state.runtimeDir, bits.BindWritable)
return nil
}
func (s spRuntimeOp) commonPaths(state *outcomeState) (runtimeDir, runtimeDirInst *check.Absolute) {
func (s *spRuntimeOp) commonPaths(state *outcomeState) (runtimeDir, runtimeDirInst *check.Absolute) {
runtimeDir = state.sc.SharePath.Append("runtime")
runtimeDirInst = runtimeDir.Append(state.identity.String())
return

View File

@@ -0,0 +1,128 @@
package app
import (
"testing"
"hakurei.app/container"
"hakurei.app/container/bits"
"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"), bits.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"), bits.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"), bits.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"), bits.BindWritable),
}, paramsWantEnv(config, map[string]string{
"XDG_RUNTIME_DIR": "/run/user/1000",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_TYPE": "wayland",
}, nil), nil},
})
}

View File

@@ -0,0 +1,34 @@
package app
import (
"testing"
"hakurei.app/container"
"hakurei.app/container/bits"
"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, bits.BindWritable),
}, nil, nil},
})
}

View File

@@ -12,6 +12,7 @@ import (
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

View File

@@ -0,0 +1,104 @@
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},
})
}

119
internal/app/spx11_test.go Normal file
View File

@@ -0,0 +1,119 @@
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},
})
}

View File

@@ -16,17 +16,17 @@ import (
"hakurei.app/message"
)
const (
lddName = "ldd"
lddTimeout = 2 * time.Second
)
var (
msgStatic = []byte("Not a valid dynamic program")
msgStaticGlibc = []byte("not a dynamic executable")
)
func Exec(ctx context.Context, msg message.Msg, p string) ([]*Entry, error) {
const (
lddName = "ldd"
lddTimeout = 4 * time.Second
)
c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel()

View File

@@ -46,7 +46,7 @@ in
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
"XDG_SESSION_TYPE=wayland"
];
fs = fs "dead" {
@@ -218,6 +218,15 @@ in
(ent "/" ignore ignore ignore ignore ignore)
(ent "/" ignore ignore ignore ignore ignore)
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000004,gid=1000004")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/tmp/hakurei.0/runtime/4" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
@@ -233,15 +242,6 @@ in
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000004,gid=1000004")
(ent "/tmp/hakurei.0/runtime/4" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000004,gid=1000004")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
];
seccomp = true;

View File

@@ -54,7 +54,7 @@ in
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/1000"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
"XDG_SESSION_TYPE=wayland"
];
fs = fs "dead" {
@@ -245,6 +245,14 @@ in
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000003,gid=1000003")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
@@ -260,14 +268,6 @@ in
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000003,gid=1000003")
(ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000003,gid=1000003")
(ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
];
seccomp = true;

View File

@@ -161,7 +161,7 @@
(ent "/../../.." "/sys/fs/cgroup" "rw,nosuid,nodev,noexec,relatime" "cgroup2" "cgroup2" "rw,nsdelegate,memory_recursiveprot")
(ent "/" "/sys/fs/pstore" "rw,nosuid,nodev,noexec,relatime" "pstore" "pstore" "rw")
(ent "/" "/sys/fs/bpf" "rw,nosuid,nodev,noexec,relatime" "bpf" "bpf" "rw,mode=700")
# systemd race: tracefs debugfs configfs fusectl
# systemd nondeterminism: tracefs debugfs configfs fusectl
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
(ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw")
@@ -181,16 +181,16 @@
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000000,gid=1000000")
(ent "/kvm" "/dev/kvm" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
(ent "/" "/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
(ent "/" "/run/dbus" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000000,gid=1000000")
(ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000000,gid=1000000")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000000,gid=1000000")
(ent "/kvm" "/dev/kvm" "rw,nosuid" "devtmpfs" "devtmpfs" ignore)
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
(ent "/" "/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
(ent "/" "/run/dbus" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=1000000,gid=1000000")
];
seccomp = true;

View File

@@ -54,7 +54,7 @@ in
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
"XDG_SESSION_TYPE=wayland"
];
fs = fs "dead" {
@@ -243,6 +243,14 @@ in
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000005,gid=1000005")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000005,gid=1000005")
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000005,gid=1000005")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000005,gid=1000005")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
@@ -255,14 +263,6 @@ in
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000005,gid=1000005")
(ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000005,gid=1000005")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000005,gid=1000005")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
];
seccomp = true;

View File

@@ -54,7 +54,7 @@ in
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
"XDG_SESSION_TYPE=wayland"
];
fs = fs "dead" {
@@ -241,6 +241,14 @@ in
(ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000001,gid=1000001")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
(ent "/tmp/hakurei.0/runtime/1" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
@@ -253,14 +261,6 @@ in
(ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000001,gid=1000001")
(ent "/tmp/hakurei.0/runtime/1" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/1" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000001,gid=1000001")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
];
seccomp = true;

View File

@@ -55,7 +55,7 @@ in
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/65534"
"XDG_SESSION_CLASS=user"
"XDG_SESSION_TYPE=tty"
"XDG_SESSION_TYPE=wayland"
];
fs = fs "dead" {
@@ -250,6 +250,15 @@ in
(ent ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666")
(ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw")
(ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=1000002,gid=1000002")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/mnt-root/nix/.ro-store,upperdir=/mnt-root/nix/.rw-store/upper,workdir=/mnt-root/nix/.rw-store/work,uuid=on")
@@ -265,15 +274,6 @@ in
(ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/host/nix/.ro-store:/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr")
(ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=1000002,gid=1000002")
(ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/hakurei.0/tmpdir/2" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=1000002,gid=1000002")
(ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
(ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore)
(ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/disk/by-label/nixos" "rw")
];
seccomp = true;