Compare commits
22 Commits
52e3324ef4
...
c0e860000a
| Author | SHA1 | Date | |
|---|---|---|---|
|
c0e860000a
|
|||
|
d87020f0ca
|
|||
|
e47aebb7a0
|
|||
|
543bf69102
|
|||
|
4cfb1fda8f
|
|||
|
c12183959a
|
|||
|
f5845e312e
|
|||
|
a103c4a7c7
|
|||
|
67ec82ae1b
|
|||
|
f6f0cb56ae
|
|||
|
d4284c109d
|
|||
|
030ad2a73b
|
|||
|
78d7955abd
|
|||
|
b066495a7d
|
|||
|
82299d34c6
|
|||
|
792013cefb
|
|||
|
3f39132935
|
|||
|
c922c3f80e
|
|||
|
6cf58ca1b3
|
|||
|
425421d9b1
|
|||
|
5e0f15d76b
|
|||
|
ae65491223
|
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -3,6 +3,7 @@ package hst
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
)
|
||||
@@ -45,6 +46,9 @@ var (
|
||||
|
||||
// ErrIdentityBounds is returned by [Config.Validate] for an out of bounds [Config.Identity] value.
|
||||
ErrIdentityBounds = errors.New("identity out of bounds")
|
||||
|
||||
// ErrEnviron is returned by [Config.Validate] if an environment variable name contains '=' or NUL.
|
||||
ErrEnviron = errors.New("invalid environment variable name")
|
||||
)
|
||||
|
||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||
@@ -83,6 +87,14 @@ func (config *Config) Validate() error {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}
|
||||
}
|
||||
|
||||
for key := range config.Container.Env {
|
||||
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrEnviron,
|
||||
Msg: "invalid environment variable " + strconv.Quote(key)}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,20 @@ func TestConfigValidate(t *testing.T) {
|
||||
Shell: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}},
|
||||
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM=": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM="`}},
|
||||
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM\x00": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM\x00"`}},
|
||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
const (
|
||||
// checkExpectUid is the uid value used by checkOpBehaviour to initialise [system.I].
|
||||
const checkExpectUid = 0xcafebabe
|
||||
|
||||
checkExpectUid = 0xcafebabe
|
||||
// wantAutoEtcPrefix is the autoetc prefix corresponding to checkExpectInstanceId.
|
||||
const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
// wantInstancePrefix is the SharePath corresponding to checkExpectInstanceId.
|
||||
wantInstancePrefix = container.Nonexistent + "/tmp/hakurei.0/" + wantAutoEtcPrefix
|
||||
|
||||
// wantRuntimePath is the XDG_RUNTIME_DIR value returned during testing.
|
||||
wantRuntimePath = "/proc/nonexistent/xdg_runtime_dir"
|
||||
// wantRunDirPath is the RunDirPath value resolved during testing.
|
||||
wantRunDirPath = wantRuntimePath + "/hakurei"
|
||||
// wantRuntimeSharePath is the runtimeSharePath value resolved during testing.
|
||||
wantRuntimeSharePath = wantRunDirPath + "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
|
||||
// checkExpectInstanceId is the [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 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)
|
||||
// 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 func(t *testing.T, state *outcomeStateSys)
|
||||
// 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)
|
||||
// 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 func(t *testing.T, state *outcomeStateParams)
|
||||
// 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{}
|
||||
|
||||
@@ -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
84
internal/app/hsu_test.go
Normal 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}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -155,7 +159,7 @@ type outcomeStateSys struct {
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
// Copied header from [hst.Config]. Safe for read by spFinalOp.toSystem only.
|
||||
// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
|
||||
extraPerms []hst.ExtraPermConfig
|
||||
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
|
||||
sessionBus, systemBus *hst.BusConfig
|
||||
@@ -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,7 +282,8 @@ func (state *outcomeStateSys) toSystem() error {
|
||||
&spPulseOp{},
|
||||
&spDBusOp{},
|
||||
|
||||
spFinalOp{},
|
||||
// must run last
|
||||
&spFilesystemOp{},
|
||||
}
|
||||
|
||||
state.Shim.Ops = make([]outcomeOp, 0, len(ops))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
}, paramsWantEnv(config, 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},
|
||||
}, 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{
|
||||
}, paramsWantEnv(config, 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},
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
@@ -120,6 +123,7 @@ func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
|
||||
func init() { gob.Register(new(spFilesystemOp)) }
|
||||
|
||||
// spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem.
|
||||
// This outcomeOp is hardcoded to always run last.
|
||||
type spFilesystemOp struct {
|
||||
// Matched paths to cover. Stored during toSystem.
|
||||
HidePaths []*check.Absolute
|
||||
@@ -259,6 +263,8 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
|
||||
}
|
||||
}
|
||||
|
||||
// append ExtraPerms last
|
||||
flattenExtraPerms(state.sys, state.extraPerms)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -278,6 +284,15 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
|
||||
if state.Container.Flags&hst.FDevice == 0 {
|
||||
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
|
||||
}
|
||||
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
|
||||
|
||||
state.params.Env = make([]string, 0, len(state.env))
|
||||
for key, value := range state.env {
|
||||
// key validated early via hst
|
||||
state.params.Env = append(state.params.Env, key+"="+value)
|
||||
}
|
||||
slices.Sort(state.params.Env)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -313,6 +328,32 @@ func evalSymlinks(msg message.Msg, k syscallDispatcher, v *string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// flattenExtraPerms expands a slice of [hst.ExtraPermConfig] into [system.I].
|
||||
func flattenExtraPerms(sys *system.I, extraPerms []hst.ExtraPermConfig) {
|
||||
for i := range extraPerms {
|
||||
p := &extraPerms[i]
|
||||
if p.Path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Ensure {
|
||||
sys.Ensure(p.Path, 0700)
|
||||
}
|
||||
|
||||
perms := make(acl.Perms, 0, 3)
|
||||
if p.Read {
|
||||
perms = append(perms, acl.Read)
|
||||
}
|
||||
if p.Write {
|
||||
perms = append(perms, acl.Write)
|
||||
}
|
||||
if p.Execute {
|
||||
perms = append(perms, acl.Execute)
|
||||
}
|
||||
sys.UpdatePermType(system.User, p.Path, perms...)
|
||||
}
|
||||
}
|
||||
|
||||
// opsAdapter implements [hst.Ops] on [container.Ops].
|
||||
type opsAdapter struct{ *container.Ops }
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"os"
|
||||
"reflect"
|
||||
"syscall"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
@@ -72,16 +73,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),
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
wantEnv := map[string]string{
|
||||
}, paramsWantEnv(config, 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"
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
|
||||
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
|
||||
}
|
||||
@@ -90,7 +84,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 +111,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),
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
wantEnv := map[string]string{
|
||||
}, paramsWantEnv(config, 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)
|
||||
}
|
||||
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
|
||||
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
|
||||
}
|
||||
@@ -134,7 +122,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 +147,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 +308,14 @@ 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().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
|
||||
state.filesystem = append(configSmall.Container.Filesystem, hst.FilesystemConfigJSON{})
|
||||
})), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
@@ -341,13 +341,22 @@ 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().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
|
||||
state.filesystem = configSmall.Container.Filesystem
|
||||
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{
|
||||
Env: []string{
|
||||
"GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
},
|
||||
|
||||
Ops: new(container.Ops).
|
||||
Etc(fhs.AbsEtc, wantAutoEtcPrefix).
|
||||
OverlayReadonly(
|
||||
@@ -356,7 +365,8 @@ func TestSpFilesystemOp(t *testing.T) {
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium")).
|
||||
Readonly(hst.AbsPrivateTmp, 0755).
|
||||
Tmpfs(m("/proc/nonexistent/eval/etc/dbus"), 1<<13, 0755).
|
||||
Remount(fhs.AbsDev, syscall.MS_RDONLY),
|
||||
Remount(fhs.AbsDev, syscall.MS_RDONLY).
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}, nil, nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
@@ -386,13 +396,22 @@ 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().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
|
||||
state.filesystem = config.Container.Filesystem[1:]
|
||||
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{
|
||||
Env: []string{
|
||||
"GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
},
|
||||
|
||||
Ops: new(container.Ops).
|
||||
Etc(fhs.AbsEtc, wantAutoEtcPrefix).
|
||||
Tmpfs(fhs.AbsTmp, 0, 0755).
|
||||
@@ -407,11 +426,47 @@ func TestSpFilesystemOp(t *testing.T) {
|
||||
fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
|
||||
check.MustAbs("/data/data/org.chromium.Chromium"),
|
||||
bits.BindWritable|bits.BindEnsure).
|
||||
Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"), bits.BindDevice|bits.BindWritable|bits.BindOptional),
|
||||
Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"), bits.BindDevice|bits.BindWritable|bits.BindOptional).
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}, nil, nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlattenExtraPerms(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
perms []hst.ExtraPermConfig
|
||||
want *system.I
|
||||
}{
|
||||
{"path nil check", append(hst.Template().ExtraPerms, hst.ExtraPermConfig{}), newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute)},
|
||||
|
||||
{"template", hst.Template().ExtraPerms, newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute)},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := newI()
|
||||
flattenExtraPerms(got, tc.perms)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("flattenExtraPerms: sys = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// invalidFSHost implements the Host method of [hst.FilesystemConfig] with an invalid response.
|
||||
type invalidFSHost bool
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
}, paramsWantEnv(config, 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},
|
||||
}, 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{
|
||||
}, 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",
|
||||
}
|
||||
maps.Copy(wantEnv, config.Container.Env)
|
||||
if !maps.Equal(state.env, wantEnv) {
|
||||
t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv)
|
||||
}
|
||||
}, nil},
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
func init() { gob.Register(spFinalOp{}) }
|
||||
|
||||
// spFinalOp is a transitional op destined for removal after #3, #8, #9 has been resolved.
|
||||
// It exists to avoid reordering the expected entries in test cases.
|
||||
type spFinalOp struct{}
|
||||
|
||||
func (s spFinalOp) toSystem(state *outcomeStateSys) error {
|
||||
// append ExtraPerms last
|
||||
for i := range state.extraPerms {
|
||||
p := &state.extraPerms[i]
|
||||
if p.Path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Ensure {
|
||||
state.sys.Ensure(p.Path, 0700)
|
||||
}
|
||||
|
||||
perms := make(acl.Perms, 0, 3)
|
||||
if p.Read {
|
||||
perms = append(perms, acl.Read)
|
||||
}
|
||||
if p.Write {
|
||||
perms = append(perms, acl.Write)
|
||||
}
|
||||
if p.Execute {
|
||||
perms = append(perms, acl.Execute)
|
||||
}
|
||||
state.sys.UpdatePermType(system.User, p.Path, perms...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spFinalOp) toContainer(state *outcomeStateParams) error {
|
||||
// TODO(ophestra): move this to spFilesystemOp after #8 and #9
|
||||
|
||||
// mount root read-only as the final setup Op
|
||||
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
|
||||
|
||||
state.params.Env = make([]string, 0, len(state.env))
|
||||
for key, value := range state.env {
|
||||
if strings.IndexByte(key, '=') != -1 {
|
||||
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
|
||||
Msg: fmt.Sprintf("invalid environment variable %s", key)}
|
||||
}
|
||||
state.params.Env = append(state.params.Env, key+"="+value)
|
||||
}
|
||||
// range over map has randomised order
|
||||
slices.Sort(state.params.Env)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
460
internal/app/sppulse_test.go
Normal file
460
internal/app/sppulse_test.go
Normal 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},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
128
internal/app/spruntime_test.go
Normal file
128
internal/app/spruntime_test.go
Normal 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},
|
||||
})
|
||||
}
|
||||
34
internal/app/sptmpdir_test.go
Normal file
34
internal/app/sptmpdir_test.go
Normal 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},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
104
internal/app/spwayland_test.go
Normal file
104
internal/app/spwayland_test.go
Normal 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
119
internal/app/spx11_test.go
Normal 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},
|
||||
})
|
||||
}
|
||||
10
ldd/exec.go
10
ldd/exec.go
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user