internal/outcome: rename from app
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m27s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m1s
All checks were successful
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m27s
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m11s
Test / Hakurei (push) Successful in 3m9s
Test / Hpkg (push) Successful in 4m1s
This is less ambiguous, and more accurately describes the purpose of the package. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
163
internal/outcome/dispatcher.go
Normal file
163
internal/outcome/dispatcher.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// osFile represents [os.File].
|
||||
type osFile interface {
|
||||
Name() string
|
||||
io.Writer
|
||||
fs.File
|
||||
}
|
||||
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
|
||||
type syscallDispatcher interface {
|
||||
// new starts a goroutine with a new instance of syscallDispatcher.
|
||||
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
|
||||
// just synchronising access is not enough, as this is for test instrumentation.
|
||||
new(f func(k syscallDispatcher, msg message.Msg))
|
||||
|
||||
// getpid provides [os.Getpid].
|
||||
getpid() int
|
||||
// getuid provides [os.Getuid].
|
||||
getuid() int
|
||||
// getgid provides [os.Getgid].
|
||||
getgid() int
|
||||
// lookupEnv provides [os.LookupEnv].
|
||||
lookupEnv(key string) (string, bool)
|
||||
// pipe provides os.Pipe.
|
||||
pipe() (r, w *os.File, err error)
|
||||
// stat provides [os.Stat].
|
||||
stat(name string) (os.FileInfo, error)
|
||||
// open provides [os.Open].
|
||||
open(name string) (osFile, error)
|
||||
// readdir provides [os.ReadDir].
|
||||
readdir(name string) ([]os.DirEntry, error)
|
||||
// tempdir provides [os.TempDir].
|
||||
tempdir() string
|
||||
// exit provides [os.Exit].
|
||||
exit(code int)
|
||||
|
||||
// evalSymlinks provides [filepath.EvalSymlinks].
|
||||
evalSymlinks(path string) (string, error)
|
||||
|
||||
// lookupGroupId calls [user.LookupGroup] and returns the Gid field of the resulting [user.Group] struct.
|
||||
lookupGroupId(name string) (string, error)
|
||||
|
||||
// cmdOutput provides the Output method of [exec.Cmd].
|
||||
cmdOutput(cmd *exec.Cmd) ([]byte, error)
|
||||
|
||||
// notifyContext provides [signal.NotifyContext].
|
||||
notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
|
||||
|
||||
// prctl provides [container.Prctl].
|
||||
prctl(op, arg2, arg3 uintptr) error
|
||||
// overflowUid provides [container.OverflowUid].
|
||||
overflowUid(msg message.Msg) int
|
||||
// overflowGid provides [container.OverflowGid].
|
||||
overflowGid(msg message.Msg) int
|
||||
// setDumpable provides [container.SetDumpable].
|
||||
setDumpable(dumpable uintptr) error
|
||||
// receive provides [container.Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
|
||||
// containerStart provides the Start method of [container.Container].
|
||||
containerStart(z *container.Container) error
|
||||
// containerStart provides the Serve method of [container.Container].
|
||||
containerServe(z *container.Container) error
|
||||
// containerStart provides the Wait method of [container.Container].
|
||||
containerWait(z *container.Container) error
|
||||
|
||||
// seccompLoad provides [seccomp.Load].
|
||||
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
|
||||
|
||||
// mustHsuPath provides [internal.MustHsuPath].
|
||||
mustHsuPath() *check.Absolute
|
||||
|
||||
// dbusAddress provides [dbus.Address].
|
||||
dbusAddress() (session, system string)
|
||||
|
||||
// setupContSignal provides setupContSignal.
|
||||
setupContSignal(pid int) (io.ReadCloser, func(), error)
|
||||
|
||||
// getMsg returns the [message.Msg] held by syscallDispatcher.
|
||||
getMsg() message.Msg
|
||||
// fatal provides [log.Fatal].
|
||||
fatal(v ...any)
|
||||
// fatalf provides [log.Fatalf].
|
||||
fatalf(format string, v ...any)
|
||||
}
|
||||
|
||||
// direct implements syscallDispatcher on the current kernel.
|
||||
type direct struct{ msg message.Msg }
|
||||
|
||||
func (k direct) new(f func(k syscallDispatcher, msg message.Msg)) { go f(k, k.msg) }
|
||||
|
||||
func (direct) getpid() int { return os.Getpid() }
|
||||
func (direct) getuid() int { return os.Getuid() }
|
||||
func (direct) getgid() int { return os.Getgid() }
|
||||
func (direct) lookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
|
||||
func (direct) pipe() (r, w *os.File, err error) { return os.Pipe() }
|
||||
func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
|
||||
func (direct) open(name string) (osFile, error) { return os.Open(name) }
|
||||
func (direct) readdir(name string) ([]os.DirEntry, error) { return os.ReadDir(name) }
|
||||
func (direct) tempdir() string { return os.TempDir() }
|
||||
func (direct) exit(code int) { os.Exit(code) }
|
||||
|
||||
func (direct) evalSymlinks(path string) (string, error) { return filepath.EvalSymlinks(path) }
|
||||
|
||||
func (direct) lookupGroupId(name string) (gid string, err error) {
|
||||
var group *user.Group
|
||||
group, err = user.LookupGroup(name)
|
||||
if group != nil {
|
||||
gid = group.Gid
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() }
|
||||
|
||||
func (direct) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
|
||||
return signal.NotifyContext(parent, signals...)
|
||||
}
|
||||
|
||||
func (direct) prctl(op, arg2, arg3 uintptr) error { return container.Prctl(op, arg2, arg3) }
|
||||
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
|
||||
func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return container.SetDumpable(dumpable) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return container.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) containerStart(z *container.Container) error { return z.Start() }
|
||||
func (direct) containerServe(z *container.Container) error { return z.Serve() }
|
||||
func (direct) containerWait(z *container.Container) error { return z.Wait() }
|
||||
|
||||
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||
return seccomp.Load(rules, flags)
|
||||
}
|
||||
|
||||
func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() }
|
||||
|
||||
func (direct) dbusAddress() (session, system string) { return dbus.Address() }
|
||||
|
||||
func (direct) setupContSignal(pid int) (io.ReadCloser, func(), error) { return setupContSignal(pid) }
|
||||
|
||||
func (k direct) getMsg() message.Msg { return k.msg }
|
||||
func (k direct) fatal(v ...any) { k.msg.GetLogger().Fatal(v...) }
|
||||
func (k direct) fatalf(format string, v ...any) { k.msg.GetLogger().Fatalf(format, v...) }
|
||||
662
internal/outcome/dispatcher_test.go
Normal file
662
internal/outcome/dispatcher_test.go
Normal file
@@ -0,0 +1,662 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
// call initialises a [stub.Call].
|
||||
// This keeps composites analysis happy without making the test cases too bloated.
|
||||
func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
|
||||
return stub.Call{Name: name, Args: args, Ret: ret, Err: err}
|
||||
}
|
||||
|
||||
const (
|
||||
// checkExpectUid is the uid value used by checkOpBehaviour to initialise [system.I].
|
||||
checkExpectUid = 0xcafebabe
|
||||
// wantAutoEtcPrefix is the autoetc prefix corresponding to checkExpectInstanceId.
|
||||
wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
// wantInstancePrefix is the SharePath corresponding to checkExpectInstanceId.
|
||||
wantInstancePrefix = container.Nonexistent + "/tmp/hakurei.0/" + wantAutoEtcPrefix
|
||||
|
||||
// wantRuntimePath is the XDG_RUNTIME_DIR value returned during testing.
|
||||
wantRuntimePath = "/proc/nonexistent/xdg_runtime_dir"
|
||||
// wantRunDirPath is the RunDirPath value resolved during testing.
|
||||
wantRunDirPath = wantRuntimePath + "/hakurei"
|
||||
// wantRuntimeSharePath is the runtimeSharePath value resolved during testing.
|
||||
wantRuntimeSharePath = wantRunDirPath + "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
|
||||
// checkExpectInstanceId is the [hst.ID] value used by checkOpBehaviour to initialise outcomeState.
|
||||
var checkExpectInstanceId = *(*hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{})))
|
||||
|
||||
type (
|
||||
// pStateSysFunc is called before each test case is run to prepare outcomeStateSys.
|
||||
pStateSysFunc = func(state *outcomeStateSys)
|
||||
// pStateContainerFunc is called before each test case is run to prepare outcomeStateParams.
|
||||
pStateContainerFunc = func(state *outcomeStateParams)
|
||||
|
||||
// extraCheckSysFunc is called to check outcomeStateSys and must not have side effects.
|
||||
extraCheckSysFunc = func(t *testing.T, state *outcomeStateSys)
|
||||
// extraCheckParamsFunc is called to check outcomeStateParams and must not have side effects.
|
||||
extraCheckParamsFunc = func(t *testing.T, state *outcomeStateParams)
|
||||
)
|
||||
|
||||
// insertsOps prepares outcomeStateParams to allow [container.Op] to be inserted.
|
||||
func insertsOps(next pStateContainerFunc) pStateContainerFunc {
|
||||
return func(state *outcomeStateParams) {
|
||||
state.params.Ops = new(container.Ops)
|
||||
|
||||
if next != nil {
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// afterSpRuntimeOp prepares outcomeStateParams for an outcomeOp meant to run after spRuntimeOp.
|
||||
func afterSpRuntimeOp(next pStateContainerFunc) pStateContainerFunc {
|
||||
return func(state *outcomeStateParams) {
|
||||
// emulates spRuntimeOp
|
||||
state.runtimeDir = m("/run/user/1000")
|
||||
|
||||
if next != nil {
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sysUsesInstance checks for use of the outcomeStateSys.instance method.
|
||||
func sysUsesInstance(next extraCheckSysFunc) extraCheckSysFunc {
|
||||
return func(t *testing.T, state *outcomeStateSys) {
|
||||
if want := m(wantInstancePrefix); !reflect.DeepEqual(state.sharePath, want) {
|
||||
t.Errorf("outcomeStateSys: sharePath = %v, want %v", state.sharePath, want)
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
next(t, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sysUsesRuntime checks for use of the outcomeStateSys.runtime method.
|
||||
func sysUsesRuntime(next extraCheckSysFunc) extraCheckSysFunc {
|
||||
return func(t *testing.T, state *outcomeStateSys) {
|
||||
if want := m(wantRuntimeSharePath); !reflect.DeepEqual(state.runtimeSharePath, want) {
|
||||
t.Errorf("outcomeStateSys: runtimeSharePath = %v, want %v", state.runtimeSharePath, want)
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
next(t, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// paramsWantEnv checks outcomeStateParams.env for inserted entries on top of [hst.Config].
|
||||
func paramsWantEnv(config *hst.Config, wantEnv map[string]string, next extraCheckParamsFunc) extraCheckParamsFunc {
|
||||
want := make(map[string]string, len(wantEnv)+len(config.Container.Env))
|
||||
maps.Copy(want, wantEnv)
|
||||
maps.Copy(want, config.Container.Env)
|
||||
return func(t *testing.T, state *outcomeStateParams) {
|
||||
if !maps.Equal(state.env, want) {
|
||||
t.Errorf("toContainer: env = %#v, want %#v", state.env, want)
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
next(t, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// opBehaviourTestCase checks outcomeOp behaviour against outcomeStateSys and outcomeStateParams.
|
||||
type opBehaviourTestCase struct {
|
||||
name string
|
||||
// newOp returns a new instance of outcomeOp under testing that is safe to clobber.
|
||||
newOp func(isShim, clearUnexported bool) outcomeOp
|
||||
// newConfig returns a new instance of [hst.Config] that is checked not to be clobbered by outcomeOp.
|
||||
newConfig func() *hst.Config
|
||||
|
||||
// pStateSys is called before outcomeOp.toSystem to prepare outcomeStateSys.
|
||||
pStateSys pStateSysFunc
|
||||
// toSystem are expected syscallDispatcher calls during outcomeOp.toSystem.
|
||||
toSystem []stub.Call
|
||||
// wantSys is the expected [system.I] state after outcomeOp.toSystem.
|
||||
wantSys *system.I
|
||||
// extraCheckSys is called after outcomeOp.toSystem to check the state of outcomeStateSys.
|
||||
extraCheckSys extraCheckSysFunc
|
||||
// wantErrSystem is the expected error value returned by outcomeOp.toSystem.
|
||||
// Further testing is skipped if not nil.
|
||||
wantErrSystem error
|
||||
|
||||
// pStateContainer is called before outcomeOp.toContainer to prepare outcomeStateParams.
|
||||
pStateContainer pStateContainerFunc
|
||||
// toContainer are expected syscallDispatcher calls during outcomeOp.toContainer.
|
||||
toContainer []stub.Call
|
||||
// wantParams is the expected [container.Params] after outcomeOp.toContainer.
|
||||
wantParams *container.Params
|
||||
// extraCheckParams is called after outcomeOp.toContainer to check the state of outcomeStateParams.
|
||||
extraCheckParams extraCheckParamsFunc
|
||||
// wantErrContainer is the expected error value returned by outcomeOp.toContainer.
|
||||
wantErrContainer error
|
||||
}
|
||||
|
||||
// checkOpBehaviour runs a slice of opBehaviourTestCase.
|
||||
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
||||
t.Helper()
|
||||
|
||||
wantNewState := []stub.Call{
|
||||
// newOutcomeState
|
||||
call("getpid", stub.ExpectArgs{}, 0xdead, nil),
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("mustHsuPath", stub.ExpectArgs{}, m(container.Nonexistent), nil),
|
||||
call("cmdOutput", stub.ExpectArgs{container.Nonexistent, os.Stderr, []string{}, "/"}, []byte("0"), nil),
|
||||
call("tempdir", stub.ExpectArgs{}, container.Nonexistent+"/tmp", nil),
|
||||
call("lookupEnv", stub.ExpectArgs{"XDG_RUNTIME_DIR"}, wantRuntimePath, nil),
|
||||
call("getuid", stub.ExpectArgs{}, 1000, nil),
|
||||
call("getgid", stub.ExpectArgs{}, 100, nil),
|
||||
|
||||
// populateLocal
|
||||
call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{
|
||||
m(container.Nonexistent + "/tmp/hakurei.0"),
|
||||
m(container.Nonexistent + "/xdg_runtime_dir/hakurei"),
|
||||
}}, nil, nil),
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
wantCallsFull := slices.Concat(wantNewState, tc.toSystem, []stub.Call{{Name: stub.CallSeparator}})
|
||||
if tc.wantErrSystem == nil {
|
||||
wantCallsFull = append(wantCallsFull, slices.Concat(wantNewState, tc.toContainer)...)
|
||||
}
|
||||
|
||||
wantConfig := tc.newConfig()
|
||||
k := &kstub{panicDispatcher{}, stub.New(t,
|
||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} },
|
||||
stub.Expect{Calls: wantCallsFull},
|
||||
)}
|
||||
defer stub.HandleExit(t)
|
||||
|
||||
{
|
||||
config := tc.newConfig()
|
||||
s := newOutcomeState(k, k, &checkExpectInstanceId, config, &Hsu{k: k})
|
||||
if err := s.populateLocal(k, k); err != nil {
|
||||
t.Fatalf("populateLocal: error = %v", err)
|
||||
}
|
||||
stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid))
|
||||
if tc.pStateSys != nil {
|
||||
tc.pStateSys(stateSys)
|
||||
}
|
||||
op := tc.newOp(false, true)
|
||||
|
||||
if err := op.toSystem(stateSys); !reflect.DeepEqual(err, tc.wantErrSystem) {
|
||||
t.Fatalf("toSystem: error = %#v, want %#v", err, tc.wantErrSystem)
|
||||
}
|
||||
k.Expects(stub.CallSeparator)
|
||||
if !reflect.DeepEqual(config, wantConfig) {
|
||||
t.Errorf("toSystem clobbered config: %#v, want %#v", config, wantConfig)
|
||||
}
|
||||
|
||||
if tc.wantErrSystem != nil {
|
||||
goto out
|
||||
}
|
||||
|
||||
if !stateSys.sys.Equal(tc.wantSys) {
|
||||
t.Errorf("toSystem: %#v, want %#v", stateSys.sys, tc.wantSys)
|
||||
}
|
||||
if tc.extraCheckSys != nil {
|
||||
tc.extraCheckSys(t, stateSys)
|
||||
}
|
||||
if wantOpSys := tc.newOp(true, false); !reflect.DeepEqual(op, wantOpSys) {
|
||||
t.Errorf("toSystem: op = %#v, want %#v", op, wantOpSys)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
config := tc.newConfig()
|
||||
s := newOutcomeState(k, k, &checkExpectInstanceId, config, &Hsu{k: k})
|
||||
stateParams := s.newParams()
|
||||
if err := s.populateLocal(k, k); err != nil {
|
||||
t.Fatalf("populateLocal: error = %v", err)
|
||||
}
|
||||
if tc.pStateContainer != nil {
|
||||
tc.pStateContainer(stateParams)
|
||||
}
|
||||
op := tc.newOp(true, true)
|
||||
|
||||
if err := op.toContainer(stateParams); !reflect.DeepEqual(err, tc.wantErrContainer) {
|
||||
t.Fatalf("toContainer: error = %#v, want %#v", err, tc.wantErrContainer)
|
||||
}
|
||||
|
||||
if tc.wantErrContainer != nil {
|
||||
goto out
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(stateParams.params, tc.wantParams) {
|
||||
t.Errorf("toContainer:\n%s\nwant\n%s", mustMarshal(stateParams.params), mustMarshal(tc.wantParams))
|
||||
}
|
||||
if tc.extraCheckParams != nil {
|
||||
tc.extraCheckParams(t, stateParams)
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||
count := k.Pos() - 1 // separator
|
||||
if count-len(wantNewState) < len(tc.toSystem) {
|
||||
t.Errorf("toSystem: %d calls, want %d", count-len(wantNewState), len(tc.toSystem))
|
||||
} else {
|
||||
t.Errorf("toContainer: %d calls, want %d", count-len(tc.toSystem)-2*len(wantNewState), len(tc.toContainer))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid) }
|
||||
|
||||
// simpleTestCase is a simple freeform test case utilising kstub.
|
||||
type simpleTestCase struct {
|
||||
name string
|
||||
f func(k *kstub) error
|
||||
// want are expected syscallDispatcher calls during f.
|
||||
want stub.Expect
|
||||
// wantErr is the expected error value returned by f.
|
||||
wantErr error
|
||||
}
|
||||
|
||||
// checkSimple runs a slice of simpleTestCase.
|
||||
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
||||
t.Helper()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
defer stub.HandleExit(t)
|
||||
k := &kstub{panicDispatcher{}, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{panicDispatcher{}, s} }, tc.want)}
|
||||
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("%s: error = %#v, want %#v", fname, err, tc.wantErr)
|
||||
}
|
||||
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
|
||||
t.Helper()
|
||||
|
||||
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// kstub partially implements syscallDispatcher via [stub.Stub].
|
||||
type kstub struct {
|
||||
panicDispatcher
|
||||
*stub.Stub[syscallDispatcher]
|
||||
}
|
||||
|
||||
func (k *kstub) new(f func(k syscallDispatcher, msg message.Msg)) {
|
||||
k.New(func(k syscallDispatcher) { f(k, k.(*kstub)) })
|
||||
}
|
||||
|
||||
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
|
||||
func (k *kstub) getuid() int { k.Helper(); return k.Expects("getuid").Ret.(int) }
|
||||
func (k *kstub) getgid() int { k.Helper(); return k.Expects("getgid").Ret.(int) }
|
||||
func (k *kstub) lookupEnv(key string) (string, bool) {
|
||||
k.Helper()
|
||||
expect := k.Expects("lookupEnv")
|
||||
if expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0)) != nil {
|
||||
k.FailNow()
|
||||
}
|
||||
if expect.Ret == nil {
|
||||
return "\x00", false
|
||||
}
|
||||
return expect.Ret.(string), true
|
||||
}
|
||||
func (k *kstub) stat(name string) (os.FileInfo, error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("stat")
|
||||
return expect.Ret.(os.FileInfo), expect.Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0))
|
||||
}
|
||||
func (k *kstub) open(name string) (osFile, error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("open")
|
||||
return expect.Ret.(osFile), expect.Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0))
|
||||
}
|
||||
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("readdir")
|
||||
return expect.Ret.([]os.DirEntry), expect.Error(
|
||||
stub.CheckArg(k.Stub, "name", name, 0))
|
||||
}
|
||||
func (k *kstub) tempdir() string { k.Helper(); return k.Expects("tempdir").Ret.(string) }
|
||||
func (k *kstub) evalSymlinks(path string) (string, error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("evalSymlinks")
|
||||
return expect.Ret.(string), expect.Error(
|
||||
stub.CheckArg(k.Stub, "path", path, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) prctl(op, arg2, arg3 uintptr) error {
|
||||
k.Helper()
|
||||
return k.Expects("prctl").Error(
|
||||
stub.CheckArg(k.Stub, "op", op, 0),
|
||||
stub.CheckArg(k.Stub, "arg2", arg2, 1),
|
||||
stub.CheckArg(k.Stub, "arg3", arg3, 2))
|
||||
}
|
||||
|
||||
func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||
k.Helper()
|
||||
return k.Expects("setDumpable").Error(
|
||||
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
|
||||
if expect.Args[2] != nil {
|
||||
*fdp = expect.Args[2].(uintptr)
|
||||
}
|
||||
return func() error { return k.Expects("closeReceive").Err }, expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) expectCheckContainer(expect *stub.Call, z *container.Container) error {
|
||||
k.Helper()
|
||||
err := expect.Error(
|
||||
stub.CheckArgReflect(k.Stub, "params", &z.Params, 0))
|
||||
if err != nil {
|
||||
k.Errorf("params:\n%s\n%s", mustMarshal(&z.Params), mustMarshal(expect.Args[0]))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (k *kstub) containerStart(z *container.Container) error {
|
||||
k.Helper()
|
||||
return k.expectCheckContainer(k.Expects("containerStart"), z)
|
||||
}
|
||||
func (k *kstub) containerServe(z *container.Container) error {
|
||||
k.Helper()
|
||||
return k.expectCheckContainer(k.Expects("containerServe"), z)
|
||||
}
|
||||
func (k *kstub) containerWait(z *container.Container) error {
|
||||
k.Helper()
|
||||
return k.expectCheckContainer(k.Expects("containerWait"), z)
|
||||
}
|
||||
|
||||
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||
k.Helper()
|
||||
return k.Expects("seccompLoad").Error(
|
||||
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
|
||||
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||
}
|
||||
|
||||
func (k *kstub) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("cmdOutput")
|
||||
return expect.Ret.([]byte), expect.Error(
|
||||
stub.CheckArg(k.Stub, "cmd.Path", cmd.Path, 0),
|
||||
stub.CheckArgReflect(k.Stub, "cmd.Stderr", cmd.Stderr, 1),
|
||||
stub.CheckArgReflect(k.Stub, "cmd.Env", cmd.Env, 2),
|
||||
stub.CheckArg(k.Stub, "cmd.Dir", cmd.Dir, 3))
|
||||
}
|
||||
|
||||
func (k *kstub) notifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
|
||||
k.Helper()
|
||||
if k.Expects("notifyContext").Error(
|
||||
stub.CheckArgReflect(k.Stub, "parent", parent, 0),
|
||||
stub.CheckArgReflect(k.Stub, "signals", signals, 1)) != nil {
|
||||
k.FailNow()
|
||||
}
|
||||
return k.Context(), func() { k.Helper(); k.Expects("notifyContextStop") }
|
||||
}
|
||||
|
||||
func (k *kstub) mustHsuPath() *check.Absolute {
|
||||
k.Helper()
|
||||
return k.Expects("mustHsuPath").Ret.(*check.Absolute)
|
||||
}
|
||||
|
||||
func (k *kstub) dbusAddress() (session, system string) {
|
||||
k.Helper()
|
||||
ret := k.Expects("dbusAddress").Ret.([2]string)
|
||||
return ret[0], ret[1]
|
||||
}
|
||||
|
||||
// stubTrackReader embeds kstub but switches the underlying [stub.Stub] index to sub on its first Read.
|
||||
// The resulting kstub does not share any state with the instance passed to the instrumented goroutine.
|
||||
// Therefore, any method making use of such must not be called.
|
||||
type stubTrackReader struct {
|
||||
sub int
|
||||
subOnce sync.Once
|
||||
|
||||
*kstub
|
||||
}
|
||||
|
||||
func (r *stubTrackReader) Read(p []byte) (n int, err error) {
|
||||
r.subOnce.Do(func() {
|
||||
subVal := reflect.ValueOf(r.kstub.Stub).Elem().FieldByName("sub")
|
||||
r.kstub = &kstub{panicDispatcher{}, reflect.
|
||||
NewAt(subVal.Type(), unsafe.Pointer(subVal.UnsafeAddr())).Elem().
|
||||
Interface().([]*stub.Stub[syscallDispatcher])[r.sub]}
|
||||
})
|
||||
|
||||
return r.kstub.Read(p)
|
||||
}
|
||||
|
||||
func (k *kstub) setupContSignal(pid int) (io.ReadCloser, func(), error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("setupContSignal")
|
||||
return &stubTrackReader{sub: expect.Ret.(int), kstub: k}, func() { k.Expects("wKeepAlive") }, expect.Error(
|
||||
stub.CheckArg(k.Stub, "pid", pid, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) getMsg() message.Msg { k.Helper(); k.Expects("getMsg"); return k }
|
||||
|
||||
func (k *kstub) Close() error { k.Helper(); return k.Expects("rcClose").Err }
|
||||
func (k *kstub) Read(p []byte) (n int, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("rcRead")
|
||||
|
||||
// special case to terminate exit outcomes goroutine
|
||||
// to proceed with further testing of the entrypoint
|
||||
if expect.Ret == nil {
|
||||
panic(stub.PanicExit)
|
||||
}
|
||||
|
||||
return copy(p, expect.Ret.([]byte)), expect.Err
|
||||
}
|
||||
|
||||
func (k *kstub) GetLogger() *log.Logger { k.Helper(); return k.Expects("getLogger").Ret.(*log.Logger) }
|
||||
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
||||
func (k *kstub) SwapVerbose(verbose bool) bool {
|
||||
k.Helper()
|
||||
expect := k.Expects("swapVerbose")
|
||||
if expect.Error(
|
||||
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
|
||||
k.FailNow()
|
||||
}
|
||||
return expect.Ret.(bool)
|
||||
}
|
||||
|
||||
// ignoreValue marks a value to be ignored by the test suite.
|
||||
type ignoreValue struct{}
|
||||
|
||||
func (k *kstub) Verbose(v ...any) {
|
||||
k.Helper()
|
||||
expect := k.Expects("verbose")
|
||||
|
||||
// translate ignores in v
|
||||
if want, ok := expect.Args[0].([]any); ok && len(v) == len(want) {
|
||||
for i, a := range want {
|
||||
if _, ok = a.(ignoreValue); ok {
|
||||
v[i] = ignoreValue{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expect.Error(
|
||||
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||
k.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) Verbosef(format string, v ...any) {
|
||||
k.Helper()
|
||||
if k.Expects("verbosef").Error(
|
||||
stub.CheckArg(k.Stub, "format", format, 0),
|
||||
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
|
||||
k.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
|
||||
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
|
||||
func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }
|
||||
|
||||
// stubOsFile partially implements osFile.
|
||||
type stubOsFile struct {
|
||||
closeErr error
|
||||
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (f *stubOsFile) Close() error { return f.closeErr }
|
||||
func (f *stubOsFile) Name() string { panic("unreachable") }
|
||||
func (f *stubOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
|
||||
|
||||
// stubFi partially implements [os.FileInfo]. Can be passed as nil to assert all methods unreachable.
|
||||
type stubFi struct {
|
||||
size int64
|
||||
mode os.FileMode
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (fi *stubFi) Name() string { panic("unreachable") }
|
||||
func (fi *stubFi) ModTime() time.Time { panic("unreachable") }
|
||||
func (fi *stubFi) Sys() any { panic("unreachable") }
|
||||
func (fi *stubFi) Size() int64 { return fi.size }
|
||||
func (fi *stubFi) Mode() os.FileMode { return fi.mode }
|
||||
func (fi *stubFi) IsDir() bool { return fi.isDir }
|
||||
|
||||
// stubDir returns a slice of [os.DirEntry] with only their Name method implemented.
|
||||
func stubDir(names ...string) []os.DirEntry {
|
||||
d := make([]os.DirEntry, len(names))
|
||||
for i, name := range names {
|
||||
d[i] = nameDentry(name)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// nameDentry implements the Name method on [os.DirEntry].
|
||||
type nameDentry string
|
||||
|
||||
func (e nameDentry) Name() string { return string(e) }
|
||||
func (nameDentry) IsDir() bool { panic("unreachable") }
|
||||
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
|
||||
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
|
||||
|
||||
// errorReader implements [io.Reader] that unconditionally returns -1, val.
|
||||
type errorReader struct{ val error }
|
||||
|
||||
func (r errorReader) Read([]byte) (int, error) { return -1, r.val }
|
||||
|
||||
// mustMarshal returns the result of [json.Marshal] as a string and panics on error.
|
||||
func mustMarshal(v any) string {
|
||||
if b, err := json.Marshal(v); err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
// m is a shortcut for [check.MustAbs].
|
||||
func m(pathname string) *check.Absolute { return check.MustAbs(pathname) }
|
||||
|
||||
// f returns [hst.FilesystemConfig] wrapped in its [json] adapter.
|
||||
func f(c hst.FilesystemConfig) hst.FilesystemConfigJSON {
|
||||
return hst.FilesystemConfigJSON{FilesystemConfig: c}
|
||||
}
|
||||
|
||||
// panicMsgContext implements [message.Msg] and [context.Context] with methods wrapping panic.
|
||||
// This should be assigned to test cases to be checked against.
|
||||
type panicMsgContext struct{}
|
||||
|
||||
func (panicMsgContext) GetLogger() *log.Logger { panic("unreachable") }
|
||||
func (panicMsgContext) IsVerbose() bool { panic("unreachable") }
|
||||
func (panicMsgContext) SwapVerbose(bool) bool { panic("unreachable") }
|
||||
func (panicMsgContext) Verbose(...any) { panic("unreachable") }
|
||||
func (panicMsgContext) Verbosef(string, ...any) { panic("unreachable") }
|
||||
func (panicMsgContext) Suspend() bool { panic("unreachable") }
|
||||
func (panicMsgContext) Resume() bool { panic("unreachable") }
|
||||
func (panicMsgContext) BeforeExit() { panic("unreachable") }
|
||||
|
||||
func (panicMsgContext) Deadline() (time.Time, bool) { panic("unreachable") }
|
||||
func (panicMsgContext) Done() <-chan struct{} { panic("unreachable") }
|
||||
func (panicMsgContext) Err() error { panic("unreachable") }
|
||||
func (panicMsgContext) Value(any) any { panic("unreachable") }
|
||||
|
||||
// panicDispatcher implements syscallDispatcher with methods wrapping panic.
|
||||
// This type is meant to be embedded in partial syscallDispatcher implementations.
|
||||
type panicDispatcher struct{}
|
||||
|
||||
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
|
||||
func (panicDispatcher) getpid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getuid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getgid() int { panic("unreachable") }
|
||||
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
|
||||
func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
|
||||
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
|
||||
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
|
||||
func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
|
||||
func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
|
||||
func (panicDispatcher) fatal(...any) { panic("unreachable") }
|
||||
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
|
||||
|
||||
func (panicDispatcher) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {
|
||||
panic("unreachable")
|
||||
}
|
||||
func (panicDispatcher) seccompLoad([]seccomp.NativeRule, seccomp.ExportFlag) error {
|
||||
panic("unreachable")
|
||||
}
|
||||
85
internal/outcome/finalise.go
Normal file
85
internal/outcome/finalise.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"sync/atomic"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) }
|
||||
func newWithMessageError(msg string, err error) error {
|
||||
return &hst.AppError{Step: "finalise", Err: err, Msg: msg}
|
||||
}
|
||||
|
||||
// An outcome is the runnable state of a hakurei container via [hst.Config].
|
||||
type outcome struct {
|
||||
// Supplementary group ids. Populated during finalise.
|
||||
supp []string
|
||||
// Resolved priv side operating system interactions. Populated during finalise.
|
||||
sys *system.I
|
||||
// Transmitted to shim. Populated during finalise.
|
||||
state *outcomeState
|
||||
// Kept for saving to [state].
|
||||
config *hst.Config
|
||||
|
||||
// Whether the current process is in outcome.main.
|
||||
active atomic.Bool
|
||||
|
||||
ctx context.Context
|
||||
syscallDispatcher
|
||||
}
|
||||
|
||||
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
|
||||
if ctx == nil || id == nil {
|
||||
// unreachable
|
||||
panic("invalid call to finalise")
|
||||
}
|
||||
if k.ctx != nil || k.sys != nil || k.state != nil {
|
||||
// unreachable
|
||||
panic("attempting to finalise twice")
|
||||
}
|
||||
k.ctx = ctx
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// hsu expects numerical group ids
|
||||
supp := make([]string, len(config.Groups))
|
||||
for i, name := range config.Groups {
|
||||
if gid, err := k.lookupGroupId(name); err != nil {
|
||||
var unknownGroupError user.UnknownGroupError
|
||||
if errors.As(err, &unknownGroupError) {
|
||||
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
|
||||
} else {
|
||||
return &hst.AppError{Step: "look up group by name", Err: err}
|
||||
}
|
||||
} else {
|
||||
supp[i] = gid
|
||||
}
|
||||
}
|
||||
|
||||
// early validation complete at this point
|
||||
s := newOutcomeState(k.syscallDispatcher, msg, id, config, &Hsu{k: k})
|
||||
if err := s.populateLocal(k.syscallDispatcher, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys := system.New(k.ctx, msg, s.uid.unwrap())
|
||||
if err := s.newSys(config, sys).toSystem(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.sys = sys
|
||||
k.supp = supp
|
||||
k.state = s
|
||||
k.config = config
|
||||
return nil
|
||||
}
|
||||
101
internal/outcome/hsu.go
Normal file
101
internal/outcome/hsu.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// Hsu caches responses from cmd/hsu.
|
||||
type Hsu struct {
|
||||
idOnce sync.Once
|
||||
idErr error
|
||||
id int
|
||||
|
||||
kOnce sync.Once
|
||||
|
||||
// msg is not populated
|
||||
k syscallDispatcher
|
||||
}
|
||||
|
||||
var ErrHsuAccess = errors.New("current user is not in the hsurc file")
|
||||
|
||||
// ensureDispatcher ensures Hsu.k is not nil.
|
||||
func (h *Hsu) ensureDispatcher() {
|
||||
h.kOnce.Do(func() {
|
||||
if h.k == nil {
|
||||
h.k = direct{}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ID returns the current user hsurc identifier.
|
||||
// [ErrHsuAccess] is returned if the current user is not in hsurc.
|
||||
func (h *Hsu) ID() (int, error) {
|
||||
h.ensureDispatcher()
|
||||
h.idOnce.Do(func() {
|
||||
h.id = -1
|
||||
hsuPath := h.k.mustHsuPath().String()
|
||||
|
||||
cmd := exec.Command(hsuPath)
|
||||
cmd.Path = hsuPath
|
||||
cmd.Stderr = os.Stderr // pass through fatal messages
|
||||
cmd.Env = make([]string, 0)
|
||||
cmd.Dir = fhs.Root
|
||||
var (
|
||||
p []byte
|
||||
exitError *exec.ExitError
|
||||
)
|
||||
|
||||
const step = "obtain uid from hsu"
|
||||
if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil {
|
||||
h.id, h.idErr = strconv.Atoi(string(p))
|
||||
if h.idErr != nil {
|
||||
h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"}
|
||||
}
|
||||
} else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
|
||||
// hsu prints an error message in this case
|
||||
h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
|
||||
} else if errors.Is(h.idErr, os.ErrNotExist) {
|
||||
h.idErr = &hst.AppError{Step: step, Err: h.idErr,
|
||||
Msg: fmt.Sprintf("the setuid helper is missing: %s", hsuPath)}
|
||||
}
|
||||
})
|
||||
|
||||
return h.id, h.idErr
|
||||
}
|
||||
|
||||
// MustID calls [Hsu.ID] and terminates on error.
|
||||
func (h *Hsu) MustID(msg message.Msg) int {
|
||||
id, err := h.ID()
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
|
||||
const fallback = "cannot retrieve user id from setuid wrapper:"
|
||||
if errors.Is(err, ErrHsuAccess) {
|
||||
if msg != nil {
|
||||
msg.Verbose("*"+fallback, err)
|
||||
}
|
||||
os.Exit(1)
|
||||
return -0xdeadbeef // not reached
|
||||
} else if m, ok := message.GetMessage(err); ok {
|
||||
log.Fatal(m)
|
||||
return -0xdeadbeef // not reached
|
||||
} else {
|
||||
log.Fatalln(fallback, err)
|
||||
return -0xdeadbeef // not reached
|
||||
}
|
||||
}
|
||||
|
||||
// HsuUid returns target uid for the stable hsu uid format.
|
||||
// No bounds check is performed, a value retrieved by [Hsu] is expected.
|
||||
func HsuUid(id, identity int) int { return 1000000 + id*10000 + identity }
|
||||
84
internal/outcome/hsu_test.go
Normal file
84
internal/outcome/hsu_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package outcome
|
||||
|
||||
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}
|
||||
}
|
||||
27
internal/outcome/main.go
Normal file
27
internal/outcome/main.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// Main runs an app according to [hst.Config] and terminates. Main does not return.
|
||||
func Main(ctx context.Context, msg message.Msg, config *hst.Config) {
|
||||
var id hst.ID
|
||||
if err := hst.NewInstanceID(&id); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
seal := outcome{syscallDispatcher: direct{msg}}
|
||||
if err := seal.finalise(ctx, msg, &id, config); err != nil {
|
||||
printMessageError("cannot seal app:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
seal.main(msg)
|
||||
panic("unreachable")
|
||||
}
|
||||
923
internal/outcome/main_test.go
Normal file
923
internal/outcome/main_test.go
Normal file
@@ -0,0 +1,923 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestOutcomeMain(t *testing.T) {
|
||||
t.Parallel()
|
||||
msg := message.NewMsg(nil)
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
k syscallDispatcher
|
||||
config *hst.Config
|
||||
id hst.ID
|
||||
wantSys *system.I
|
||||
wantParams *container.Params
|
||||
}{
|
||||
{"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.NewMsg(nil), 1000009).
|
||||
// spParamsOp
|
||||
Ensure(m("/tmp/hakurei.0"), 0711).
|
||||
|
||||
// spRuntimeOp
|
||||
Ensure(m("/tmp/hakurei.0/runtime"), 0700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
|
||||
|
||||
// spTmpdirOp
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
|
||||
|
||||
// instance
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0711).
|
||||
|
||||
// spWaylandOp
|
||||
Wayland(
|
||||
m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"),
|
||||
m("/run/user/1971/wayland-0"),
|
||||
"org.chromium.Chromium",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
).
|
||||
|
||||
// ensureRuntimeDir
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).
|
||||
UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
Ensure(m("/run/user/1971"), 0700).
|
||||
UpdatePermType(system.User, m("/run/user/1971"), acl.Execute).
|
||||
|
||||
// runtime
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0700).
|
||||
UpdatePerm(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), acl.Execute).
|
||||
|
||||
// spPulseOp
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse")).
|
||||
|
||||
// spDBusOp
|
||||
MustProxyDBus(
|
||||
hst.Template().SessionBus,
|
||||
hst.Template().SystemBus, dbus.ProxyPair{
|
||||
"unix:path=/run/user/1971/bus",
|
||||
"/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus",
|
||||
}, dbus.ProxyPair{
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
"/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket",
|
||||
},
|
||||
).UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), acl.Read, acl.Write).
|
||||
UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), acl.Read, acl.Write).
|
||||
|
||||
// spFilesystemOp
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), acl.Read, acl.Write, acl.Execute), &container.Params{
|
||||
|
||||
Dir: m("/data/data/org.chromium.Chromium"),
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
"HOME=/data/data/org.chromium.Chromium",
|
||||
"PULSE_COOKIE=/.hakurei/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/1971",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=wayland",
|
||||
},
|
||||
|
||||
// spParamsOp
|
||||
Hostname: "localhost",
|
||||
RetainSession: true,
|
||||
HostNet: true,
|
||||
HostAbstract: true,
|
||||
Path: m("/run/current-system/sw/bin/chromium"),
|
||||
Args: []string{
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland",
|
||||
},
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Uid: 1971,
|
||||
Gid: 100,
|
||||
|
||||
Ops: new(container.Ops).
|
||||
// resolveRoot
|
||||
Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
|
||||
// spParamsOp
|
||||
Proc(fhs.AbsProc).
|
||||
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), comp.BindWritable).
|
||||
|
||||
// spTmpdirOp
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/9"), fhs.AbsTmp, comp.BindWritable).
|
||||
|
||||
// spAccountOp
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:1971:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
|
||||
|
||||
// spWaylandOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
|
||||
|
||||
// spPulseOp
|
||||
Bind(m("/run/user/1971/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||
Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
|
||||
// spDBusOp
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
|
||||
// spFilesystemOp
|
||||
Etc(fhs.AbsEtc, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
|
||||
Tmpfs(fhs.AbsTmp, 0, 0755).
|
||||
Overlay(m("/nix/store"),
|
||||
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
|
||||
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
|
||||
Link(m("/run/current-system"), "/run/current-system", true).
|
||||
Link(m("/run/opengl-driver"), "/run/opengl-driver", true).
|
||||
Bind(fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
|
||||
m("/data/data/org.chromium.Chromium"),
|
||||
comp.BindWritable|comp.BindEnsure).
|
||||
Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"),
|
||||
comp.BindOptional|comp.BindWritable|comp.BindDevice).
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults no enablements", new(stubNixOS), &hst.Config{Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsRoot,
|
||||
Source: fhs.AbsRoot,
|
||||
Write: true,
|
||||
Special: true,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("kvm"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsEtc,
|
||||
Source: fhs.AbsEtc,
|
||||
Special: true,
|
||||
}},
|
||||
},
|
||||
|
||||
Username: "chronos",
|
||||
Shell: m("/run/current-system/sw/bin/zsh"),
|
||||
Home: m("/home/chronos"),
|
||||
|
||||
Path: m("/run/current-system/sw/bin/zsh"),
|
||||
Args: []string{"/run/current-system/sw/bin/zsh"},
|
||||
|
||||
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty | hst.FShareRuntime | hst.FShareTmpdir,
|
||||
}}, hst.ID{
|
||||
0x4a, 0x45, 0x0b, 0x65,
|
||||
0x96, 0xd7, 0xbc, 0x15,
|
||||
0xbd, 0x01, 0x78, 0x0e,
|
||||
0xb9, 0xa6, 0x07, 0xac,
|
||||
}, system.New(t.Context(), msg, 1000000).
|
||||
Ensure(m("/tmp/hakurei.0"), 0711).
|
||||
Ensure(m("/tmp/hakurei.0/runtime"), 0700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/runtime/0"), 0700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read, acl.Write, acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700).
|
||||
UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/0"), acl.Read, acl.Write, acl.Execute), &container.Params{
|
||||
|
||||
Dir: m("/home/chronos"),
|
||||
Path: m("/run/current-system/sw/bin/zsh"),
|
||||
Args: []string{"/run/current-system/sw/bin/zsh"},
|
||||
Env: []string{
|
||||
"HOME=/home/chronos",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Root(m("/"), comp.BindWritable).
|
||||
Proc(m("/proc/")).
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/0"), m("/run/user/65534"), comp.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/0"), m("/tmp/"), comp.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
|
||||
Bind(m("/dev/kvm"), m("/dev/kvm"), comp.BindWritable|comp.BindDevice|comp.BindOptional).
|
||||
Etc(m("/etc/"), "4a450b6596d7bc15bd01780eb9a607ac").
|
||||
Tmpfs(m("/run/user/1971"), 8192, 0755).
|
||||
Tmpfs(m("/run/nscd"), 8192, 0755).
|
||||
Tmpfs(m("/run/dbus"), 8192, 0755).
|
||||
Remount(m("/dev/"), syscall.MS_RDONLY).
|
||||
Remount(m("/"), syscall.MS_RDONLY),
|
||||
SeccompPresets: comp.PresetExt | comp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
HostAbstract: true,
|
||||
RetainSession: true,
|
||||
ForwardCancel: true,
|
||||
}},
|
||||
|
||||
{"nixos permissive defaults chromium", new(stubNixOS), &hst.Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
Identity: 9,
|
||||
Groups: []string{"video"},
|
||||
SessionBus: &hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{
|
||||
"org.freedesktop.portal.*": "*",
|
||||
},
|
||||
Broadcast: map[string]string{
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
SystemBus: &hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsRoot,
|
||||
Source: fhs.AbsRoot,
|
||||
Write: true,
|
||||
Special: true,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("dri"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("kvm"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsEtc,
|
||||
Source: fhs.AbsEtc,
|
||||
Special: true,
|
||||
}},
|
||||
},
|
||||
|
||||
Username: "chronos",
|
||||
Shell: m("/run/current-system/sw/bin/zsh"),
|
||||
Home: m("/home/chronos"),
|
||||
|
||||
Path: m("/run/current-system/sw/bin/zsh"),
|
||||
Args: []string{"zsh", "-c", "exec chromium "},
|
||||
|
||||
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty | hst.FShareRuntime | hst.FShareTmpdir,
|
||||
},
|
||||
}, hst.ID{
|
||||
0xeb, 0xf0, 0x83, 0xd1,
|
||||
0xb1, 0x75, 0x91, 0x17,
|
||||
0x82, 0xd4, 0x13, 0x36,
|
||||
0x9b, 0x64, 0xce, 0x7c,
|
||||
}, system.New(t.Context(), msg, 1000009).
|
||||
Ensure(m("/tmp/hakurei.0"), 0711).
|
||||
Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711).
|
||||
Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c"), acl.Execute).
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse")).
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{
|
||||
"org.freedesktop.portal.*": "*",
|
||||
},
|
||||
Broadcast: map[string]string{
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||
},
|
||||
Filter: true,
|
||||
}, &hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
}, dbus.ProxyPair{
|
||||
"unix:path=/run/user/1971/bus",
|
||||
"/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus",
|
||||
}, dbus.ProxyPair{
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
"/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket",
|
||||
}).
|
||||
UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write).
|
||||
UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read, acl.Write), &container.Params{
|
||||
|
||||
Dir: m("/home/chronos"),
|
||||
Path: m("/run/current-system/sw/bin/zsh"),
|
||||
Args: []string{"zsh", "-c", "exec chromium "},
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"HOME=/home/chronos",
|
||||
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=wayland",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Root(m("/"), comp.BindWritable).
|
||||
Proc(m("/proc/")).
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/65534"), comp.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/9"), m("/tmp/"), comp.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:65534:\n")).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/65534/wayland-0"), 0).
|
||||
Bind(m("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse"), m("/run/user/65534/pulse/native"), 0).
|
||||
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), m("/run/user/65534/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
Bind(m("/dev/dri"), m("/dev/dri"), comp.BindWritable|comp.BindDevice|comp.BindOptional).
|
||||
Bind(m("/dev/kvm"), m("/dev/kvm"), comp.BindWritable|comp.BindDevice|comp.BindOptional).
|
||||
Etc(m("/etc/"), "ebf083d1b175911782d413369b64ce7c").
|
||||
Tmpfs(m("/run/user/1971"), 8192, 0755).
|
||||
Tmpfs(m("/run/nscd"), 8192, 0755).
|
||||
Tmpfs(m("/run/dbus"), 8192, 0755).
|
||||
Remount(m("/dev/"), syscall.MS_RDONLY).
|
||||
Remount(m("/"), syscall.MS_RDONLY),
|
||||
SeccompPresets: comp.PresetExt | comp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
HostAbstract: true,
|
||||
RetainSession: true,
|
||||
ForwardCancel: true,
|
||||
}},
|
||||
|
||||
{"nixos chromium direct wayland", new(stubNixOS), &hst.Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: nil,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
f(&hst.FSBind{Source: m("/bin")}),
|
||||
f(&hst.FSBind{Source: m("/usr/bin/")}),
|
||||
f(&hst.FSBind{Source: m("/nix/store")}),
|
||||
f(&hst.FSBind{Source: m("/run/current-system")}),
|
||||
f(&hst.FSBind{Source: m("/sys/block"), Optional: true}),
|
||||
f(&hst.FSBind{Source: m("/sys/bus"), Optional: true}),
|
||||
f(&hst.FSBind{Source: m("/sys/class"), Optional: true}),
|
||||
f(&hst.FSBind{Source: m("/sys/dev"), Optional: true}),
|
||||
f(&hst.FSBind{Source: m("/sys/devices"), Optional: true}),
|
||||
f(&hst.FSBind{Source: m("/run/opengl-driver")}),
|
||||
f(&hst.FSBind{Source: m("/dev/dri"), Device: true, Optional: true}),
|
||||
f(&hst.FSBind{Source: m("/etc/"), Target: m("/etc/"), Special: true}),
|
||||
f(&hst.FSBind{Source: m("/var/lib/persist/module/hakurei/0/1"), Write: true, Ensure: true}),
|
||||
},
|
||||
|
||||
Username: "u0_a1",
|
||||
Shell: m("/run/current-system/sw/bin/zsh"),
|
||||
Home: m("/var/lib/persist/module/hakurei/0/1"),
|
||||
|
||||
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
|
||||
|
||||
Flags: hst.FUserns | hst.FHostNet | hst.FMapRealUID | hst.FShareRuntime | hst.FShareTmpdir,
|
||||
},
|
||||
SystemBus: &hst.BusConfig{
|
||||
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
|
||||
Filter: true,
|
||||
},
|
||||
SessionBus: &hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||
Filter: true,
|
||||
},
|
||||
DirectWayland: true,
|
||||
|
||||
Identity: 1, Groups: []string{},
|
||||
}, hst.ID{
|
||||
0x8e, 0x2c, 0x76, 0xb0,
|
||||
0x66, 0xda, 0xbe, 0x57,
|
||||
0x4c, 0xf0, 0x73, 0xbd,
|
||||
0xb4, 0x6e, 0xb5, 0xc1,
|
||||
}, system.New(t.Context(), msg, 1000001).
|
||||
Ensure(m("/tmp/hakurei.0"), 0711).
|
||||
Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/runtime/1"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read, acl.Write, acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute).
|
||||
Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read, acl.Write, acl.Execute).
|
||||
Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute).
|
||||
Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), 0700).UpdatePermType(system.Process, m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1"), acl.Execute).
|
||||
Link(m("/run/user/1971/pulse/native"), m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse")).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
|
||||
MustProxyDBus(&hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5", "org.kde.kwalletd6",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||
Filter: true,
|
||||
}, &hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
}, dbus.ProxyPair{
|
||||
"unix:path=/run/user/1971/bus",
|
||||
"/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus",
|
||||
}, dbus.ProxyPair{
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
"/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket",
|
||||
}).
|
||||
UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write).
|
||||
UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write), &container.Params{
|
||||
|
||||
Uid: 1971,
|
||||
Gid: 100,
|
||||
Dir: m("/var/lib/persist/module/hakurei/0/1"),
|
||||
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
|
||||
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"HOME=/var/lib/persist/module/hakurei/0/1",
|
||||
"PULSE_COOKIE=" + hst.PrivateTmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=u0_a1",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/1971",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=wayland",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Proc(m("/proc/")).
|
||||
Tmpfs(hst.AbsPrivateTmp, 4096, 0755).
|
||||
DevWritable(m("/dev/"), true).
|
||||
Tmpfs(m("/dev/shm"), 0, 01777).
|
||||
Tmpfs(m("/run/user/"), 4096, 0755).
|
||||
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), comp.BindWritable).
|
||||
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), comp.BindWritable).
|
||||
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
|
||||
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
|
||||
Bind(m("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse"), m("/run/user/1971/pulse/native"), 0).
|
||||
Place(m(hst.PrivateTmp+"/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
Bind(m("/bin"), m("/bin"), 0).
|
||||
Bind(m("/usr/bin/"), m("/usr/bin/"), 0).
|
||||
Bind(m("/nix/store"), m("/nix/store"), 0).
|
||||
Bind(m("/run/current-system"), m("/run/current-system"), 0).
|
||||
Bind(m("/sys/block"), m("/sys/block"), comp.BindOptional).
|
||||
Bind(m("/sys/bus"), m("/sys/bus"), comp.BindOptional).
|
||||
Bind(m("/sys/class"), m("/sys/class"), comp.BindOptional).
|
||||
Bind(m("/sys/dev"), m("/sys/dev"), comp.BindOptional).
|
||||
Bind(m("/sys/devices"), m("/sys/devices"), comp.BindOptional).
|
||||
Bind(m("/run/opengl-driver"), m("/run/opengl-driver"), 0).
|
||||
Bind(m("/dev/dri"), m("/dev/dri"), comp.BindDevice|comp.BindWritable|comp.BindOptional).
|
||||
Etc(m("/etc/"), "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||
Bind(m("/var/lib/persist/module/hakurei/0/1"), m("/var/lib/persist/module/hakurei/0/1"), comp.BindWritable|comp.BindEnsure).
|
||||
Remount(m("/dev/"), syscall.MS_RDONLY).
|
||||
Remount(m("/"), syscall.MS_RDONLY),
|
||||
SeccompPresets: comp.PresetExt | comp.PresetDenyTTY | comp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
ForwardCancel: true,
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
gr, gw := io.Pipe()
|
||||
|
||||
var gotSys *system.I
|
||||
{
|
||||
sPriv := newOutcomeState(tc.k, msg, &tc.id, tc.config, &Hsu{k: tc.k})
|
||||
if err := sPriv.populateLocal(tc.k, msg); err != nil {
|
||||
t.Fatalf("populateLocal: error = %#v", err)
|
||||
}
|
||||
|
||||
gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap())
|
||||
if err := sPriv.newSys(tc.config, gotSys).toSystem(); err != nil {
|
||||
t.Fatalf("toSystem: error = %#v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
e := gob.NewEncoder(gw)
|
||||
if err := errors.Join(e.Encode(&sPriv)); err != nil {
|
||||
t.Errorf("Encode: error = %v", err)
|
||||
panic("unexpected encode fault")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var gotParams *container.Params
|
||||
{
|
||||
var sShim outcomeState
|
||||
|
||||
d := gob.NewDecoder(gr)
|
||||
if err := errors.Join(d.Decode(&sShim)); err != nil {
|
||||
t.Fatalf("Decode: error = %v", err)
|
||||
}
|
||||
if err := sShim.populateLocal(tc.k, msg); err != nil {
|
||||
t.Fatalf("populateLocal: error = %#v", err)
|
||||
}
|
||||
|
||||
stateParams := sShim.newParams()
|
||||
for _, op := range sShim.Shim.Ops {
|
||||
if err := op.toContainer(stateParams); err != nil {
|
||||
t.Fatalf("toContainer: error = %#v", err)
|
||||
}
|
||||
}
|
||||
gotParams = stateParams.params
|
||||
}
|
||||
|
||||
t.Run("sys", func(t *testing.T) {
|
||||
if !gotSys.Equal(tc.wantSys) {
|
||||
t.Errorf("toSystem: sys = %#v, want %#v", gotSys, tc.wantSys)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("params", func(t *testing.T) {
|
||||
if !reflect.DeepEqual(gotParams, tc.wantParams) {
|
||||
t.Errorf("toContainer: params =\n%s\n, want\n%s", mustMarshal(gotParams), mustMarshal(tc.wantParams))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
|
||||
e = make([]fs.DirEntry, len(names))
|
||||
for i, name := range names {
|
||||
e[i] = stubDirEntryPath(name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type stubDirEntryPath string
|
||||
|
||||
func (p stubDirEntryPath) Name() string { return string(p) }
|
||||
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
|
||||
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
|
||||
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
|
||||
|
||||
type stubFileInfoMode fs.FileMode
|
||||
|
||||
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
|
||||
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
|
||||
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
|
||||
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
|
||||
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
|
||||
|
||||
type stubFileInfoIsDir bool
|
||||
|
||||
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
|
||||
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
|
||||
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
|
||||
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
|
||||
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
|
||||
|
||||
type stubFileInfoPulseCookie struct{ stubFileInfoIsDir }
|
||||
|
||||
func (s stubFileInfoPulseCookie) Size() int64 { return pulseCookieSizeMax }
|
||||
|
||||
type stubOsFileReadCloser struct{ io.ReadCloser }
|
||||
|
||||
func (s stubOsFileReadCloser) Name() string { panic("attempting to call Name") }
|
||||
func (s stubOsFileReadCloser) Write([]byte) (int, error) { panic("attempting to call Write") }
|
||||
func (s stubOsFileReadCloser) Stat() (fs.FileInfo, error) { panic("attempting to call Stat") }
|
||||
|
||||
type stubNixOS struct {
|
||||
usernameErr map[string]error
|
||||
panicDispatcher
|
||||
}
|
||||
|
||||
func (k *stubNixOS) getpid() int { return 0xdeadbeef }
|
||||
func (k *stubNixOS) getuid() int { return 1971 }
|
||||
func (k *stubNixOS) getgid() int { return 100 }
|
||||
|
||||
func (k *stubNixOS) lookupEnv(key string) (string, bool) {
|
||||
switch key {
|
||||
case "SHELL":
|
||||
return "/run/current-system/sw/bin/zsh", true
|
||||
case "TERM":
|
||||
return "xterm-256color", true
|
||||
case "WAYLAND_DISPLAY":
|
||||
return "wayland-0", true
|
||||
case "PULSE_COOKIE":
|
||||
return "", false
|
||||
case "HOME":
|
||||
return "/home/ophestra", true
|
||||
case "XDG_RUNTIME_DIR":
|
||||
return "/run/user/1971", true
|
||||
case "XDG_CONFIG_HOME":
|
||||
return "/home/ophestra/xdg/config", true
|
||||
case "DBUS_SYSTEM_BUS_ADDRESS":
|
||||
return "", false
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
|
||||
switch name {
|
||||
case "/var/run/nscd":
|
||||
return nil, nil
|
||||
case "/run/user/1971/pulse":
|
||||
return nil, nil
|
||||
case "/run/user/1971/pulse/native":
|
||||
return stubFileInfoMode(0666), nil
|
||||
case "/home/ophestra/.pulse-cookie":
|
||||
return stubFileInfoIsDir(true), nil
|
||||
case "/home/ophestra/xdg/config/pulse/cookie":
|
||||
return stubFileInfoPulseCookie{false}, nil
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to stat unexpected path %q", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) open(name string) (osFile, error) {
|
||||
switch name {
|
||||
case "/home/ophestra/xdg/config/pulse/cookie":
|
||||
return stubOsFileReadCloser{io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{0}, pulseCookieSizeMax)))}, nil
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to open unexpected path %q", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) readdir(name string) ([]fs.DirEntry, error) {
|
||||
switch name {
|
||||
case "/":
|
||||
return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib",
|
||||
"lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
|
||||
|
||||
case "/run":
|
||||
return stubDirEntries("agetty.reload", "binfmt", "booted-system",
|
||||
"credentials", "cryptsetup", "current-system", "dbus", "host", "keys",
|
||||
"libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager",
|
||||
"nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm",
|
||||
"store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2",
|
||||
"user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state")
|
||||
|
||||
case "/etc":
|
||||
return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default",
|
||||
"ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid",
|
||||
"hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd",
|
||||
"libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm",
|
||||
"machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc",
|
||||
"netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf",
|
||||
"opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1",
|
||||
"profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba",
|
||||
"sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh",
|
||||
"ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc",
|
||||
"zoneinfo", "zprofile", "zshenv", "zshrc")
|
||||
|
||||
case "/var/lib/hakurei/base/org.debian":
|
||||
return stubDirEntries("bin", "dev", "etc", "home", "lib64", "lost+found",
|
||||
"mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to read unexpected directory %q", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) tempdir() string { return "/tmp/" }
|
||||
|
||||
func (k *stubNixOS) evalSymlinks(path string) (string, error) {
|
||||
switch path {
|
||||
case "/var/run/nscd":
|
||||
return "/run/nscd", nil
|
||||
case "/run/user/1971":
|
||||
return "/run/user/1971", nil
|
||||
case "/tmp/hakurei.0":
|
||||
return "/tmp/hakurei.0", nil
|
||||
case "/var/run/dbus":
|
||||
return "/run/dbus", nil
|
||||
case "/dev/kvm":
|
||||
return "/dev/kvm", nil
|
||||
case "/etc/":
|
||||
return "/etc/", nil
|
||||
case "/bin":
|
||||
return "/bin", nil
|
||||
case "/boot":
|
||||
return "/boot", nil
|
||||
case "/home":
|
||||
return "/home", nil
|
||||
case "/lib":
|
||||
return "/lib", nil
|
||||
case "/lib64":
|
||||
return "/lib64", nil
|
||||
case "/nix":
|
||||
return "/nix", nil
|
||||
case "/root":
|
||||
return "/root", nil
|
||||
case "/run":
|
||||
return "/run", nil
|
||||
case "/srv":
|
||||
return "/srv", nil
|
||||
case "/sys":
|
||||
return "/sys", nil
|
||||
case "/usr":
|
||||
return "/usr", nil
|
||||
case "/var":
|
||||
return "/var", nil
|
||||
case "/dev/dri":
|
||||
return "/dev/dri", nil
|
||||
case "/usr/bin/":
|
||||
return "/usr/bin/", nil
|
||||
case "/nix/store":
|
||||
return "/nix/store", nil
|
||||
case "/run/current-system":
|
||||
return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos-system-satori-25.05.99999999.aaaaaaa", nil
|
||||
case "/sys/block":
|
||||
return "/sys/block", nil
|
||||
case "/sys/bus":
|
||||
return "/sys/bus", nil
|
||||
case "/sys/class":
|
||||
return "/sys/class", nil
|
||||
case "/sys/dev":
|
||||
return "/sys/dev", nil
|
||||
case "/sys/devices":
|
||||
return "/sys/devices", nil
|
||||
case "/run/opengl-driver":
|
||||
return "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-graphics-drivers", nil
|
||||
case "/var/lib/persist/module/hakurei/0/1":
|
||||
return "/var/lib/persist/module/hakurei/0/1", nil
|
||||
|
||||
case "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper":
|
||||
return "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper", nil
|
||||
case "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work":
|
||||
return "/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work", nil
|
||||
case "/var/lib/hakurei/base/org.nixos/ro-store":
|
||||
return "/var/lib/hakurei/base/org.nixos/ro-store", nil
|
||||
case "/var/lib/hakurei/u0/org.chromium.Chromium":
|
||||
return "/var/lib/hakurei/u0/org.chromium.Chromium", nil
|
||||
case "/var/lib/hakurei/base/org.debian/bin":
|
||||
return "/var/lib/hakurei/base/org.debian/bin", nil
|
||||
case "/var/lib/hakurei/base/org.debian/home":
|
||||
return "/var/lib/hakurei/base/org.debian/home", nil
|
||||
case "/var/lib/hakurei/base/org.debian/lib64":
|
||||
return "/var/lib/hakurei/base/org.debian/lib64", nil
|
||||
case "/var/lib/hakurei/base/org.debian/lost+found":
|
||||
return "/var/lib/hakurei/base/org.debian/lost+found", nil
|
||||
case "/var/lib/hakurei/base/org.debian/nix":
|
||||
return "/var/lib/hakurei/base/org.debian/nix", nil
|
||||
case "/var/lib/hakurei/base/org.debian/root":
|
||||
return "/var/lib/hakurei/base/org.debian/root", nil
|
||||
case "/var/lib/hakurei/base/org.debian/run":
|
||||
return "/var/lib/hakurei/base/org.debian/run", nil
|
||||
case "/var/lib/hakurei/base/org.debian/srv":
|
||||
return "/var/lib/hakurei/base/org.debian/srv", nil
|
||||
case "/var/lib/hakurei/base/org.debian/sys":
|
||||
return "/var/lib/hakurei/base/org.debian/sys", nil
|
||||
case "/var/lib/hakurei/base/org.debian/usr":
|
||||
return "/var/lib/hakurei/base/org.debian/usr", nil
|
||||
case "/var/lib/hakurei/base/org.debian/var":
|
||||
return "/var/lib/hakurei/base/org.debian/var", nil
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to evaluate unexpected path %q", path))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) lookupGroupId(name string) (string, error) {
|
||||
switch name {
|
||||
case "video":
|
||||
return "26", nil
|
||||
default:
|
||||
return "", user.UnknownGroupError(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
switch cmd.Path {
|
||||
case "/proc/nonexistent/hsu":
|
||||
return []byte{'0'}, nil
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected cmd %#v", cmd))
|
||||
}
|
||||
}
|
||||
|
||||
func (k *stubNixOS) overflowUid(message.Msg) int { return 65534 }
|
||||
func (k *stubNixOS) overflowGid(message.Msg) int { return 65534 }
|
||||
|
||||
func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") }
|
||||
|
||||
func (k *stubNixOS) dbusAddress() (string, string) {
|
||||
return "unix:path=/run/user/1971/bus", "unix:path=/var/run/dbus/system_bus_socket"
|
||||
}
|
||||
|
||||
func (k *stubNixOS) fatalf(format string, v ...any) { panic(fmt.Sprintf(format, v...)) }
|
||||
|
||||
func (k *stubNixOS) isVerbose() bool { return true }
|
||||
func (k *stubNixOS) verbose(v ...any) { log.Print(v...) }
|
||||
func (k *stubNixOS) verbosef(format string, v ...any) { log.Printf(format, v...) }
|
||||
303
internal/outcome/outcome.go
Normal file
303
internal/outcome/outcome.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Package outcome implements the outcome of the privileged and container sides of a hakurei container.
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"strconv"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
|
||||
// It should be large enough to fit all insertions by outcomeOp.toContainer.
|
||||
const envAllocSize = 1 << 6
|
||||
|
||||
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
||||
|
||||
// stringPair stores a value and its string representation.
|
||||
type stringPair[T comparable] struct {
|
||||
v T
|
||||
s string
|
||||
}
|
||||
|
||||
func (s *stringPair[T]) unwrap() T { return s.v }
|
||||
func (s *stringPair[T]) String() string { return s.s }
|
||||
|
||||
// outcomeState is copied to the shim process and available while applying outcomeOp.
|
||||
// This is transmitted from the priv side to the shim, so exported fields should be kept to a minimum.
|
||||
type outcomeState struct {
|
||||
// Params only used by the shim process. Populated by populateEarly.
|
||||
Shim *shimParams
|
||||
|
||||
// Generated and accounted for by the caller.
|
||||
ID *hst.ID
|
||||
// Copied from ID.
|
||||
id *stringPair[hst.ID]
|
||||
|
||||
// Copied from the [hst.Config] field of the same name.
|
||||
Identity int
|
||||
// Copied from Identity.
|
||||
identity *stringPair[int]
|
||||
// Returned by [Hsu.MustID].
|
||||
UserID int
|
||||
// Target init namespace uid resolved from UserID and identity.
|
||||
uid *stringPair[int]
|
||||
|
||||
// Included as part of [hst.Config], transmitted as-is unless permissive defaults.
|
||||
Container *hst.ContainerConfig
|
||||
|
||||
// Mapped credentials within container user namespace.
|
||||
Mapuid, Mapgid int
|
||||
// Copied from their respective exported values.
|
||||
mapuid, mapgid *stringPair[int]
|
||||
|
||||
// Copied from [EnvPaths] per-process.
|
||||
sc hst.Paths
|
||||
*env.Paths
|
||||
|
||||
// Copied via populateLocal.
|
||||
k syscallDispatcher
|
||||
// Copied via populateLocal.
|
||||
msg message.Msg
|
||||
}
|
||||
|
||||
// valid checks outcomeState to be safe for use with outcomeOp.
|
||||
func (s *outcomeState) valid() bool {
|
||||
return s != nil &&
|
||||
s.Shim.valid() &&
|
||||
s.ID != nil &&
|
||||
s.Container != nil &&
|
||||
s.Paths != nil
|
||||
}
|
||||
|
||||
// newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher.
|
||||
func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *hst.Config, hsu *Hsu) *outcomeState {
|
||||
s := outcomeState{
|
||||
Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()},
|
||||
ID: id,
|
||||
Identity: config.Identity,
|
||||
UserID: hsu.MustID(msg),
|
||||
Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }),
|
||||
Container: config.Container,
|
||||
}
|
||||
|
||||
// enforce bounds and default early
|
||||
if s.Container.WaitDelay < 0 {
|
||||
s.Shim.WaitDelay = 0
|
||||
} else if s.Container.WaitDelay == 0 {
|
||||
s.Shim.WaitDelay = hst.WaitDelayDefault
|
||||
} else if s.Container.WaitDelay > hst.WaitDelayMax {
|
||||
s.Shim.WaitDelay = hst.WaitDelayMax
|
||||
} else {
|
||||
s.Shim.WaitDelay = s.Container.WaitDelay
|
||||
}
|
||||
|
||||
if s.Container.Flags&hst.FMapRealUID != 0 {
|
||||
s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
|
||||
} else {
|
||||
s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// populateLocal populates unexported fields from transmitted exported fields.
|
||||
// These fields are cheaper to recompute per-process.
|
||||
func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error {
|
||||
if !s.valid() || k == nil || msg == nil {
|
||||
return newWithMessage("impossible outcome state reached")
|
||||
}
|
||||
|
||||
if s.k != nil || s.msg != nil {
|
||||
panic("attempting to call populateLocal twice")
|
||||
}
|
||||
s.k = k
|
||||
s.msg = msg
|
||||
|
||||
s.id = &stringPair[hst.ID]{*s.ID, s.ID.String()}
|
||||
|
||||
s.Copy(&s.sc, s.UserID)
|
||||
msg.Verbosef("process share directory at %q, runtime directory at %q", s.sc.SharePath, s.sc.RunDirPath)
|
||||
|
||||
s.identity = newInt(s.Identity)
|
||||
s.mapuid, s.mapgid = newInt(s.Mapuid), newInt(s.Mapgid)
|
||||
s.uid = newInt(HsuUid(s.UserID, s.identity.unwrap()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// instancePath returns a path formatted for outcomeStateSys.instance.
|
||||
// This method must only be called from outcomeOp.toContainer if
|
||||
// outcomeOp.toSystem has already called outcomeStateSys.instance.
|
||||
func (s *outcomeState) instancePath() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) }
|
||||
|
||||
// runtimePath returns a path formatted for outcomeStateSys.runtime.
|
||||
// This method must only be called from outcomeOp.toContainer if
|
||||
// outcomeOp.toSystem has already called outcomeStateSys.runtime.
|
||||
func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) }
|
||||
|
||||
// outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only.
|
||||
// Implementations of outcomeOp must not access fields other than sys unless explicitly stated.
|
||||
type outcomeStateSys struct {
|
||||
// Whether XDG_RUNTIME_DIR is used post hsu.
|
||||
useRuntimeDir bool
|
||||
// Process-specific directory in TMPDIR, nil if unused.
|
||||
sharePath *check.Absolute
|
||||
// Process-specific directory in XDG_RUNTIME_DIR, nil if unused.
|
||||
runtimeSharePath *check.Absolute
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
appId string
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
et hst.Enablement
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
|
||||
extraPerms []hst.ExtraPermConfig
|
||||
// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
|
||||
sessionBus, systemBus *hst.BusConfig
|
||||
|
||||
sys *system.I
|
||||
*outcomeState
|
||||
}
|
||||
|
||||
// newSys returns the address of a new outcomeStateSys embedding the current outcomeState.
|
||||
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
|
||||
return &outcomeStateSys{
|
||||
appId: config.ID, et: config.Enablements.Unwrap(),
|
||||
directWayland: config.DirectWayland, extraPerms: config.ExtraPerms,
|
||||
sessionBus: config.SessionBus, systemBus: config.SystemBus,
|
||||
sys: sys, outcomeState: s,
|
||||
}
|
||||
}
|
||||
|
||||
// newParams returns the address of a new outcomeStateParams embedding the current outcomeState.
|
||||
func (s *outcomeState) newParams() *outcomeStateParams {
|
||||
stateParams := outcomeStateParams{params: new(container.Params), outcomeState: s}
|
||||
if s.Container.Env == nil {
|
||||
stateParams.env = make(map[string]string, envAllocSize)
|
||||
} else {
|
||||
stateParams.env = maps.Clone(s.Container.Env)
|
||||
}
|
||||
return &stateParams
|
||||
}
|
||||
|
||||
// ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required.
|
||||
func (state *outcomeStateSys) ensureRuntimeDir() {
|
||||
if state.useRuntimeDir {
|
||||
return
|
||||
}
|
||||
state.useRuntimeDir = true
|
||||
state.sys.Ensure(state.sc.RunDirPath, 0700)
|
||||
state.sys.UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
|
||||
state.sys.Ensure(state.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
|
||||
state.sys.UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute)
|
||||
}
|
||||
|
||||
// instance returns the pathname to a process-specific directory within TMPDIR.
|
||||
// This directory must only hold entries bound to [system.Process].
|
||||
func (state *outcomeStateSys) instance() *check.Absolute {
|
||||
if state.sharePath != nil {
|
||||
return state.sharePath
|
||||
}
|
||||
state.sharePath = state.instancePath()
|
||||
state.sys.Ephemeral(system.Process, state.sharePath, 0711)
|
||||
return state.sharePath
|
||||
}
|
||||
|
||||
// runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR.
|
||||
// This directory must only hold entries bound to [system.Process].
|
||||
func (state *outcomeStateSys) runtime() *check.Absolute {
|
||||
if state.runtimeSharePath != nil {
|
||||
return state.runtimeSharePath
|
||||
}
|
||||
state.ensureRuntimeDir()
|
||||
state.runtimeSharePath = state.runtimePath()
|
||||
state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700)
|
||||
state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute)
|
||||
return state.runtimeSharePath
|
||||
}
|
||||
|
||||
// outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only.
|
||||
type outcomeStateParams struct {
|
||||
// Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used.
|
||||
params *container.Params
|
||||
// Collapsed into the Env slice in [container.Params] by the final outcomeOp.
|
||||
env map[string]string
|
||||
|
||||
// Filesystems with the optional root sliced off if present. Populated by spParamsOp.
|
||||
// Safe for use by spFilesystemOp.
|
||||
filesystem []hst.FilesystemConfigJSON
|
||||
|
||||
// Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid.
|
||||
// Populated by spRuntimeOp.
|
||||
runtimeDir *check.Absolute
|
||||
|
||||
as hst.ApplyState
|
||||
*outcomeState
|
||||
}
|
||||
|
||||
// errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission.
|
||||
var errNotEnabled = errors.New("op not enabled in the configuration")
|
||||
|
||||
// An outcomeOp inflicts an outcome on [system.I] and contains enough information to
|
||||
// inflict it on [container.Params] in a separate process.
|
||||
// An implementation of outcomeOp must store cross-process states in exported fields only.
|
||||
type outcomeOp interface {
|
||||
// toSystem inflicts the current outcome on [system.I] in the priv side process.
|
||||
toSystem(state *outcomeStateSys) error
|
||||
|
||||
// toContainer inflicts the current outcome on [container.Params] in the shim process.
|
||||
// The implementation must not write to the Env field of [container.Params] as it will be overwritten
|
||||
// by flattened env map.
|
||||
toContainer(state *outcomeStateParams) error
|
||||
}
|
||||
|
||||
// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops.
|
||||
// This function assumes the caller has already called the Validate method on [hst.Config]
|
||||
// and checked that it returns nil.
|
||||
func (state *outcomeStateSys) toSystem() error {
|
||||
if state.Shim == nil || state.Shim.Ops != nil {
|
||||
return newWithMessage("invalid ops state reached")
|
||||
}
|
||||
|
||||
ops := [...]outcomeOp{
|
||||
// must run first
|
||||
&spParamsOp{},
|
||||
|
||||
&spRuntimeOp{},
|
||||
spTmpdirOp{},
|
||||
spAccountOp{},
|
||||
|
||||
// optional via enablements
|
||||
&spWaylandOp{},
|
||||
&spX11Op{},
|
||||
&spPulseOp{},
|
||||
&spDBusOp{},
|
||||
|
||||
// must run last
|
||||
&spFilesystemOp{},
|
||||
}
|
||||
|
||||
state.Shim.Ops = make([]outcomeOp, 0, len(ops))
|
||||
for _, op := range ops {
|
||||
if err := op.toSystem(state); err != nil {
|
||||
// this error is used internally to exclude this outcomeOp from transmission
|
||||
if errors.Is(err, errNotEnabled) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
state.Shim.Ops = append(state.Shim.Ops, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
34
internal/outcome/outcome_test.go
Normal file
34
internal/outcome/outcome_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
)
|
||||
|
||||
func TestOutcomeStateValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
s *outcomeState
|
||||
want bool
|
||||
}{
|
||||
{"nil", nil, false},
|
||||
{"zero", new(outcomeState), false},
|
||||
{"shim", &outcomeState{Shim: &shimParams{PrivPID: -1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, false},
|
||||
{"id", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, false},
|
||||
{"container", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Paths: new(env.Paths)}, false},
|
||||
{"envpaths", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig)}, false},
|
||||
{"valid", &outcomeState{Shim: &shimParams{PrivPID: 1, Ops: []outcomeOp{}}, ID: new(hst.ID), Container: new(hst.ContainerConfig), Paths: new(env.Paths)}, true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tc.s.valid(); got != tc.want {
|
||||
t.Errorf("valid: %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
321
internal/outcome/process.go
Normal file
321
internal/outcome/process.go
Normal file
@@ -0,0 +1,321 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/state"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
// Duration to wait for shim to exit on top of container WaitDelay.
|
||||
const shimWaitTimeout = 5 * time.Second
|
||||
|
||||
// mainState holds persistent state bound to outcome.main.
|
||||
type mainState struct {
|
||||
// done is whether beforeExit has been called already.
|
||||
done bool
|
||||
|
||||
// Time is the exact point in time where the process was created.
|
||||
// Location must be set to UTC.
|
||||
//
|
||||
// Time is nil if no process was ever created.
|
||||
Time *time.Time
|
||||
|
||||
store state.Store
|
||||
cancel context.CancelFunc
|
||||
cmd *exec.Cmd
|
||||
cmdWait chan error
|
||||
|
||||
k *outcome
|
||||
message.Msg
|
||||
uintptr
|
||||
}
|
||||
|
||||
const (
|
||||
// mainNeedsRevert indicates the call to Commit has succeeded.
|
||||
mainNeedsRevert uintptr = 1 << iota
|
||||
// mainNeedsDestroy indicates the instance state entry is present in the store.
|
||||
mainNeedsDestroy
|
||||
)
|
||||
|
||||
// beforeExit must be called immediately before a call to [os.Exit].
|
||||
func (ms mainState) beforeExit(isFault bool) {
|
||||
if ms.done {
|
||||
panic("attempting to call beforeExit twice")
|
||||
}
|
||||
ms.done = true
|
||||
defer ms.BeforeExit()
|
||||
|
||||
if isFault && ms.cancel != nil {
|
||||
ms.cancel()
|
||||
}
|
||||
|
||||
var hasErr bool
|
||||
// updates hasErr but does not terminate
|
||||
perror := func(err error, message string) {
|
||||
hasErr = true
|
||||
printMessageError("cannot "+message+":", err)
|
||||
}
|
||||
exitCode := 1
|
||||
defer func() {
|
||||
if hasErr {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
|
||||
// this also handles wait for a non-fault termination
|
||||
if ms.cmd != nil && ms.cmdWait != nil {
|
||||
waitDone := make(chan struct{})
|
||||
|
||||
// this ties waitDone to ctx with the additional compensated timeout duration
|
||||
go func() { <-ms.k.ctx.Done(); time.Sleep(ms.k.state.Shim.WaitDelay + shimWaitTimeout); close(waitDone) }()
|
||||
|
||||
select {
|
||||
case err := <-ms.cmdWait:
|
||||
wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||
if ok {
|
||||
if v := wstatus.ExitStatus(); v != 0 {
|
||||
hasErr = true
|
||||
exitCode = v
|
||||
}
|
||||
}
|
||||
|
||||
if ms.IsVerbose() {
|
||||
if !ok {
|
||||
if err != nil {
|
||||
ms.Verbosef("wait: %v", err)
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case wstatus.Exited():
|
||||
ms.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
|
||||
|
||||
case wstatus.CoreDump():
|
||||
ms.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
|
||||
|
||||
case wstatus.Signaled():
|
||||
ms.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
|
||||
|
||||
default:
|
||||
ms.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case <-waitDone:
|
||||
ms.Resume()
|
||||
// this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
|
||||
// This is different from the container failing to terminate within its timeout period, as that is enforced
|
||||
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
|
||||
log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
|
||||
}
|
||||
|
||||
ms.Resume()
|
||||
}
|
||||
|
||||
if ms.uintptr&mainNeedsRevert != 0 {
|
||||
if ok, err := ms.store.Do(ms.k.state.identity.unwrap(), func(c state.Cursor) {
|
||||
if ms.uintptr&mainNeedsDestroy != 0 {
|
||||
if err := c.Destroy(ms.k.state.id.unwrap()); err != nil {
|
||||
perror(err, "destroy state entry")
|
||||
}
|
||||
}
|
||||
|
||||
var rt hst.Enablement
|
||||
if states, err := c.Load(); err != nil {
|
||||
// it is impossible to continue from this point;
|
||||
// revert per-process state here to limit damage
|
||||
ec := system.Process
|
||||
if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
|
||||
var joinError interface {
|
||||
Unwrap() []error
|
||||
error
|
||||
}
|
||||
if !errors.As(revertErr, &joinError) || joinError == nil {
|
||||
perror(revertErr, "revert system setup")
|
||||
} else {
|
||||
for _, v := range joinError.Unwrap() {
|
||||
perror(v, "revert system setup step")
|
||||
}
|
||||
}
|
||||
}
|
||||
perror(err, "load instance states")
|
||||
} else {
|
||||
ec := system.Process
|
||||
if l := len(states); l == 0 {
|
||||
ec |= system.User
|
||||
} else {
|
||||
ms.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
|
||||
}
|
||||
|
||||
// accumulate enablements of remaining launchers
|
||||
for i, s := range states {
|
||||
if s.Config != nil {
|
||||
rt |= s.Config.Enablements.Unwrap()
|
||||
} else {
|
||||
log.Printf("state entry %d does not contain config", i)
|
||||
}
|
||||
}
|
||||
|
||||
ec |= rt ^ (hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse)
|
||||
if ms.IsVerbose() {
|
||||
if ec > 0 {
|
||||
ms.Verbose("reverting operations scope", system.TypeString(ec))
|
||||
}
|
||||
}
|
||||
|
||||
if err = ms.k.sys.Revert((*system.Criteria)(&ec)); err != nil {
|
||||
perror(err, "revert system setup")
|
||||
}
|
||||
}
|
||||
}); err != nil {
|
||||
if ok {
|
||||
perror(err, "unlock state store")
|
||||
} else {
|
||||
perror(err, "open state store")
|
||||
}
|
||||
}
|
||||
} else if ms.uintptr&mainNeedsDestroy != 0 {
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
|
||||
func (ms mainState) fatal(fallback string, ferr error) {
|
||||
printMessageError(fallback, ferr)
|
||||
ms.beforeExit(true)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// main carries out outcome and terminates. main does not return.
|
||||
func (k *outcome) main(msg message.Msg) {
|
||||
if !k.active.CompareAndSwap(false, true) {
|
||||
panic("outcome: attempted to run twice")
|
||||
}
|
||||
|
||||
if k.ctx == nil || k.sys == nil || k.state == nil {
|
||||
panic("outcome: did not finalise")
|
||||
}
|
||||
|
||||
// read comp value early for early failure
|
||||
hsuPath := internal.MustHsuPath()
|
||||
|
||||
// ms.beforeExit required beyond this point
|
||||
ms := &mainState{Msg: msg, k: k}
|
||||
|
||||
if err := k.sys.Commit(); err != nil {
|
||||
ms.fatal("cannot commit system setup:", err)
|
||||
}
|
||||
ms.uintptr |= mainNeedsRevert
|
||||
ms.store = state.NewMulti(msg, k.state.sc.RunDirPath)
|
||||
|
||||
ctx, cancel := context.WithCancel(k.ctx)
|
||||
defer cancel()
|
||||
ms.cancel = cancel
|
||||
|
||||
ms.cmd = exec.CommandContext(ctx, hsuPath.String())
|
||||
ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
ms.cmd.Dir = fhs.Root // container init enters final working directory
|
||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||
ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
|
||||
|
||||
var e *gob.Encoder
|
||||
if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
|
||||
ms.fatal("cannot create shim setup pipe:", err)
|
||||
} else {
|
||||
e = encoder
|
||||
ms.cmd.Env = []string{
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_IDENTITY=" + k.state.identity.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if len(k.supp) > 0 {
|
||||
msg.Verbosef("attaching supplementary group ids %s", k.supp)
|
||||
// interpreted by hsu
|
||||
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.supp, " "))
|
||||
}
|
||||
|
||||
msg.Verbosef("setuid helper at %s", hsuPath)
|
||||
msg.Suspend()
|
||||
if err := ms.cmd.Start(); err != nil {
|
||||
ms.fatal("cannot start setuid wrapper:", err)
|
||||
}
|
||||
startTime := time.Now().UTC()
|
||||
ms.cmdWait = make(chan error, 1)
|
||||
// this ties context back to the life of the process
|
||||
go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
|
||||
ms.Time = &startTime
|
||||
|
||||
// unfortunately the I/O here cannot be directly canceled;
|
||||
// the cancellation path leads to fatal in this case so that is fine
|
||||
select {
|
||||
case err := <-func() (setupErr chan error) {
|
||||
setupErr = make(chan error, 1)
|
||||
go func() { setupErr <- e.Encode(k.state) }()
|
||||
return
|
||||
}():
|
||||
if err != nil {
|
||||
msg.Resume()
|
||||
ms.fatal("cannot transmit shim config:", err)
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
msg.Resume()
|
||||
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
|
||||
}
|
||||
|
||||
// shim accepted setup payload, create process state
|
||||
if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c state.Cursor) {
|
||||
if err := c.Save(&hst.State{
|
||||
ID: k.state.id.unwrap(),
|
||||
PID: os.Getpid(),
|
||||
ShimPID: ms.cmd.Process.Pid,
|
||||
Config: k.config,
|
||||
Time: *ms.Time,
|
||||
}); err != nil {
|
||||
ms.fatal("cannot save state entry:", err)
|
||||
}
|
||||
}); err != nil {
|
||||
if ok {
|
||||
ms.uintptr |= mainNeedsDestroy
|
||||
ms.fatal("cannot unlock state store:", err)
|
||||
} else {
|
||||
ms.fatal("cannot open state store:", err)
|
||||
}
|
||||
}
|
||||
// state in store at this point, destroy defunct state entry on termination
|
||||
ms.uintptr |= mainNeedsDestroy
|
||||
|
||||
// beforeExit ties shim process to context
|
||||
ms.beforeExit(false)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// printMessageError prints the error message according to [message.GetMessage],
|
||||
// or fallback prepended to err if an error message is not available.
|
||||
func printMessageError(fallback string, err error) {
|
||||
m, ok := message.GetMessage(err)
|
||||
if !ok {
|
||||
log.Println(fallback, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Print(m)
|
||||
}
|
||||
65
internal/outcome/shim-signal.c
Normal file
65
internal/outcome/shim-signal.c
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "shim-signal.h"
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static pid_t hakurei_shim_param_ppid = -1;
|
||||
static int hakurei_shim_fd = -1;
|
||||
|
||||
static ssize_t hakurei_shim_write(const void *buf, size_t count) {
|
||||
int savedErrno = errno;
|
||||
ssize_t ret = write(hakurei_shim_fd, buf, count);
|
||||
if (ret == -1 && errno != EAGAIN)
|
||||
exit(EXIT_FAILURE);
|
||||
errno = savedErrno;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* see shim_linux.go for handling of the value */
|
||||
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
if (sig != SIGCONT || si == NULL) {
|
||||
/* unreachable */
|
||||
hakurei_shim_write("\2", 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (si->si_pid == hakurei_shim_param_ppid) {
|
||||
/* monitor requests shim exit */
|
||||
hakurei_shim_write("\0", 1);
|
||||
return;
|
||||
}
|
||||
|
||||
/* unexpected si_pid */
|
||||
hakurei_shim_write("\3", 1);
|
||||
|
||||
if (getppid() != hakurei_shim_param_ppid)
|
||||
/* shim orphaned before monitor delivers a signal */
|
||||
hakurei_shim_write("\1", 1);
|
||||
}
|
||||
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) {
|
||||
if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1)
|
||||
*(int *)NULL = 0; /* unreachable */
|
||||
|
||||
struct sigaction new_action = {0}, old_action = {0};
|
||||
if (sigaction(SIGCONT, NULL, &old_action) != 0)
|
||||
return;
|
||||
if (old_action.sa_handler != SIG_DFL) {
|
||||
errno = ENOTRECOVERABLE;
|
||||
return;
|
||||
}
|
||||
|
||||
new_action.sa_sigaction = hakurei_shim_sigaction;
|
||||
if (sigemptyset(&new_action.sa_mask) != 0)
|
||||
return;
|
||||
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
|
||||
|
||||
if (sigaction(SIGCONT, &new_action, NULL) != 0)
|
||||
return;
|
||||
|
||||
errno = 0;
|
||||
hakurei_shim_param_ppid = ppid;
|
||||
hakurei_shim_fd = fd;
|
||||
}
|
||||
3
internal/outcome/shim-signal.h
Normal file
3
internal/outcome/shim-signal.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#include <signal.h>
|
||||
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid, int fd);
|
||||
235
internal/outcome/shim.go
Normal file
235
internal/outcome/shim.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
//#include "shim-signal.h"
|
||||
import "C"
|
||||
|
||||
// setupContSignal sets up the SIGCONT signal handler for the cross-uid shim exit hack.
|
||||
// The signal handler is implemented in C, signals can be processed by reading from the returned reader.
|
||||
// The returned function must be called after all signal processing concludes.
|
||||
func setupContSignal(pid int) (io.ReadCloser, func(), error) {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return nil, nil, err
|
||||
} else if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(pid), C.int(w.Fd())); err != nil {
|
||||
_, _ = r.Close(), w.Close()
|
||||
return nil, nil, err
|
||||
} else {
|
||||
return r, func() { runtime.KeepAlive(w) }, nil
|
||||
}
|
||||
}
|
||||
|
||||
// shimEnv is the name of the environment variable storing decimal representation of
|
||||
// setup pipe fd for [container.Receive].
|
||||
const shimEnv = "HAKUREI_SHIM"
|
||||
|
||||
// shimParams is embedded in outcomeState and transmitted from priv side to shim.
|
||||
type shimParams struct {
|
||||
// Priv side pid, checked against ppid in signal handler for the syscall.SIGCONT hack.
|
||||
PrivPID int
|
||||
|
||||
// Duration to wait for after the initial process receives os.Interrupt before the container is killed.
|
||||
// Limits are enforced on the priv side.
|
||||
WaitDelay time.Duration
|
||||
|
||||
// Verbosity pass through from [message.Msg].
|
||||
Verbose bool
|
||||
|
||||
// Outcome setup ops, contains setup state. Populated by outcome.finalise.
|
||||
Ops []outcomeOp
|
||||
}
|
||||
|
||||
// valid checks shimParams to be safe for use.
|
||||
func (p *shimParams) valid() bool { return p != nil && p.PrivPID > 0 }
|
||||
|
||||
// shimName is the prefix used by log.std in the shim process.
|
||||
const shimName = "shim"
|
||||
|
||||
// Shim is called by the main function of the shim process and runs as the unconstrained target user.
|
||||
// Shim does not return.
|
||||
func Shim(msg message.Msg) {
|
||||
if msg == nil {
|
||||
msg = message.NewMsg(log.Default())
|
||||
}
|
||||
shimEntrypoint(direct{msg})
|
||||
}
|
||||
|
||||
func shimEntrypoint(k syscallDispatcher) {
|
||||
msg := k.getMsg()
|
||||
if msg == nil {
|
||||
panic("attempting to call shimEntrypoint with nil msg")
|
||||
} else if logger := msg.GetLogger(); logger != nil {
|
||||
logger.SetPrefix(shimName + ": ")
|
||||
logger.SetFlags(0)
|
||||
}
|
||||
|
||||
if err := k.setDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
k.fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
}
|
||||
|
||||
var (
|
||||
state outcomeState
|
||||
closeSetup func() error
|
||||
)
|
||||
if f, err := k.receive(shimEnv, &state, nil); err != nil {
|
||||
if errors.Is(err, syscall.EBADF) {
|
||||
k.fatal("invalid config descriptor")
|
||||
}
|
||||
if errors.Is(err, container.ErrReceiveEnv) {
|
||||
k.fatal(shimEnv + " not set")
|
||||
}
|
||||
|
||||
k.fatalf("cannot receive shim setup params: %v", err)
|
||||
} else {
|
||||
msg.SwapVerbose(state.Shim.Verbose)
|
||||
closeSetup = f
|
||||
|
||||
if err = state.populateLocal(k, msg); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
k.fatal(m)
|
||||
} else {
|
||||
k.fatalf("cannot populate local state: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
|
||||
var signalPipe io.ReadCloser
|
||||
if r, wKeepAlive, err := k.setupContSignal(state.Shim.PrivPID); err != nil {
|
||||
switch {
|
||||
case errors.As(err, new(*os.SyscallError)): // returned by os.Pipe
|
||||
k.fatal(err.Error())
|
||||
return
|
||||
|
||||
case errors.As(err, new(syscall.Errno)): // returned by hakurei_shim_setup_cont_signal
|
||||
k.fatalf("cannot install SIGCONT handler: %v", err)
|
||||
return
|
||||
|
||||
default: // unreachable
|
||||
k.fatalf("cannot set up exit request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
defer wKeepAlive()
|
||||
signalPipe = r
|
||||
}
|
||||
|
||||
// pdeath_signal delivery is checked as if the dying process called kill(2), see kernel/exit.c
|
||||
if err := k.prctl(syscall.PR_SET_PDEATHSIG, uintptr(syscall.SIGCONT), 0); err != nil {
|
||||
k.fatalf("cannot set parent-death signal: %v", err)
|
||||
}
|
||||
|
||||
stateParams := state.newParams()
|
||||
for _, op := range state.Shim.Ops {
|
||||
if err := op.toContainer(stateParams); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
k.fatal(m)
|
||||
} else {
|
||||
k.fatalf("cannot create container state: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shim exit outcomes
|
||||
var cancelContainer atomic.Pointer[context.CancelFunc]
|
||||
k.new(func(k syscallDispatcher, msg message.Msg) {
|
||||
buf := make([]byte, 1)
|
||||
for {
|
||||
if _, err := signalPipe.Read(buf); err != nil {
|
||||
k.fatalf("cannot read from signal pipe: %v", err)
|
||||
}
|
||||
|
||||
switch buf[0] {
|
||||
case 0: // got SIGCONT from monitor: shim exit requested
|
||||
if fp := cancelContainer.Load(); stateParams.params.ForwardCancel && fp != nil && *fp != nil {
|
||||
(*fp)()
|
||||
// shim now bound by ShimWaitDelay, implemented below
|
||||
continue
|
||||
}
|
||||
|
||||
// setup has not completed, terminate immediately
|
||||
msg.Resume()
|
||||
k.exit(hst.ExitRequest)
|
||||
return
|
||||
|
||||
case 1: // got SIGCONT after adoption: monitor died before delivering signal
|
||||
msg.BeforeExit()
|
||||
k.exit(hst.ExitOrphan)
|
||||
return
|
||||
|
||||
case 2: // unreachable
|
||||
msg.Verbose("sa_sigaction got invalid siginfo")
|
||||
|
||||
case 3: // got SIGCONT from unexpected process: hopefully the terminal driver
|
||||
msg.Verbose("got SIGCONT from unexpected process")
|
||||
|
||||
default: // unreachable
|
||||
k.fatalf("got invalid message %d from signal handler", buf[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if stateParams.params.Ops == nil {
|
||||
k.fatal("invalid container params")
|
||||
}
|
||||
|
||||
// close setup socket
|
||||
if err := closeSetup(); err != nil {
|
||||
msg.Verbosef("cannot close setup pipe: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
ctx, stop := k.notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
cancelContainer.Store(&stop)
|
||||
z := container.New(ctx, msg)
|
||||
z.Params = *stateParams.params
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
|
||||
// bounds and default enforced in finalise.go
|
||||
z.WaitDelay = state.Shim.WaitDelay
|
||||
|
||||
if err := k.containerStart(z); err != nil {
|
||||
printMessageError("cannot start container:", err)
|
||||
k.exit(hst.ExitFailure)
|
||||
}
|
||||
if err := k.containerServe(z); err != nil {
|
||||
printMessageError("cannot configure container:", err)
|
||||
}
|
||||
|
||||
if err := k.seccompLoad(
|
||||
seccomp.Preset(comp.PresetStrict, seccomp.AllowMultiarch),
|
||||
seccomp.AllowMultiarch,
|
||||
); err != nil {
|
||||
k.fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
|
||||
if err := k.containerWait(z); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
k.exit(hst.ExitCancel)
|
||||
}
|
||||
msg.Verbosef("cannot wait: %v", err)
|
||||
k.exit(127)
|
||||
}
|
||||
k.exit(exitError.ExitCode())
|
||||
}
|
||||
}
|
||||
156
internal/outcome/shim_test.go
Normal file
156
internal/outcome/shim_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
)
|
||||
|
||||
func TestShimEntrypoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
shimPreset := seccomp.Preset(comp.PresetStrict, seccomp.AllowMultiarch)
|
||||
templateParams := &container.Params{
|
||||
Dir: m("/data/data/org.chromium.Chromium"),
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket",
|
||||
"GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
"HOME=/data/data/org.chromium.Chromium",
|
||||
"PULSE_COOKIE=/.hakurei/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1000/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/1000",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=wayland",
|
||||
},
|
||||
|
||||
// spParamsOp
|
||||
Hostname: "localhost",
|
||||
RetainSession: true,
|
||||
HostNet: true,
|
||||
HostAbstract: true,
|
||||
ForwardCancel: true,
|
||||
Path: m("/run/current-system/sw/bin/chromium"),
|
||||
Args: []string{
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland",
|
||||
},
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Uid: 1000,
|
||||
Gid: 100,
|
||||
|
||||
Ops: new(container.Ops).
|
||||
// resolveRoot
|
||||
Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
|
||||
// spParamsOp
|
||||
Proc(fhs.AbsProc).
|
||||
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777).
|
||||
|
||||
// spRuntimeOp
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), comp.BindWritable).
|
||||
|
||||
// spTmpdirOp
|
||||
Bind(m("/tmp/hakurei.10/tmpdir/9999"), fhs.AbsTmp, comp.BindWritable).
|
||||
|
||||
// spAccountOp
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
|
||||
|
||||
// spWaylandOp
|
||||
Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1000/wayland-0"), 0).
|
||||
|
||||
// spPulseOp
|
||||
Bind(m("/run/user/1000/hakurei/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/pulse"), m("/run/user/1000/pulse/native"), 0).
|
||||
Place(m("/.hakurei/pulse-cookie"), bytes.Repeat([]byte{0}, pulseCookieSizeMax)).
|
||||
|
||||
// spDBusOp
|
||||
Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), m("/run/user/1000/bus"), 0).
|
||||
Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), m("/var/run/dbus/system_bus_socket"), 0).
|
||||
|
||||
// spFilesystemOp
|
||||
Etc(fhs.AbsEtc, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
|
||||
Tmpfs(fhs.AbsTmp, 0, 0755).
|
||||
Overlay(m("/nix/store"),
|
||||
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
|
||||
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
|
||||
Link(m("/run/current-system"), "/run/current-system", true).
|
||||
Link(m("/run/opengl-driver"), "/run/opengl-driver", true).
|
||||
Bind(fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
|
||||
m("/data/data/org.chromium.Chromium"),
|
||||
comp.BindWritable|comp.BindEnsure).
|
||||
Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"),
|
||||
comp.BindOptional|comp.BindWritable|comp.BindDevice).
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}
|
||||
|
||||
checkSimple(t, "shimEntrypoint", []simpleTestCase{
|
||||
{"success", func(k *kstub) error { shimEntrypoint(k); return nil }, stub.Expect{Calls: []stub.Call{
|
||||
call("getMsg", stub.ExpectArgs{}, nil, nil),
|
||||
call("getLogger", stub.ExpectArgs{}, (*log.Logger)(nil), nil),
|
||||
call("setDumpable", stub.ExpectArgs{uintptr(container.SUID_DUMP_DISABLE)}, nil, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{
|
||||
Shim: &shimParams{PrivPID: 0xbad, WaitDelay: 0xf, Verbose: true, Ops: []outcomeOp{
|
||||
&spParamsOp{"xterm-256color", true},
|
||||
&spRuntimeOp{sessionTypeWayland},
|
||||
spTmpdirOp{},
|
||||
spAccountOp{},
|
||||
&spWaylandOp{},
|
||||
&spPulseOp{(*[pulseCookieSizeMax]byte)(bytes.Repeat([]byte{0}, pulseCookieSizeMax)), pulseCookieSizeMax},
|
||||
&spDBusOp{true},
|
||||
&spFilesystemOp{},
|
||||
}},
|
||||
|
||||
ID: &checkExpectInstanceId,
|
||||
Identity: hst.IdentityMax,
|
||||
UserID: 10,
|
||||
Container: hst.Template().Container,
|
||||
Mapuid: 1000,
|
||||
Mapgid: 100,
|
||||
Paths: &env.Paths{TempDir: fhs.AbsTmp, RuntimePath: fhs.AbsRunUser.Append("1000")},
|
||||
}, nil}, nil, nil),
|
||||
call("swapVerbose", stub.ExpectArgs{true}, false, nil),
|
||||
call("verbosef", stub.ExpectArgs{"process share directory at %q, runtime directory at %q", []any{m("/tmp/hakurei.10"), m("/run/user/1000/hakurei")}}, nil, nil),
|
||||
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
|
||||
call("prctl", stub.ExpectArgs{uintptr(syscall.PR_SET_PDEATHSIG), uintptr(syscall.SIGCONT), uintptr(0)}, nil, nil),
|
||||
call("New", stub.ExpectArgs{}, nil, nil),
|
||||
call("closeReceive", stub.ExpectArgs{}, nil, nil),
|
||||
call("notifyContext", stub.ExpectArgs{context.Background(), []os.Signal{os.Interrupt, syscall.SIGTERM}}, nil, nil),
|
||||
call("containerStart", stub.ExpectArgs{templateParams}, nil, nil),
|
||||
call("containerServe", stub.ExpectArgs{templateParams}, nil, nil),
|
||||
call("seccompLoad", stub.ExpectArgs{shimPreset, seccomp.AllowMultiarch}, nil, nil),
|
||||
call("containerWait", stub.ExpectArgs{templateParams}, nil, nil),
|
||||
|
||||
// deferred
|
||||
call("wKeepAlive", stub.ExpectArgs{}, nil, nil),
|
||||
}, Tracks: []stub.Expect{{Calls: []stub.Call{
|
||||
call("rcRead", stub.ExpectArgs{}, []byte{2}, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"sa_sigaction got invalid siginfo"}}, nil, nil),
|
||||
call("rcRead", stub.ExpectArgs{}, []byte{3}, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"got SIGCONT from unexpected process"}}, nil, nil),
|
||||
call("rcRead", stub.ExpectArgs{}, nil, nil), // stub terminates this goroutine
|
||||
}}}}, nil},
|
||||
})
|
||||
}
|
||||
56
internal/outcome/spaccount.go
Normal file
56
internal/outcome/spaccount.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/internal/validate"
|
||||
)
|
||||
|
||||
func init() { gob.Register(spAccountOp{}) }
|
||||
|
||||
// spAccountOp sets up user account emulation inside the container.
|
||||
type spAccountOp struct{}
|
||||
|
||||
func (s spAccountOp) toSystem(state *outcomeStateSys) error {
|
||||
// do checks here to fail before fork/exec
|
||||
if state.Container == nil || state.Container.Home == nil || state.Container.Shell == nil {
|
||||
// unreachable
|
||||
return syscall.ENOTRECOVERABLE
|
||||
}
|
||||
|
||||
// default is applied in toContainer
|
||||
if state.Container.Username != "" && !validate.IsValidUsername(state.Container.Username) {
|
||||
return newWithMessage(fmt.Sprintf("invalid user name %q", state.Container.Username))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spAccountOp) toContainer(state *outcomeStateParams) error {
|
||||
const fallbackUsername = "chronos"
|
||||
|
||||
username := state.Container.Username
|
||||
if username == "" {
|
||||
username = fallbackUsername
|
||||
}
|
||||
|
||||
state.params.Dir = state.Container.Home
|
||||
state.env["HOME"] = state.Container.Home.String()
|
||||
state.env["USER"] = username
|
||||
state.env["SHELL"] = state.Container.Shell.String()
|
||||
|
||||
state.params.
|
||||
Place(fhs.AbsEtc.Append("passwd"),
|
||||
[]byte(username+":x:"+
|
||||
state.mapuid.String()+":"+
|
||||
state.mapgid.String()+
|
||||
":Hakurei:"+
|
||||
state.Container.Home.String()+":"+
|
||||
state.Container.Shell.String()+"\n")).
|
||||
Place(fhs.AbsEtc.Append("group"),
|
||||
[]byte("hakurei:x:"+state.mapgid.String()+":\n"))
|
||||
|
||||
return nil
|
||||
}
|
||||
72
internal/outcome/spaccount_test.go
Normal file
72
internal/outcome/spaccount_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func TestSpAccountOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := hst.Template()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"invalid state", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.Container.Shell = nil
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
// this op performs basic validation and does not make calls during toSystem
|
||||
}, nil, nil, syscall.ENOTRECOVERABLE, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid user name", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.Container.Username = "9"
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
// this op performs basic validation and does not make calls during toSystem
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
Err: os.ErrInvalid,
|
||||
Msg: `invalid user name "9"`,
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"success fallback username", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.Container.Username = ""
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
// this op performs basic validation and does not make calls during toSystem
|
||||
}, newI(), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Dir: config.Container.Home,
|
||||
Ops: new(container.Ops).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"HOME": config.Container.Home.String(),
|
||||
"USER": config.Container.Username,
|
||||
"SHELL": config.Container.Shell.String(),
|
||||
}, nil), nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp { return spAccountOp{} }, hst.Template, nil, []stub.Call{
|
||||
// this op performs basic validation and does not make calls during toSystem
|
||||
}, newI(), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Dir: config.Container.Home,
|
||||
Ops: new(container.Ops).
|
||||
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
|
||||
Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"HOME": config.Container.Home.String(),
|
||||
"USER": config.Container.Username,
|
||||
"SHELL": config.Container.Shell.String(),
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
391
internal/outcome/spcontainer.go
Normal file
391
internal/outcome/spcontainer.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/validate"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
const varRunNscd = fhs.Var + "run/nscd"
|
||||
|
||||
func init() { gob.Register(new(spParamsOp)) }
|
||||
|
||||
// spParamsOp initialises unordered fields of [container.Params] and the optional root filesystem.
|
||||
// This outcomeOp is hardcoded to always run first.
|
||||
type spParamsOp struct {
|
||||
// Value of $TERM, stored during toSystem.
|
||||
Term string
|
||||
// Whether $TERM is set, stored during toSystem.
|
||||
TermSet bool
|
||||
}
|
||||
|
||||
func (s *spParamsOp) toSystem(state *outcomeStateSys) error {
|
||||
s.Term, s.TermSet = state.k.lookupEnv("TERM")
|
||||
state.sys.Ensure(state.sc.SharePath, 0711)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spParamsOp) toContainer(state *outcomeStateParams) error {
|
||||
// pass $TERM for proper terminal I/O in initial process
|
||||
if s.TermSet {
|
||||
state.env["TERM"] = s.Term
|
||||
}
|
||||
|
||||
// in practice there should be less than 30 system mount points
|
||||
const preallocateOpsCount = 1 << 5
|
||||
|
||||
state.params.Hostname = state.Container.Hostname
|
||||
state.params.RetainSession = state.Container.Flags&hst.FTty != 0
|
||||
state.params.HostNet = state.Container.Flags&hst.FHostNet != 0
|
||||
state.params.HostAbstract = state.Container.Flags&hst.FHostAbstract != 0
|
||||
|
||||
if state.Container.Path == nil {
|
||||
return newWithMessage("invalid program path")
|
||||
}
|
||||
state.params.Path = state.Container.Path
|
||||
|
||||
if len(state.Container.Args) == 0 {
|
||||
state.params.Args = []string{state.Container.Path.String()}
|
||||
} else {
|
||||
state.params.Args = state.Container.Args
|
||||
}
|
||||
|
||||
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
|
||||
// this behaviour is implemented in the shim
|
||||
state.params.ForwardCancel = state.Shim.WaitDelay > 0
|
||||
|
||||
if state.Container.Flags&hst.FMultiarch != 0 {
|
||||
state.params.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
|
||||
if state.Container.Flags&hst.FSeccompCompat == 0 {
|
||||
state.params.SeccompPresets |= comp.PresetExt
|
||||
}
|
||||
if state.Container.Flags&hst.FDevel == 0 {
|
||||
state.params.SeccompPresets |= comp.PresetDenyDevel
|
||||
}
|
||||
if state.Container.Flags&hst.FUserns == 0 {
|
||||
state.params.SeccompPresets |= comp.PresetDenyNS
|
||||
}
|
||||
if state.Container.Flags&hst.FTty == 0 {
|
||||
state.params.SeccompPresets |= comp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if state.Container.Flags&hst.FMapRealUID != 0 {
|
||||
state.params.Uid = state.Mapuid
|
||||
state.params.Gid = state.Mapgid
|
||||
}
|
||||
|
||||
{
|
||||
state.as.AutoEtcPrefix = state.id.String()
|
||||
ops := make(container.Ops, 0, preallocateOpsCount+len(state.Container.Filesystem))
|
||||
state.params.Ops = &ops
|
||||
state.as.Ops = opsAdapter{&ops}
|
||||
}
|
||||
|
||||
rootfs, filesystem, _ := resolveRoot(state.Container)
|
||||
state.filesystem = filesystem
|
||||
if rootfs != nil {
|
||||
rootfs.Apply(&state.as)
|
||||
}
|
||||
|
||||
// early mount points
|
||||
state.params.
|
||||
Proc(fhs.AbsProc).
|
||||
Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755)
|
||||
if state.Container.Flags&hst.FDevice == 0 {
|
||||
state.params.DevWritable(fhs.AbsDev, true)
|
||||
} else {
|
||||
state.params.Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice)
|
||||
}
|
||||
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
|
||||
state.params.Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(spFilesystemOp)) }
|
||||
|
||||
// spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem.
|
||||
// This outcomeOp is hardcoded to always run last.
|
||||
type spFilesystemOp struct {
|
||||
// Matched paths to cover. Stored during toSystem.
|
||||
HidePaths []*check.Absolute
|
||||
}
|
||||
|
||||
func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
|
||||
/* retrieve paths and hide them if they're made available in the sandbox;
|
||||
|
||||
this feature tries to improve user experience of permissive defaults, and
|
||||
to warn about issues in custom configuration; it is NOT a security feature
|
||||
and should not be treated as such, ALWAYS be careful with what you bind */
|
||||
hidePaths := []string{
|
||||
state.sc.RuntimePath.String(),
|
||||
state.sc.SharePath.String(),
|
||||
|
||||
// this causes emulated passwd database to be bypassed on some /etc/ setups
|
||||
varRunNscd,
|
||||
}
|
||||
|
||||
// dbus.Address does not go through syscallDispatcher
|
||||
systemBusAddr := dbus.FallbackSystemBusAddress
|
||||
if addr, ok := state.k.lookupEnv(dbus.SystemBusAddress); ok {
|
||||
systemBusAddr = addr
|
||||
}
|
||||
|
||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
||||
return &hst.AppError{Step: "parse dbus address", Err: err}
|
||||
} else {
|
||||
// there is usually only one, do not preallocate
|
||||
for _, entry := range entries {
|
||||
if entry.Method != "unix" {
|
||||
continue
|
||||
}
|
||||
for _, pair := range entry.Values {
|
||||
if pair[0] == "path" {
|
||||
if path.IsAbs(pair[1]) {
|
||||
// get parent dir of socket
|
||||
dir := path.Dir(pair[1])
|
||||
if dir == "." || dir == fhs.Root {
|
||||
state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
|
||||
}
|
||||
hidePaths = append(hidePaths, dir)
|
||||
} else {
|
||||
state.msg.Verbosef("dbus socket %q is not absolute", pair[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hidePathMatch := make([]bool, len(hidePaths))
|
||||
for i := range hidePaths {
|
||||
if err := evalSymlinks(state.msg, state.k, &hidePaths[i]); err != nil {
|
||||
return &hst.AppError{Step: "evaluate path hiding target", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
_, filesystem, autoroot := resolveRoot(state.Container)
|
||||
|
||||
var hidePathSourceCount int
|
||||
for i, c := range filesystem {
|
||||
if !c.Valid() {
|
||||
return newWithMessage("invalid filesystem at index " + strconv.Itoa(i))
|
||||
}
|
||||
|
||||
// fs counter
|
||||
hidePathSourceCount += len(c.Host())
|
||||
}
|
||||
|
||||
// AutoRootOp is a collection of many BindMountOp internally
|
||||
var autoRootEntries []fs.DirEntry
|
||||
if autoroot != nil {
|
||||
if d, err := state.k.readdir(autoroot.Source.String()); err != nil {
|
||||
return &hst.AppError{Step: "access autoroot source", Err: err}
|
||||
} else {
|
||||
// autoroot counter
|
||||
hidePathSourceCount += len(d)
|
||||
autoRootEntries = d
|
||||
}
|
||||
}
|
||||
|
||||
hidePathSource := make([]*check.Absolute, 0, hidePathSourceCount)
|
||||
|
||||
// fs append
|
||||
for _, c := range filesystem {
|
||||
// all entries already checked above
|
||||
hidePathSource = append(hidePathSource, c.Host()...)
|
||||
}
|
||||
|
||||
// autoroot append
|
||||
if autoroot != nil {
|
||||
for _, ent := range autoRootEntries {
|
||||
name := ent.Name()
|
||||
if container.IsAutoRootBindable(state.msg, name) {
|
||||
hidePathSource = append(hidePathSource, autoroot.Source.Append(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evaluated path, input path
|
||||
hidePathSourceEval := make([][2]string, len(hidePathSource))
|
||||
for i, a := range hidePathSource {
|
||||
if a == nil {
|
||||
// unreachable
|
||||
return newWithMessage("impossible path hiding state reached")
|
||||
}
|
||||
|
||||
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
|
||||
if err := evalSymlinks(state.msg, state.k, &hidePathSourceEval[i][0]); err != nil {
|
||||
return &hst.AppError{Step: "evaluate path hiding source", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range hidePathSourceEval {
|
||||
for i := range hidePaths {
|
||||
// skip matched entries
|
||||
if hidePathMatch[i] {
|
||||
continue
|
||||
}
|
||||
|
||||
if ok, err := validate.DeepContainsH(p[0], hidePaths[i]); err != nil {
|
||||
return &hst.AppError{Step: "determine path hiding outcome", Err: err}
|
||||
} else if ok {
|
||||
hidePathMatch[i] = true
|
||||
state.msg.Verbosef("hiding path %q from %q", hidePaths[i], p[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copy matched paths for shim
|
||||
for i, ok := range hidePathMatch {
|
||||
if ok {
|
||||
if a, err := check.NewAbs(hidePaths[i]); err != nil {
|
||||
return newWithMessage("invalid path hiding candidate " + strconv.Quote(hidePaths[i]))
|
||||
} else {
|
||||
s.HidePaths = append(s.HidePaths, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append ExtraPerms last
|
||||
flattenExtraPerms(state.sys, state.extraPerms)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
|
||||
for i, c := range state.filesystem {
|
||||
if !c.Valid() {
|
||||
return newWithMessage("invalid filesystem at index " + strconv.Itoa(i))
|
||||
}
|
||||
c.Apply(&state.as)
|
||||
}
|
||||
|
||||
for _, a := range s.HidePaths {
|
||||
state.params.Tmpfs(a, 1<<13, 0755)
|
||||
}
|
||||
|
||||
// no more configured paths beyond this point
|
||||
if state.Container.Flags&hst.FDevice == 0 {
|
||||
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
|
||||
}
|
||||
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
|
||||
|
||||
state.params.Env = make([]string, 0, len(state.env))
|
||||
for key, value := range state.env {
|
||||
// key validated early via hst
|
||||
state.params.Env = append(state.params.Env, key+"="+value)
|
||||
}
|
||||
slices.Sort(state.params.Env)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRoot handles the root filesystem special case for [hst.FilesystemConfig] and additionally resolves autoroot
|
||||
// as it requires special handling during path hiding.
|
||||
func resolveRoot(c *hst.ContainerConfig) (rootfs hst.FilesystemConfig, filesystem []hst.FilesystemConfigJSON, autoroot *hst.FSBind) {
|
||||
// root filesystem special case
|
||||
filesystem = c.Filesystem
|
||||
// valid happens late, so root gets it here
|
||||
if len(filesystem) > 0 && filesystem[0].Valid() && filesystem[0].Path().String() == fhs.Root {
|
||||
// if the first element targets /, it is inserted early and excluded from path hiding
|
||||
rootfs = filesystem[0].FilesystemConfig
|
||||
filesystem = filesystem[1:]
|
||||
|
||||
// autoroot requires special handling during path hiding
|
||||
if b, ok := rootfs.(*hst.FSBind); ok && b.IsAutoRoot() {
|
||||
autoroot = b
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
|
||||
func evalSymlinks(msg message.Msg, k syscallDispatcher, v *string) error {
|
||||
if p, err := k.evalSymlinks(*v); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
msg.Verbosef("path %q does not yet exist", *v)
|
||||
} else {
|
||||
*v = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flattenExtraPerms expands a slice of [hst.ExtraPermConfig] into [system.I].
|
||||
func flattenExtraPerms(sys *system.I, extraPerms []hst.ExtraPermConfig) {
|
||||
for i := range extraPerms {
|
||||
p := &extraPerms[i]
|
||||
if p.Path == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Ensure {
|
||||
sys.Ensure(p.Path, 0700)
|
||||
}
|
||||
|
||||
perms := make(acl.Perms, 0, 3)
|
||||
if p.Read {
|
||||
perms = append(perms, acl.Read)
|
||||
}
|
||||
if p.Write {
|
||||
perms = append(perms, acl.Write)
|
||||
}
|
||||
if p.Execute {
|
||||
perms = append(perms, acl.Execute)
|
||||
}
|
||||
sys.UpdatePermType(system.User, p.Path, perms...)
|
||||
}
|
||||
}
|
||||
|
||||
// opsAdapter implements [hst.Ops] on [container.Ops].
|
||||
type opsAdapter struct{ *container.Ops }
|
||||
|
||||
func (p opsAdapter) Tmpfs(target *check.Absolute, size int, perm os.FileMode) hst.Ops {
|
||||
return opsAdapter{p.Ops.Tmpfs(target, size, perm)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Readonly(target *check.Absolute, perm os.FileMode) hst.Ops {
|
||||
return opsAdapter{p.Ops.Readonly(target, perm)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Bind(source, target *check.Absolute, flags int) hst.Ops {
|
||||
return opsAdapter{p.Ops.Bind(source, target, flags)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) hst.Ops {
|
||||
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
|
||||
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Link(target *check.Absolute, linkName string, dereference bool) hst.Ops {
|
||||
return opsAdapter{p.Ops.Link(target, linkName, dereference)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Root(host *check.Absolute, flags int) hst.Ops {
|
||||
return opsAdapter{p.Ops.Root(host, flags)}
|
||||
}
|
||||
|
||||
func (p opsAdapter) Etc(host *check.Absolute, prefix string) hst.Ops {
|
||||
return opsAdapter{p.Ops.Etc(host, prefix)}
|
||||
}
|
||||
477
internal/outcome/spcontainer_test.go
Normal file
477
internal/outcome/spcontainer_test.go
Normal file
@@ -0,0 +1,477 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestSpParamsOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := hst.Template()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"invalid program path", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spParamsOp)
|
||||
}
|
||||
return &spParamsOp{Term: "xterm", TermSet: true}
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.Container.Path = nil
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
|
||||
}, newI().
|
||||
Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
Err: os.ErrInvalid,
|
||||
Msg: "invalid program path",
|
||||
}},
|
||||
|
||||
{"success defaultargs secure", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spParamsOp)
|
||||
}
|
||||
return &spParamsOp{Term: "xterm", TermSet: true}
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.Container.Args = nil
|
||||
c.Container.Flags = hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
|
||||
}, newI().
|
||||
Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Hostname: config.Container.Hostname,
|
||||
HostNet: true,
|
||||
HostAbstract: true,
|
||||
Path: config.Container.Path,
|
||||
Args: []string{config.Container.Path.String()},
|
||||
SeccompPresets: comp.PresetExt | comp.PresetDenyDevel | comp.PresetDenyNS | comp.PresetDenyTTY,
|
||||
Uid: 1000,
|
||||
Gid: 100,
|
||||
Ops: new(container.Ops).
|
||||
Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
|
||||
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
DevWritable(fhs.AbsDev, true).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"TERM": "xterm",
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
|
||||
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
|
||||
}
|
||||
|
||||
wantFilesystems := config.Container.Filesystem[1:]
|
||||
if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
|
||||
t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
|
||||
}
|
||||
}), nil},
|
||||
|
||||
{"success", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spParamsOp)
|
||||
}
|
||||
return &spParamsOp{Term: "xterm", TermSet: true}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil),
|
||||
}, newI().
|
||||
Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Hostname: config.Container.Hostname,
|
||||
RetainSession: true,
|
||||
HostNet: true,
|
||||
HostAbstract: true,
|
||||
Path: config.Container.Path,
|
||||
Args: config.Container.Args,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Uid: 1000,
|
||||
Gid: 100,
|
||||
Ops: new(container.Ops).
|
||||
Root(m("/var/lib/hakurei/base/org.debian"), comp.BindWritable).
|
||||
Proc(fhs.AbsProc).Tmpfs(hst.AbsPrivateTmp, 1<<12, 0755).
|
||||
Bind(fhs.AbsDev, fhs.AbsDev, comp.BindWritable|comp.BindDevice).
|
||||
Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"TERM": "xterm",
|
||||
}, func(t *testing.T, state *outcomeStateParams) {
|
||||
if state.as.AutoEtcPrefix != wantAutoEtcPrefix {
|
||||
t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix)
|
||||
}
|
||||
|
||||
wantFilesystems := config.Container.Filesystem[1:]
|
||||
if !reflect.DeepEqual(state.filesystem, wantFilesystems) {
|
||||
t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems)
|
||||
}
|
||||
}), nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpFilesystemOp(t *testing.T) {
|
||||
const nePrefix = container.Nonexistent + "/eval"
|
||||
var stubDebianRoot = stubDir("bin", "dev", "etc", "home", "lib64", "lost+found",
|
||||
"mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var")
|
||||
config := hst.Template()
|
||||
|
||||
newConfigSmall := func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.Container.Filesystem = []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: fhs.AbsEtc, Source: fhs.AbsEtc, Special: true}},
|
||||
{FilesystemConfig: &hst.FSOverlay{Target: m("/nix/store"), Lower: []*check.Absolute{
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/.ro-store"),
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium"),
|
||||
}}},
|
||||
{FilesystemConfig: &hst.FSEphemeral{Target: hst.AbsPrivateTmp}},
|
||||
}
|
||||
c.Container.Flags &= ^hst.FDevice
|
||||
return c
|
||||
}
|
||||
configSmall := newConfigSmall()
|
||||
|
||||
needsApplyState := func(next pStateContainerFunc) pStateContainerFunc {
|
||||
return func(state *outcomeStateParams) {
|
||||
state.as = hst.ApplyState{AutoEtcPrefix: wantAutoEtcPrefix, Ops: opsAdapter{state.params.Ops}}
|
||||
|
||||
if next != nil {
|
||||
next(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"readdir", func(bool, bool) outcomeOp {
|
||||
return new(spFilesystemOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/dbus"}, nePrefix+"/run/dbus", nil),
|
||||
call("readdir", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian"}, []os.DirEntry{}, stub.UniqueError(2)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "access autoroot source",
|
||||
Err: stub.UniqueError(2),
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid dbus address", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
|
||||
c := newConfigSmall()
|
||||
c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(false)})
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid", nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "parse dbus address",
|
||||
Err: &dbus.BadAddressError{
|
||||
Type: dbus.ErrNoColon,
|
||||
EntryVal: []byte("invalid"),
|
||||
PairPos: -1,
|
||||
},
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid fs early", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
|
||||
c := newConfigSmall()
|
||||
c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(false)})
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
Err: os.ErrInvalid,
|
||||
Msg: "invalid filesystem at index 3",
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"evalSymlinks early", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, "", stub.UniqueError(0)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "evaluate path hiding target",
|
||||
Err: stub.UniqueError(0),
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"host nil abs", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, func() *hst.Config {
|
||||
c := newConfigSmall()
|
||||
c.Container.Filesystem = append(c.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: invalidFSHost(true)})
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
Err: os.ErrInvalid,
|
||||
Msg: "impossible path hiding state reached",
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"evalSymlinks late", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", stub.UniqueError(1)),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "evaluate path hiding source",
|
||||
Err: stub.UniqueError(1),
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid contains", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
|
||||
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "determine path hiding outcome",
|
||||
Err: errors.New("Rel: can't make /proc/nonexistent/eval/xdg_runtime_dir relative to var/lib/hakurei/base/org.nixos/org.chromium.Chromium"),
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid hide", func(bool, bool) outcomeOp { return new(spFilesystemOp) }, newConfigSmall, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, "xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, "tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "nscd", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, "nonexistent/dbus", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, "nonexistent", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, ".ro-store", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, "org.chromium.Chromium", nil),
|
||||
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"nonexistent/dbus", "/etc/"}}, nil, nil),
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
Err: os.ErrInvalid,
|
||||
Msg: `invalid path hiding candidate "nonexistent/dbus"`,
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid fs", func(isShim, clearUnexported bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spFilesystemOp)
|
||||
}
|
||||
return &spFilesystemOp{HidePaths: []*check.Absolute{m("/proc/nonexistent/eval/etc/dbus")}}
|
||||
}, newConfigSmall, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
|
||||
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
|
||||
}, newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
|
||||
state.filesystem = append(configSmall.Container.Filesystem, hst.FilesystemConfigJSON{})
|
||||
})), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, nil, nil, &hst.AppError{
|
||||
Step: "finalise",
|
||||
Err: os.ErrInvalid,
|
||||
Msg: "invalid filesystem at index 3",
|
||||
}},
|
||||
|
||||
{"success noroot nodev envdbus strangedbus dbusnotabs hide", func(isShim, clearUnexported bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spFilesystemOp)
|
||||
}
|
||||
return &spFilesystemOp{HidePaths: []*check.Absolute{m("/proc/nonexistent/eval/etc/dbus")}}
|
||||
}, newConfigSmall, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, "invalid:meow=0;unix:path=/system_bus_socket;unix:path=system_bus_socket", nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is in an unusual location", []any{"/system_bus_socket"}}, nil, nil),
|
||||
call("verbosef", stub.ExpectArgs{"dbus socket %q is not absolute", []any{"system_bus_socket"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/"}, nePrefix+"/etc/dbus", nil), // to match hidePaths
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/.ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/.ro-store", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil),
|
||||
call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil),
|
||||
}, newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
|
||||
state.filesystem = configSmall.Container.Filesystem
|
||||
})), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Env: []string{
|
||||
"GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
},
|
||||
|
||||
Ops: new(container.Ops).
|
||||
Etc(fhs.AbsEtc, wantAutoEtcPrefix).
|
||||
OverlayReadonly(
|
||||
check.MustAbs("/nix/store"),
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/.ro-store"),
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/org.chromium.Chromium")).
|
||||
Readonly(hst.AbsPrivateTmp, 0755).
|
||||
Tmpfs(m("/proc/nonexistent/eval/etc/dbus"), 1<<13, 0755).
|
||||
Remount(fhs.AbsDev, syscall.MS_RDONLY).
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}, nil, nil},
|
||||
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return new(spFilesystemOp)
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("lookupEnv", stub.ExpectArgs{dbus.SystemBusAddress}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/xdg_runtime_dir"}, nePrefix+"/xdg_runtime_dir", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{container.Nonexistent + "/tmp/hakurei.0"}, nePrefix+"/tmp/hakurei.0", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/nscd"}, "", &os.PathError{Op: "lstat", Path: "/var/run/nscd", Err: os.ErrNotExist}),
|
||||
call("verbosef", stub.ExpectArgs{"path %q does not yet exist", []any{"/var/run/nscd"}}, nil, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/run/dbus"}, nePrefix+"/run/dbus", nil),
|
||||
call("readdir", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian"}, stubDebianRoot, nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/etc/"}, nePrefix+"/etc", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"}, nePrefix+"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work"}, nePrefix+"/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/ro-store"}, nePrefix+"/var/lib/hakurei/base/org.nixos/ro-store", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/u0/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/u0/org.chromium.Chromium", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/dev/dri"}, nePrefix+"/dev/dri", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/bin"}, nePrefix+"/var/lib/hakurei/base/org.debian/bin", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/home"}, nePrefix+"/var/lib/hakurei/base/org.debian/home", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/lib64"}, nePrefix+"/var/lib/hakurei/base/org.debian/lib64", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/lost+found"}, nePrefix+"/var/lib/hakurei/base/org.debian/lost+found", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/nix"}, nePrefix+"/var/lib/hakurei/base/org.debian/nix", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/root"}, nePrefix+"/var/lib/hakurei/base/org.debian/root", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/run"}, nePrefix+"/var/lib/hakurei/base/org.debian/run", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/srv"}, nePrefix+"/var/lib/hakurei/base/org.debian/srv", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/sys"}, nePrefix+"/var/lib/hakurei/base/org.debian/sys", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/usr"}, nePrefix+"/var/lib/hakurei/base/org.debian/usr", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/var"}, nePrefix+"/var/lib/hakurei/base/org.debian/var", nil),
|
||||
}, newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) {
|
||||
state.filesystem = config.Container.Filesystem[1:]
|
||||
})), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Env: []string{
|
||||
"GOOGLE_API_KEY=AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT",
|
||||
},
|
||||
|
||||
Ops: new(container.Ops).
|
||||
Etc(fhs.AbsEtc, wantAutoEtcPrefix).
|
||||
Tmpfs(fhs.AbsTmp, 0, 0755).
|
||||
Overlay(
|
||||
check.MustAbs("/nix/store"),
|
||||
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
|
||||
fhs.AbsVarLib.Append("hakurei/nix/u0/org.chromium.Chromium/rw-store/work"),
|
||||
fhs.AbsVarLib.Append("hakurei/base/org.nixos/ro-store")).
|
||||
Link(fhs.AbsRun.Append("current-system"), "/run/current-system", true).
|
||||
Link(fhs.AbsRun.Append("opengl-driver"), "/run/opengl-driver", true).
|
||||
Bind(
|
||||
fhs.AbsVarLib.Append("hakurei/u0/org.chromium.Chromium"),
|
||||
check.MustAbs("/data/data/org.chromium.Chromium"),
|
||||
comp.BindWritable|comp.BindEnsure).
|
||||
Bind(fhs.AbsDev.Append("dri"), fhs.AbsDev.Append("dri"), comp.BindDevice|comp.BindWritable|comp.BindOptional).
|
||||
Remount(fhs.AbsRoot, syscall.MS_RDONLY),
|
||||
}, nil, nil},
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlattenExtraPerms(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
perms []hst.ExtraPermConfig
|
||||
want *system.I
|
||||
}{
|
||||
{"path nil check", append(hst.Template().ExtraPerms, hst.ExtraPermConfig{}), newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute)},
|
||||
|
||||
{"template", hst.Template().ExtraPerms, newI().
|
||||
Ensure(m("/var/lib/hakurei/u0"), 0700).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0"),
|
||||
acl.Execute).
|
||||
UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"),
|
||||
acl.Read, acl.Write, acl.Execute)},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := newI()
|
||||
flattenExtraPerms(got, tc.perms)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("flattenExtraPerms: sys = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// invalidFSHost implements the Host method of [hst.FilesystemConfig] with an invalid response.
|
||||
type invalidFSHost bool
|
||||
|
||||
func (f invalidFSHost) Valid() bool { return bool(f) }
|
||||
func (invalidFSHost) Path() *check.Absolute { panic("unreachable") }
|
||||
func (invalidFSHost) Host() []*check.Absolute { return []*check.Absolute{nil} }
|
||||
func (invalidFSHost) Apply(*hst.ApplyState) { panic("unreachable") }
|
||||
func (invalidFSHost) String() string { panic("unreachable") }
|
||||
58
internal/outcome/spdbus.go
Normal file
58
internal/outcome/spdbus.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spDBusOp)) }
|
||||
|
||||
// spDBusOp maintains an xdg-dbus-proxy instance for the container.
|
||||
// Runs after spRuntimeOp.
|
||||
type spDBusOp struct {
|
||||
// Whether to bind the system bus socket. Populated during toSystem.
|
||||
ProxySystem bool
|
||||
}
|
||||
|
||||
func (s *spDBusOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EDBus == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
if state.sessionBus == nil {
|
||||
state.sessionBus = dbus.NewConfig(state.appId, true, true)
|
||||
}
|
||||
|
||||
// downstream socket paths
|
||||
sessionPath, systemPath := state.instance().Append("bus"), state.instance().Append("system_bus_socket")
|
||||
|
||||
var sessionBus, systemBus dbus.ProxyPair
|
||||
sessionBus[0], systemBus[0] = state.k.dbusAddress()
|
||||
sessionBus[1], systemBus[1] = sessionPath.String(), systemPath.String()
|
||||
if err := state.sys.ProxyDBus(state.sessionBus, state.systemBus, sessionBus, systemBus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
|
||||
if state.systemBus != nil {
|
||||
s.ProxySystem = true
|
||||
state.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spDBusOp) toContainer(state *outcomeStateParams) error {
|
||||
sessionInner := state.runtimeDir.Append("bus")
|
||||
state.env["DBUS_SESSION_BUS_ADDRESS"] = "unix:path=" + sessionInner.String()
|
||||
state.params.Bind(state.instancePath().Append("bus"), sessionInner, 0)
|
||||
if s.ProxySystem {
|
||||
systemInner := fhs.AbsVar.Append("run/dbus/system_bus_socket")
|
||||
state.env["DBUS_SYSTEM_BUS_ADDRESS"] = "unix:path=" + systemInner.String()
|
||||
state.params.Bind(state.instancePath().Append("system_bus_socket"), systemInner, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
190
internal/outcome/spdbus_test.go
Normal file
190
internal/outcome/spdbus_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/helper"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func TestSpDBusOp(t *testing.T) {
|
||||
config := hst.Template()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"not enabled", func(bool, bool) outcomeOp {
|
||||
return new(spDBusOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, nil, nil, nil, errNotEnabled, nil, nil, nil, nil, nil},
|
||||
|
||||
{"invalid", func(bool, bool) outcomeOp {
|
||||
return new(spDBusOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.SessionBus.Talk[0] += "\x00"
|
||||
c.SystemBus = nil
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("dbusAddress", stub.ExpectArgs{}, [2]string{
|
||||
"unix:path=/run/user/1000/bus",
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
}, nil),
|
||||
}, nil, sysUsesInstance(nil), &system.OpError{
|
||||
Op: "dbus",
|
||||
Err: syscall.EINVAL,
|
||||
Msg: "message bus proxy configuration contains NUL byte",
|
||||
Revert: false,
|
||||
}, nil, nil, nil, nil, nil},
|
||||
|
||||
{"success default", func(bool, bool) outcomeOp {
|
||||
return new(spDBusOp)
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
c.SessionBus, c.SystemBus = nil, nil
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
call("dbusAddress", stub.ExpectArgs{}, [2]string{
|
||||
"unix:path=/run/user/1000/bus",
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
}, nil),
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
|
||||
"unix:path=/run/user/1000/bus",
|
||||
wantInstancePrefix + "/bus",
|
||||
"--filter",
|
||||
"--talk=org.freedesktop.DBus",
|
||||
"--talk=org.freedesktop.Notifications",
|
||||
"--own=org.chromium.Chromium.*",
|
||||
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"--call=org.freedesktop.portal.*=*",
|
||||
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
|
||||
}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
|
||||
"unix:path=/run/user/1000/bus",
|
||||
wantInstancePrefix+"/bus",
|
||||
"--filter",
|
||||
"--talk=org.freedesktop.DBus",
|
||||
"--talk=org.freedesktop.Notifications",
|
||||
"--own=org.chromium.Chromium.*",
|
||||
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"--call=org.freedesktop.portal.*=*",
|
||||
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
|
||||
)}}, nil, nil),
|
||||
}, func() *system.I {
|
||||
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
|
||||
sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
|
||||
if err := sys.ProxyDBus(
|
||||
dbus.NewConfig(config.ID, true, true), nil,
|
||||
dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"},
|
||||
dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", wantInstancePrefix + "/system_bus_socket"},
|
||||
); err != nil {
|
||||
t.Fatalf("cannot prepare sys: %v", err)
|
||||
}
|
||||
sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write)
|
||||
return sys
|
||||
}(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Bind(m(wantInstancePrefix+"/bus"),
|
||||
m("/run/user/1000/bus"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
|
||||
}, nil), nil},
|
||||
|
||||
{"success", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spDBusOp)
|
||||
}
|
||||
return &spDBusOp{ProxySystem: true}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
call("dbusAddress", stub.ExpectArgs{}, [2]string{
|
||||
"unix:path=/run/user/1000/bus",
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
}, nil),
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{
|
||||
"unix:path=/run/user/1000/bus",
|
||||
wantInstancePrefix + "/bus",
|
||||
"--filter",
|
||||
"--talk=org.freedesktop.Notifications",
|
||||
"--talk=org.freedesktop.FileManager1",
|
||||
"--talk=org.freedesktop.ScreenSaver",
|
||||
"--talk=org.freedesktop.secrets",
|
||||
"--talk=org.kde.kwalletd5",
|
||||
"--talk=org.kde.kwalletd6",
|
||||
"--talk=org.gnome.SessionManager",
|
||||
"--own=org.chromium.Chromium.*",
|
||||
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"--own=org.mpris.MediaPlayer2.chromium.*",
|
||||
"--call=org.freedesktop.portal.*=*",
|
||||
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
|
||||
}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
wantInstancePrefix + "/system_bus_socket",
|
||||
"--filter",
|
||||
"--talk=org.bluez",
|
||||
"--talk=org.freedesktop.Avahi",
|
||||
"--talk=org.freedesktop.UPower",
|
||||
}}}, nil, nil),
|
||||
call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs(
|
||||
"unix:path=/run/user/1000/bus",
|
||||
wantInstancePrefix+"/bus",
|
||||
"--filter",
|
||||
"--talk=org.freedesktop.Notifications",
|
||||
"--talk=org.freedesktop.FileManager1",
|
||||
"--talk=org.freedesktop.ScreenSaver",
|
||||
"--talk=org.freedesktop.secrets",
|
||||
"--talk=org.kde.kwalletd5",
|
||||
"--talk=org.kde.kwalletd6",
|
||||
"--talk=org.gnome.SessionManager",
|
||||
"--own=org.chromium.Chromium.*",
|
||||
"--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"--own=org.mpris.MediaPlayer2.chromium.*",
|
||||
"--call=org.freedesktop.portal.*=*",
|
||||
"--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*",
|
||||
|
||||
"unix:path=/var/run/dbus/system_bus_socket",
|
||||
wantInstancePrefix+"/system_bus_socket",
|
||||
"--filter",
|
||||
"--talk=org.bluez",
|
||||
"--talk=org.freedesktop.Avahi",
|
||||
"--talk=org.freedesktop.UPower",
|
||||
)}}, nil, nil),
|
||||
}, func() *system.I {
|
||||
sys := system.New(panicMsgContext{}, message.NewMsg(nil), checkExpectUid)
|
||||
sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711)
|
||||
if err := sys.ProxyDBus(
|
||||
config.SessionBus, config.SystemBus,
|
||||
dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"},
|
||||
dbus.ProxyPair{"unix:path=/var/run/dbus/system_bus_socket", wantInstancePrefix + "/system_bus_socket"},
|
||||
); err != nil {
|
||||
t.Fatalf("cannot prepare sys: %v", err)
|
||||
}
|
||||
sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write).
|
||||
UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read, acl.Write)
|
||||
return sys
|
||||
}(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Bind(m(wantInstancePrefix+"/bus"),
|
||||
m("/run/user/1000/bus"), 0).
|
||||
Bind(m(wantInstancePrefix+"/system_bus_socket"),
|
||||
m("/var/run/dbus/system_bus_socket"), 0),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS": "unix:path=/var/run/dbus/system_bus_socket",
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
210
internal/outcome/sppulse.go
Normal file
210
internal/outcome/sppulse.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
const pulseCookieSizeMax = 1 << 8
|
||||
|
||||
func init() { gob.Register(new(spPulseOp)) }
|
||||
|
||||
// spPulseOp exports the PulseAudio server to the container.
|
||||
// Runs after spRuntimeOp.
|
||||
type spPulseOp struct {
|
||||
// PulseAudio cookie data, populated during toSystem if a cookie is present.
|
||||
Cookie *[pulseCookieSizeMax]byte
|
||||
// PulseAudio cookie size, populated during toSystem if a cookie is present.
|
||||
CookieSize int
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EPulse == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
pulseRuntimeDir, pulseSocket := s.commonPaths(state.outcomeState)
|
||||
|
||||
if _, err := state.k.stat(pulseRuntimeDir.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
|
||||
}
|
||||
return newWithMessageError(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir), err)
|
||||
}
|
||||
|
||||
if fi, err := state.k.stat(pulseSocket.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
|
||||
}
|
||||
return newWithMessageError(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir), err)
|
||||
} else {
|
||||
if m := fi.Mode(); m&0o006 != 0o006 {
|
||||
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
|
||||
}
|
||||
}
|
||||
|
||||
// pulse socket is world writable and its parent directory DAC permissions prevents access;
|
||||
// hard link to target-executable share directory to grant access
|
||||
state.sys.Link(pulseSocket, state.runtime().Append("pulse"))
|
||||
|
||||
// load up to pulseCookieSizeMax bytes of pulse cookie for transmission to shim
|
||||
if a, err := discoverPulseCookie(state.k); err != nil {
|
||||
return err
|
||||
} else if a != nil {
|
||||
s.Cookie = new([pulseCookieSizeMax]byte)
|
||||
if s.CookieSize, err = loadFile(state.msg, state.k, "PulseAudio cookie", a.String(), s.Cookie[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
state.msg.Verbose("cannot locate PulseAudio cookie (tried " +
|
||||
"$PULSE_COOKIE, " +
|
||||
"$XDG_CONFIG_HOME/pulse/cookie, " +
|
||||
"$HOME/.pulse-cookie)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toContainer(state *outcomeStateParams) error {
|
||||
innerPulseSocket := state.runtimeDir.Append("pulse", "native")
|
||||
state.params.Bind(state.runtimePath().Append("pulse"), innerPulseSocket, 0)
|
||||
state.env["PULSE_SERVER"] = "unix:" + innerPulseSocket.String()
|
||||
|
||||
if s.Cookie != nil {
|
||||
innerDst := hst.AbsPrivateTmp.Append("/pulse-cookie")
|
||||
|
||||
if s.CookieSize < 0 || s.CookieSize > pulseCookieSizeMax {
|
||||
return newWithMessage("unexpected PulseAudio cookie size")
|
||||
}
|
||||
state.env["PULSE_COOKIE"] = innerDst.String()
|
||||
state.params.Place(innerDst, s.Cookie[:s.CookieSize])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSocket *check.Absolute) {
|
||||
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
|
||||
pulseRuntimeDir = state.sc.RuntimePath.Append("pulse")
|
||||
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
||||
pulseSocket = pulseRuntimeDir.Append("native")
|
||||
return
|
||||
}
|
||||
|
||||
// discoverPulseCookie attempts to discover the pathname of the PulseAudio cookie of the current user.
|
||||
// If both returned pathname and error are nil, the cookie is likely unavailable and can be silently skipped.
|
||||
func discoverPulseCookie(k syscallDispatcher) (*check.Absolute, error) {
|
||||
const paLocateStep = "locate PulseAudio cookie"
|
||||
|
||||
// from environment
|
||||
if p, ok := k.lookupEnv("PULSE_COOKIE"); ok {
|
||||
if a, err := check.NewAbs(p); err != nil {
|
||||
return nil, &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
// this takes precedence, do not verify whether the file is accessible
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
|
||||
// $HOME/.pulse-cookie
|
||||
if p, ok := k.lookupEnv("HOME"); ok {
|
||||
var pulseCookiePath *check.Absolute
|
||||
if a, err := check.NewAbs(p); err != nil {
|
||||
return nil, &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
pulseCookiePath = a.Append(".pulse-cookie")
|
||||
}
|
||||
|
||||
if fi, err := k.stat(pulseCookiePath.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if fi.IsDir() {
|
||||
// fallthrough
|
||||
} else {
|
||||
return pulseCookiePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/pulse/cookie
|
||||
if p, ok := k.lookupEnv("XDG_CONFIG_HOME"); ok {
|
||||
var pulseCookiePath *check.Absolute
|
||||
if a, err := check.NewAbs(p); err != nil {
|
||||
return nil, &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
pulseCookiePath = a.Append("pulse", "cookie")
|
||||
}
|
||||
|
||||
if fi, err := k.stat(pulseCookiePath.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if fi.IsDir() {
|
||||
// fallthrough
|
||||
} else {
|
||||
return pulseCookiePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// cookie not present
|
||||
// not fatal: authentication is disabled
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// loadFile reads up to len(buf) bytes from the file at pathname.
|
||||
func loadFile(
|
||||
msg message.Msg, k syscallDispatcher,
|
||||
description, pathname string, buf []byte,
|
||||
) (int, error) {
|
||||
n := len(buf)
|
||||
if n == 0 {
|
||||
return -1, errors.New("invalid buffer")
|
||||
}
|
||||
|
||||
if fi, err := k.stat(pathname); err != nil {
|
||||
return -1, &hst.AppError{Step: "access " + description, Err: err}
|
||||
} else {
|
||||
if fi.IsDir() {
|
||||
return -1, &hst.AppError{Step: "read " + description,
|
||||
Err: &os.PathError{Op: "stat", Path: pathname, Err: syscall.EISDIR}}
|
||||
}
|
||||
if s := fi.Size(); s > int64(n) {
|
||||
return -1, newWithMessageError(
|
||||
description+" at "+strconv.Quote(pathname)+" exceeds expected size",
|
||||
&os.PathError{Op: "stat", Path: pathname, Err: syscall.ENOMEM},
|
||||
)
|
||||
} else if s < int64(n) {
|
||||
msg.Verbosef("%s at %q is %d bytes shorter than expected", description, pathname, int64(n)-s)
|
||||
} else {
|
||||
msg.Verbosef("loading %d bytes from %q", n, pathname)
|
||||
}
|
||||
}
|
||||
|
||||
if f, err := k.open(pathname); err != nil {
|
||||
return -1, &hst.AppError{Step: "open " + description, Err: err}
|
||||
} else {
|
||||
if n, err = f.Read(buf); err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
_ = f.Close()
|
||||
return n, &hst.AppError{Step: "read " + description, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
if err = f.Close(); err != nil {
|
||||
return n, &hst.AppError{Step: "close " + description, Err: err}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
460
internal/outcome/sppulse_test.go
Normal file
460
internal/outcome/sppulse_test.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package outcome
|
||||
|
||||
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},
|
||||
})
|
||||
}
|
||||
125
internal/outcome/spruntime.go
Normal file
125
internal/outcome/spruntime.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
const (
|
||||
/*
|
||||
Path to a user-private user-writable directory that is bound
|
||||
to the user login time on the machine. It is automatically
|
||||
created the first time a user logs in and removed on the
|
||||
user's final logout. If a user logs in twice at the same time,
|
||||
both sessions will see the same $XDG_RUNTIME_DIR and the same
|
||||
contents. If a user logs in once, then logs out again, and
|
||||
logs in again, the directory contents will have been lost in
|
||||
between, but applications should not rely on this behavior and
|
||||
must be able to deal with stale files. To store
|
||||
session-private data in this directory, the user should
|
||||
include the value of $XDG_SESSION_ID in the filename. This
|
||||
directory shall be used for runtime file system objects such
|
||||
as AF_UNIX sockets, FIFOs, PID files and similar. It is
|
||||
guaranteed that this directory is local and offers the
|
||||
greatest possible file system feature set the operating system
|
||||
provides. For further details, see the XDG Base Directory
|
||||
Specification[3]. $XDG_RUNTIME_DIR is not set if the current
|
||||
user is not the original user of the session.
|
||||
*/
|
||||
envXDGRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
|
||||
/*
|
||||
The session class. This may be used instead of class= on the
|
||||
module parameter line, and is usually preferred.
|
||||
*/
|
||||
envXDGSessionClass = "XDG_SESSION_CLASS"
|
||||
|
||||
/*
|
||||
A regular interactive user session. This is the default class
|
||||
for sessions for which a TTY or X display is known at session
|
||||
registration time.
|
||||
*/
|
||||
xdgSessionClassUser = "user"
|
||||
|
||||
/*
|
||||
The session type. This may be used instead of type= on the
|
||||
module parameter line, and is usually preferred.
|
||||
|
||||
One of "unspecified", "tty", "x11", "wayland", "mir", or "web".
|
||||
*/
|
||||
envXDGSessionType = "XDG_SESSION_TYPE"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spRuntimeOp)) }
|
||||
|
||||
const (
|
||||
sessionTypeUnspec = iota
|
||||
sessionTypeTTY
|
||||
sessionTypeX11
|
||||
sessionTypeWayland
|
||||
)
|
||||
|
||||
// spRuntimeOp sets up XDG_RUNTIME_DIR inside the container.
|
||||
type spRuntimeOp struct {
|
||||
// SessionType determines the value of envXDGSessionType. Populated during toSystem.
|
||||
SessionType uintptr
|
||||
}
|
||||
|
||||
func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.Container.Flags&hst.FShareRuntime != 0 {
|
||||
runtimeDir, runtimeDirInst := s.commonPaths(state.outcomeState)
|
||||
state.sys.Ensure(runtimeDir, 0700)
|
||||
state.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
|
||||
state.sys.Ensure(runtimeDirInst, 0700)
|
||||
state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
|
||||
if state.et&hst.EWayland != 0 {
|
||||
s.SessionType = sessionTypeWayland
|
||||
} else if state.et&hst.EX11 != 0 {
|
||||
s.SessionType = sessionTypeX11
|
||||
} else {
|
||||
s.SessionType = sessionTypeTTY
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
|
||||
state.runtimeDir = fhs.AbsRunUser.Append(state.mapuid.String())
|
||||
state.env[envXDGRuntimeDir] = state.runtimeDir.String()
|
||||
state.env[envXDGSessionClass] = xdgSessionClassUser
|
||||
|
||||
switch s.SessionType {
|
||||
case sessionTypeUnspec:
|
||||
state.env[envXDGSessionType] = "unspecified"
|
||||
case sessionTypeTTY:
|
||||
state.env[envXDGSessionType] = "tty"
|
||||
case sessionTypeX11:
|
||||
state.env[envXDGSessionType] = "x11"
|
||||
case sessionTypeWayland:
|
||||
state.env[envXDGSessionType] = "wayland"
|
||||
|
||||
}
|
||||
|
||||
state.params.Tmpfs(fhs.AbsRunUser, 1<<12, 0755)
|
||||
if state.Container.Flags&hst.FShareRuntime != 0 {
|
||||
_, runtimeDirInst := s.commonPaths(state.outcomeState)
|
||||
state.params.Bind(runtimeDirInst, state.runtimeDir, comp.BindWritable)
|
||||
} else {
|
||||
state.params.Mkdir(state.runtimeDir, 0700)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spRuntimeOp) commonPaths(state *outcomeState) (runtimeDir, runtimeDirInst *check.Absolute) {
|
||||
runtimeDir = state.sc.SharePath.Append("runtime")
|
||||
runtimeDirInst = runtimeDir.Append(state.identity.String())
|
||||
return
|
||||
}
|
||||
128
internal/outcome/spruntime_test.go
Normal file
128
internal/outcome/spruntime_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
func TestSpRuntimeOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := hst.Template()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"success zero", func(isShim bool, clearUnexported bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spRuntimeOp)
|
||||
}
|
||||
op := &spRuntimeOp{sessionTypeTTY}
|
||||
if clearUnexported {
|
||||
op.SessionType = sessionTypeUnspec
|
||||
}
|
||||
return op
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
// this op configures the system state and does not make calls during toSystem
|
||||
}, newI().
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
"XDG_SESSION_CLASS": "user",
|
||||
"XDG_SESSION_TYPE": "unspecified",
|
||||
}, nil), nil},
|
||||
|
||||
{"success tty", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spRuntimeOp)
|
||||
}
|
||||
return &spRuntimeOp{sessionTypeTTY}
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = 0
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
// this op configures the system state and does not make calls during toSystem
|
||||
}, newI().
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
"XDG_SESSION_CLASS": "user",
|
||||
"XDG_SESSION_TYPE": "tty",
|
||||
}, nil), nil},
|
||||
|
||||
{"success x11", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spRuntimeOp)
|
||||
}
|
||||
return &spRuntimeOp{sessionTypeX11}
|
||||
}, func() *hst.Config {
|
||||
c := hst.Template()
|
||||
*c.Enablements = hst.Enablements(hst.EX11)
|
||||
return c
|
||||
}, nil, []stub.Call{
|
||||
// this op configures the system state and does not make calls during toSystem
|
||||
}, newI().
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
"XDG_SESSION_CLASS": "user",
|
||||
"XDG_SESSION_TYPE": "x11",
|
||||
}, nil), nil},
|
||||
|
||||
{"success", func(isShim, _ bool) outcomeOp {
|
||||
if !isShim {
|
||||
return new(spRuntimeOp)
|
||||
}
|
||||
return &spRuntimeOp{sessionTypeWayland}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
// this op configures the system state and does not make calls during toSystem
|
||||
}, newI().
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute).
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Tmpfs(fhs.AbsRunUser, 1<<12, 0755).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), comp.BindWritable),
|
||||
}, paramsWantEnv(config, map[string]string{
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
"XDG_SESSION_CLASS": "user",
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
}, nil), nil},
|
||||
})
|
||||
}
|
||||
44
internal/outcome/sptmpdir.go
Normal file
44
internal/outcome/sptmpdir.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
func init() { gob.Register(spTmpdirOp{}) }
|
||||
|
||||
// spTmpdirOp sets up TMPDIR inside the container.
|
||||
type spTmpdirOp struct{}
|
||||
|
||||
func (s spTmpdirOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.Container.Flags&hst.FShareTmpdir != 0 {
|
||||
tmpdir, tmpdirInst := s.commonPaths(state.outcomeState)
|
||||
state.sys.Ensure(tmpdir, 0700)
|
||||
state.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
|
||||
state.sys.Ensure(tmpdirInst, 01700)
|
||||
state.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spTmpdirOp) toContainer(state *outcomeStateParams) error {
|
||||
if state.Container.Flags&hst.FShareTmpdir != 0 {
|
||||
_, tmpdirInst := s.commonPaths(state.outcomeState)
|
||||
state.params.Bind(tmpdirInst, fhs.AbsTmp, comp.BindWritable)
|
||||
} else {
|
||||
state.params.Tmpfs(fhs.AbsTmp, 0, 01777)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spTmpdirOp) commonPaths(state *outcomeState) (tmpdir, tmpdirInst *check.Absolute) {
|
||||
tmpdir = state.sc.SharePath.Append("tmpdir")
|
||||
tmpdirInst = tmpdir.Append(state.identity.String())
|
||||
return
|
||||
}
|
||||
34
internal/outcome/sptmpdir_test.go
Normal file
34
internal/outcome/sptmpdir_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/comp"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/container/stub"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
func TestSpTmpdirOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"success", func(bool, bool) outcomeOp {
|
||||
return spTmpdirOp{}
|
||||
}, hst.Template, nil, []stub.Call{
|
||||
// this op configures the system state and does not make calls during toSystem
|
||||
}, newI().
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), 0700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), acl.Execute).
|
||||
Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), 01700).
|
||||
UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{
|
||||
// this op configures the container state and does not make calls during toContainer
|
||||
}, &container.Params{
|
||||
Ops: new(container.Ops).
|
||||
Bind(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), fhs.AbsTmp, comp.BindWritable),
|
||||
}, nil, nil},
|
||||
})
|
||||
}
|
||||
63
internal/outcome/spwayland.go
Normal file
63
internal/outcome/spwayland.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(spWaylandOp)) }
|
||||
|
||||
// spWaylandOp exports the Wayland display server to the container.
|
||||
// Runs after spRuntimeOp.
|
||||
type spWaylandOp struct {
|
||||
// Path to host wayland socket. Populated during toSystem if DirectWayland is true.
|
||||
SocketPath *check.Absolute
|
||||
}
|
||||
|
||||
func (s *spWaylandOp) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EWayland == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath *check.Absolute
|
||||
if name, ok := state.k.lookupEnv(wayland.WaylandDisplay); !ok {
|
||||
state.msg.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||
socketPath = state.sc.RuntimePath.Append(wayland.FallbackName)
|
||||
} else if a, err := check.NewAbs(name); err != nil {
|
||||
socketPath = state.sc.RuntimePath.Append(name)
|
||||
} else {
|
||||
socketPath = a
|
||||
}
|
||||
|
||||
if !state.directWayland { // set up security-context-v1
|
||||
appId := state.appId
|
||||
if appId == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appId = "app.hakurei." + state.id.String()
|
||||
}
|
||||
// downstream socket paths
|
||||
state.sys.Wayland(state.instance().Append("wayland"), socketPath, appId, state.id.String())
|
||||
} else { // bind mount wayland socket (insecure)
|
||||
state.msg.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||
state.ensureRuntimeDir()
|
||||
s.SocketPath = socketPath
|
||||
state.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spWaylandOp) toContainer(state *outcomeStateParams) error {
|
||||
innerPath := state.runtimeDir.Append(wayland.FallbackName)
|
||||
state.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
if s.SocketPath == nil {
|
||||
state.params.Bind(state.instancePath().Append("wayland"), innerPath, 0)
|
||||
} else {
|
||||
state.params.Bind(s.SocketPath, innerPath, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
104
internal/outcome/spwayland_test.go
Normal file
104
internal/outcome/spwayland_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package outcome
|
||||
|
||||
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},
|
||||
})
|
||||
}
|
||||
71
internal/outcome/spx11.go
Normal file
71
internal/outcome/spx11.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package outcome
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
var absX11SocketDir = fhs.AbsTmp.Append(".X11-unix")
|
||||
|
||||
func init() { gob.Register(new(spX11Op)) }
|
||||
|
||||
// spX11Op exports the X11 display server to the container.
|
||||
type spX11Op struct {
|
||||
// Value of $DISPLAY, stored during toSystem
|
||||
Display string
|
||||
}
|
||||
|
||||
func (s *spX11Op) toSystem(state *outcomeStateSys) error {
|
||||
if state.et&hst.EX11 == 0 {
|
||||
return errNotEnabled
|
||||
}
|
||||
|
||||
if d, ok := state.k.lookupEnv("DISPLAY"); !ok {
|
||||
return newWithMessage("DISPLAY is not set")
|
||||
} else {
|
||||
s.Display = d
|
||||
}
|
||||
|
||||
// the socket file at `/tmp/.X11-unix/X%d` is typically owned by the priv user
|
||||
// and not accessible by the target user
|
||||
var socketPath *check.Absolute
|
||||
if len(s.Display) > 1 && s.Display[0] == ':' { // `:%d`
|
||||
if n, err := strconv.Atoi(s.Display[1:]); err == nil && n >= 0 {
|
||||
socketPath = absX11SocketDir.Append("X" + strconv.Itoa(n))
|
||||
}
|
||||
} else if len(s.Display) > 5 && strings.HasPrefix(s.Display, "unix:") { // `unix:%s`
|
||||
if a, err := check.NewAbs(s.Display[5:]); err == nil {
|
||||
socketPath = a
|
||||
}
|
||||
}
|
||||
if socketPath != nil {
|
||||
if _, err := state.k.stat(socketPath.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
|
||||
}
|
||||
} else {
|
||||
state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute)
|
||||
if state.Container.Flags&hst.FHostAbstract == 0 {
|
||||
s.Display = "unix:" + socketPath.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.sys.ChangeHosts("#" + state.uid.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spX11Op) toContainer(state *outcomeStateParams) error {
|
||||
state.env["DISPLAY"] = s.Display
|
||||
state.params.Bind(absX11SocketDir, absX11SocketDir, 0)
|
||||
return nil
|
||||
}
|
||||
119
internal/outcome/spx11_test.go
Normal file
119
internal/outcome/spx11_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package outcome
|
||||
|
||||
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},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user