internal/app: modularise outcome finalise
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m30s
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m10s
Test / Hpkg (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 4m35s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m30s
This is the initial effort of splitting up host and container side of finalisation for params to shim. The new layout also enables much finer grained unit testing of each step, as well as partition access to per-app state for each step. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
9462af08f3
commit
eb5ee4fece
@ -18,8 +18,8 @@ func Main(ctx context.Context, msg container.Msg, config *hst.Config) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
seal := outcome{id: &stringPair[state.ID]{id, id.String()}, syscallDispatcher: direct{}}
|
||||
if err := seal.finalise(ctx, msg, config); err != nil {
|
||||
seal := outcome{syscallDispatcher: direct{}}
|
||||
if err := seal.finalise(ctx, msg, &id, config); err != nil {
|
||||
printMessageError("cannot seal app:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os/exec"
|
||||
@ -52,12 +54,21 @@ func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
|
||||
case "/home/ophestra/.pulse-cookie":
|
||||
return stubFileInfoIsDir(true), nil
|
||||
case "/home/ophestra/xdg/config/pulse/cookie":
|
||||
return stubFileInfoIsDir(false), nil
|
||||
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 "/":
|
||||
|
@ -1,7 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"reflect"
|
||||
@ -19,6 +21,9 @@ import (
|
||||
)
|
||||
|
||||
func TestApp(t *testing.T) {
|
||||
msg := container.NewMsg(nil)
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
k syscallDispatcher
|
||||
@ -36,7 +41,7 @@ func TestApp(t *testing.T) {
|
||||
0xbd, 0x01, 0x78, 0x0e,
|
||||
0xb9, 0xa6, 0x07, 0xac,
|
||||
},
|
||||
system.New(t.Context(), container.NewMsg(nil), 1000000).
|
||||
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).
|
||||
@ -128,7 +133,7 @@ func TestApp(t *testing.T) {
|
||||
0x82, 0xd4, 0x13, 0x36,
|
||||
0x9b, 0x64, 0xce, 0x7c,
|
||||
},
|
||||
system.New(t.Context(), container.NewMsg(nil), 1000009).
|
||||
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).
|
||||
@ -140,7 +145,6 @@ func TestApp(t *testing.T) {
|
||||
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")).
|
||||
CopyFile(new([]byte), m("/home/ophestra/xdg/config/pulse/cookie"), 256, 256).
|
||||
MustProxyDBus(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
@ -211,7 +215,7 @@ func TestApp(t *testing.T) {
|
||||
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.Tmp+"/pulse-cookie"), nil).
|
||||
Place(m(hst.Tmp+"/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("/run/dbus/system_bus_socket"), 0).
|
||||
Remount(m("/"), syscall.MS_RDONLY),
|
||||
@ -279,7 +283,7 @@ func TestApp(t *testing.T) {
|
||||
0x4c, 0xf0, 0x73, 0xbd,
|
||||
0xb4, 0x6e, 0xb5, 0xc1,
|
||||
},
|
||||
system.New(t.Context(), container.NewMsg(nil), 1000001).
|
||||
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).
|
||||
@ -290,7 +294,6 @@ func TestApp(t *testing.T) {
|
||||
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")).
|
||||
CopyFile(nil, m("/home/ophestra/xdg/config/pulse/cookie"), 256, 256).
|
||||
Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711).
|
||||
MustProxyDBus(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), &dbus.Config{
|
||||
Talk: []string{
|
||||
@ -361,7 +364,7 @@ func TestApp(t *testing.T) {
|
||||
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.Tmp+"/pulse-cookie"), nil).
|
||||
Place(m(hst.Tmp+"/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("/run/dbus/system_bus_socket"), 0).
|
||||
Remount(m("/"), syscall.MS_RDONLY),
|
||||
@ -375,8 +378,8 @@ func TestApp(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("finalise", func(t *testing.T) {
|
||||
seal := outcome{syscallDispatcher: tc.k, id: &stringPair[state.ID]{tc.id, tc.id.String()}}
|
||||
err := seal.finalise(t.Context(), container.NewMsg(nil), tc.config)
|
||||
seal := outcome{syscallDispatcher: tc.k}
|
||||
err := seal.finalise(t.Context(), msg, &tc.id, tc.config)
|
||||
if err != nil {
|
||||
if s, ok := container.GetErrorMessage(err); !ok {
|
||||
t.Fatalf("Seal: error = %v", err)
|
||||
@ -392,8 +395,8 @@ func TestApp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("params", func(t *testing.T) {
|
||||
if !reflect.DeepEqual(seal.container, tc.wantParams) {
|
||||
t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(seal.container), mustMarshal(tc.wantParams))
|
||||
if !reflect.DeepEqual(&seal.container, tc.wantParams) {
|
||||
t.Errorf("seal: container =\n%s\n, want\n%s", mustMarshal(&seal.container), mustMarshal(tc.wantParams))
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -442,6 +445,16 @@ func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTim
|
||||
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") }
|
||||
|
||||
func m(pathname string) *container.Absolute {
|
||||
return container.MustAbs(pathname)
|
||||
}
|
||||
|
@ -1,253 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// in practice there should be less than 30 system mount points
|
||||
const preallocateOpsCount = 1 << 5
|
||||
|
||||
// newContainer initialises [container.Params] via [hst.ContainerConfig].
|
||||
// Note that remaining container setup must be queued by the caller.
|
||||
func newContainer(
|
||||
msg container.Msg,
|
||||
k syscallDispatcher,
|
||||
s *hst.ContainerConfig,
|
||||
prefix string,
|
||||
sc *hst.Paths,
|
||||
uid, gid *int,
|
||||
) (*container.Params, map[string]string, error) {
|
||||
if s == nil {
|
||||
return nil, nil, newWithMessage("invalid container configuration")
|
||||
}
|
||||
|
||||
params := &container.Params{
|
||||
Hostname: s.Hostname,
|
||||
RetainSession: s.Tty,
|
||||
HostNet: s.HostNet,
|
||||
HostAbstract: s.HostAbstract,
|
||||
|
||||
// the container is canceled when shim is requested to exit or receives an interrupt or termination signal;
|
||||
// this behaviour is implemented in the shim
|
||||
ForwardCancel: s.WaitDelay >= 0,
|
||||
}
|
||||
|
||||
as := &hst.ApplyState{AutoEtcPrefix: prefix}
|
||||
{
|
||||
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem))
|
||||
params.Ops = &ops
|
||||
as.Ops = &ops
|
||||
}
|
||||
|
||||
if s.Multiarch {
|
||||
params.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
|
||||
if !s.SeccompCompat {
|
||||
params.SeccompPresets |= seccomp.PresetExt
|
||||
}
|
||||
if !s.Devel {
|
||||
params.SeccompPresets |= seccomp.PresetDenyDevel
|
||||
}
|
||||
if !s.Userns {
|
||||
params.SeccompPresets |= seccomp.PresetDenyNS
|
||||
}
|
||||
if !s.Tty {
|
||||
params.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if s.MapRealUID {
|
||||
params.Uid = k.getuid()
|
||||
*uid = params.Uid
|
||||
params.Gid = k.getgid()
|
||||
*gid = params.Gid
|
||||
} else {
|
||||
*uid = k.overflowUid(msg)
|
||||
*gid = k.overflowGid(msg)
|
||||
}
|
||||
|
||||
filesystem := s.Filesystem
|
||||
var autoroot *hst.FSBind
|
||||
// valid happens late, so root mount gets it here
|
||||
if len(filesystem) > 0 && filesystem[0].Valid() && filesystem[0].Path().String() == container.FHSRoot {
|
||||
// if the first element targets /, it is inserted early and excluded from path hiding
|
||||
rootfs := filesystem[0].FilesystemConfig
|
||||
filesystem = filesystem[1:]
|
||||
rootfs.Apply(as)
|
||||
|
||||
// autoroot requires special handling during path hiding
|
||||
if b, ok := rootfs.(*hst.FSBind); ok && b.IsAutoRoot() {
|
||||
autoroot = b
|
||||
}
|
||||
}
|
||||
|
||||
params.
|
||||
Proc(container.AbsFHSProc).
|
||||
Tmpfs(hst.AbsTmp, 1<<12, 0755)
|
||||
|
||||
if !s.Device {
|
||||
params.DevWritable(container.AbsFHSDev, true)
|
||||
} else {
|
||||
params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
|
||||
}
|
||||
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
|
||||
params.Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
|
||||
|
||||
/* 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 */
|
||||
var hidePaths []string
|
||||
hidePaths = append(hidePaths, sc.RuntimePath.String(), sc.SharePath.String())
|
||||
_, systemBusAddr := dbus.Address()
|
||||
if entries, err := dbus.Parse([]byte(systemBusAddr)); err != nil {
|
||||
return nil, nil, 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 == container.FHSRoot {
|
||||
msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
|
||||
}
|
||||
hidePaths = append(hidePaths, dir)
|
||||
} else {
|
||||
msg.Verbosef("dbus socket %q is not absolute", pair[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hidePathMatch := make([]bool, len(hidePaths))
|
||||
for i := range hidePaths {
|
||||
if err := evalSymlinks(msg, k, &hidePaths[i]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var hidePathSourceCount int
|
||||
for i, c := range filesystem {
|
||||
if !c.Valid() {
|
||||
return nil, nil, fmt.Errorf("invalid filesystem at index %d", i)
|
||||
}
|
||||
c.Apply(as)
|
||||
|
||||
// fs counter
|
||||
hidePathSourceCount += len(c.Host())
|
||||
}
|
||||
|
||||
// AutoRootOp is a collection of many BindMountOp internally
|
||||
var autoRootEntries []fs.DirEntry
|
||||
if autoroot != nil {
|
||||
if d, err := k.readdir(autoroot.Source.String()); err != nil {
|
||||
return nil, nil, err
|
||||
} else {
|
||||
// autoroot counter
|
||||
hidePathSourceCount += len(d)
|
||||
autoRootEntries = d
|
||||
}
|
||||
}
|
||||
|
||||
hidePathSource := make([]*container.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(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 nil, nil, syscall.ENOTRECOVERABLE
|
||||
}
|
||||
|
||||
hidePathSourceEval[i] = [2]string{a.String(), a.String()}
|
||||
if err := evalSymlinks(msg, k, &hidePathSourceEval[i][0]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range hidePathSourceEval {
|
||||
for i := range hidePaths {
|
||||
// skip matched entries
|
||||
if hidePathMatch[i] {
|
||||
continue
|
||||
}
|
||||
|
||||
if ok, err := deepContainsH(p[0], hidePaths[i]); err != nil {
|
||||
return nil, nil, err
|
||||
} else if ok {
|
||||
hidePathMatch[i] = true
|
||||
msg.Verbosef("hiding path %q from %q", hidePaths[i], p[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cover matched paths
|
||||
for i, ok := range hidePathMatch {
|
||||
if ok {
|
||||
if a, err := container.NewAbs(hidePaths[i]); err != nil {
|
||||
var absoluteError *container.AbsoluteError
|
||||
if !errors.As(err, &absoluteError) {
|
||||
return nil, nil, err
|
||||
}
|
||||
if absoluteError == nil {
|
||||
return nil, nil, syscall.ENOTRECOVERABLE
|
||||
}
|
||||
return nil, nil, fmt.Errorf("invalid path hiding candidate %q", absoluteError.Pathname)
|
||||
} else {
|
||||
params.Tmpfs(a, 1<<13, 0755)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no more ContainerConfig paths beyond this point
|
||||
if !s.Device {
|
||||
params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
|
||||
}
|
||||
|
||||
return params, maps.Clone(s.Env), nil
|
||||
}
|
||||
|
||||
// evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist].
|
||||
func evalSymlinks(msg container.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
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -11,6 +13,13 @@ import (
|
||||
"hakurei.app/internal"
|
||||
)
|
||||
|
||||
// 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.
|
||||
@ -26,6 +35,8 @@ type syscallDispatcher interface {
|
||||
lookupEnv(key string) (string, bool)
|
||||
// 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].
|
||||
@ -64,6 +75,7 @@ func (direct) getuid() int { return os.Getuid() }
|
||||
func (direct) getgid() int { return os.Getgid() }
|
||||
func (direct) lookupEnv(key string) (string, bool) { return os.LookupEnv(key) }
|
||||
func (direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) }
|
||||
func (direct) 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() }
|
||||
|
||||
|
@ -14,6 +14,7 @@ func (panicDispatcher) getuid() int { panic("unreachab
|
||||
func (panicDispatcher) getgid() int { panic("unreachable") }
|
||||
func (panicDispatcher) lookupEnv(string) (string, bool) { 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) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
|
@ -8,35 +8,21 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"os/user"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
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 }
|
||||
|
||||
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}
|
||||
@ -44,115 +30,40 @@ func newWithMessageError(msg string, err error) error {
|
||||
|
||||
// An outcome is the runnable state of a hakurei container via [hst.Config].
|
||||
type outcome struct {
|
||||
// copied from initialising [app]
|
||||
id *stringPair[state.ID]
|
||||
// copied from [sys.State]
|
||||
runDirPath *container.Absolute
|
||||
|
||||
// initial [hst.Config] gob stream for state data;
|
||||
// this is prepared ahead of time as config is clobbered during seal creation
|
||||
ct io.WriterTo
|
||||
|
||||
user hsuUser
|
||||
sys *system.I
|
||||
ctx context.Context
|
||||
sys *system.I
|
||||
ctx context.Context
|
||||
|
||||
waitDelay time.Duration
|
||||
container *container.Params
|
||||
env map[string]string
|
||||
sync *os.File
|
||||
active atomic.Bool
|
||||
container container.Params
|
||||
|
||||
// TODO(ophestra): move this to the system op
|
||||
sync *os.File
|
||||
|
||||
// Populated during outcome.finalise.
|
||||
proc *finaliseProcess
|
||||
|
||||
// Whether the current process is in outcome.main.
|
||||
active atomic.Bool
|
||||
|
||||
syscallDispatcher
|
||||
}
|
||||
|
||||
// shareHost holds optional share directory state that must not be accessed directly
|
||||
type shareHost struct {
|
||||
// whether XDG_RUNTIME_DIR is used post hsu
|
||||
useRuntimeDir bool
|
||||
// process-specific directory in tmpdir, empty if unused
|
||||
sharePath *container.Absolute
|
||||
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
|
||||
runtimeSharePath *container.Absolute
|
||||
|
||||
seal *outcome
|
||||
sc hst.Paths
|
||||
}
|
||||
|
||||
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
|
||||
func (share *shareHost) ensureRuntimeDir() {
|
||||
if share.useRuntimeDir {
|
||||
return
|
||||
}
|
||||
share.useRuntimeDir = true
|
||||
share.seal.sys.Ensure(share.sc.RunDirPath, 0700)
|
||||
share.seal.sys.UpdatePermType(system.User, share.sc.RunDirPath, acl.Execute)
|
||||
share.seal.sys.Ensure(share.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset
|
||||
share.seal.sys.UpdatePermType(system.User, share.sc.RuntimePath, acl.Execute)
|
||||
}
|
||||
|
||||
// instance returns a process-specific share path within tmpdir
|
||||
func (share *shareHost) instance() *container.Absolute {
|
||||
if share.sharePath != nil {
|
||||
return share.sharePath
|
||||
}
|
||||
share.sharePath = share.sc.SharePath.Append(share.seal.id.String())
|
||||
share.seal.sys.Ephemeral(system.Process, share.sharePath, 0711)
|
||||
return share.sharePath
|
||||
}
|
||||
|
||||
// runtime returns a process-specific share path within XDG_RUNTIME_DIR
|
||||
func (share *shareHost) runtime() *container.Absolute {
|
||||
if share.runtimeSharePath != nil {
|
||||
return share.runtimeSharePath
|
||||
}
|
||||
share.ensureRuntimeDir()
|
||||
share.runtimeSharePath = share.sc.RunDirPath.Append(share.seal.id.String())
|
||||
share.seal.sys.Ephemeral(system.Process, share.runtimeSharePath, 0700)
|
||||
share.seal.sys.UpdatePerm(share.runtimeSharePath, acl.Execute)
|
||||
return share.runtimeSharePath
|
||||
}
|
||||
|
||||
// hsuUser stores post-hsu credentials and metadata
|
||||
type hsuUser struct {
|
||||
identity *stringPair[int]
|
||||
// target uid resolved by hid:aid
|
||||
uid *stringPair[int]
|
||||
|
||||
// supplementary group ids
|
||||
supp []string
|
||||
|
||||
// app user home directory
|
||||
home *container.Absolute
|
||||
// passwd database username
|
||||
username string
|
||||
}
|
||||
|
||||
func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.Config) error {
|
||||
func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID, config *hst.Config) error {
|
||||
const (
|
||||
home = "HOME"
|
||||
shell = "SHELL"
|
||||
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
xdgSessionClass = "XDG_SESSION_CLASS"
|
||||
xdgSessionType = "XDG_SESSION_TYPE"
|
||||
|
||||
term = "TERM"
|
||||
display = "DISPLAY"
|
||||
|
||||
pulseServer = "PULSE_SERVER"
|
||||
pulseCookie = "PULSE_COOKIE"
|
||||
|
||||
dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS"
|
||||
dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS"
|
||||
// only used for a nil configured env map
|
||||
envAllocSize = 1 << 6
|
||||
)
|
||||
|
||||
if ctx == nil {
|
||||
var kp finaliseProcess
|
||||
|
||||
if ctx == nil || id == nil {
|
||||
// unreachable
|
||||
panic("invalid call to finalise")
|
||||
}
|
||||
if k.ctx != nil {
|
||||
if k.ctx != nil || k.proc != nil {
|
||||
// unreachable
|
||||
panic("attempting to finalise twice")
|
||||
}
|
||||
@ -165,6 +76,7 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C
|
||||
return newWithMessage("invalid path to home directory")
|
||||
}
|
||||
|
||||
// TODO(ophestra): do not clobber during finalise
|
||||
{
|
||||
// encode initial configuration for state tracking
|
||||
ct := new(bytes.Buffer)
|
||||
@ -179,21 +91,7 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C
|
||||
return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity))
|
||||
}
|
||||
|
||||
k.user = hsuUser{
|
||||
identity: newInt(config.Identity),
|
||||
home: config.Home,
|
||||
username: config.Username,
|
||||
}
|
||||
|
||||
hsu := Hsu{k: k}
|
||||
if k.user.username == "" {
|
||||
k.user.username = "chronos"
|
||||
} else if !isValidUsername(k.user.username) {
|
||||
return newWithMessage(fmt.Sprintf("invalid user name %q", k.user.username))
|
||||
}
|
||||
k.user.uid = newInt(HsuUid(hsu.MustIDMsg(msg), k.user.identity.unwrap()))
|
||||
|
||||
k.user.supp = make([]string, len(config.Groups))
|
||||
kp.supp = make([]string, len(config.Groups))
|
||||
for i, name := range config.Groups {
|
||||
if gid, err := k.lookupGroupId(name); err != nil {
|
||||
var unknownGroupError user.UnknownGroupError
|
||||
@ -203,7 +101,7 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C
|
||||
return &hst.AppError{Step: "look up group by name", Err: err}
|
||||
}
|
||||
} else {
|
||||
k.user.supp[i] = gid
|
||||
kp.supp[i] = gid
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,7 +111,7 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C
|
||||
|
||||
if config.Shell == nil {
|
||||
config.Shell = container.AbsFHSRoot.Append("bin", "sh")
|
||||
s, _ := k.lookupEnv(shell)
|
||||
s, _ := k.lookupEnv("SHELL")
|
||||
if a, err := container.NewAbs(s); err == nil {
|
||||
config.Shell = a
|
||||
}
|
||||
@ -282,291 +180,98 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C
|
||||
return newWithMessage("invalid program path")
|
||||
}
|
||||
|
||||
// TODO(ophestra): revert this after params to shim
|
||||
share := &shareHost{seal: k}
|
||||
copyPaths(k.syscallDispatcher).Copy(&share.sc, hsu.MustIDMsg(msg))
|
||||
msg.Verbosef("process share directory at %q, runtime directory at %q", share.sc.SharePath, share.sc.RunDirPath)
|
||||
|
||||
var mapuid, mapgid *stringPair[int]
|
||||
{
|
||||
var uid, gid int
|
||||
var err error
|
||||
k.container, k.env, err = newContainer(msg, k, config.Container, k.id.String(), &share.sc, &uid, &gid)
|
||||
k.waitDelay = config.Container.WaitDelay
|
||||
if err != nil {
|
||||
return &hst.AppError{Step: "initialise container configuration", Err: err}
|
||||
}
|
||||
if len(config.Args) == 0 {
|
||||
config.Args = []string{config.Path.String()}
|
||||
}
|
||||
k.container.Path = config.Path
|
||||
k.container.Args = config.Args
|
||||
|
||||
mapuid = newInt(uid)
|
||||
mapgid = newInt(gid)
|
||||
if k.env == nil {
|
||||
k.env = make(map[string]string, 1<<6)
|
||||
}
|
||||
// enforce bounds and default early
|
||||
kp.waitDelay = shimWaitTimeout
|
||||
if config.Container.WaitDelay <= 0 {
|
||||
kp.waitDelay += DefaultShimWaitDelay
|
||||
} else if config.Container.WaitDelay > MaxShimWaitDelay {
|
||||
kp.waitDelay += MaxShimWaitDelay
|
||||
} else {
|
||||
kp.waitDelay += config.Container.WaitDelay
|
||||
}
|
||||
|
||||
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
|
||||
innerRuntimeDir := container.AbsFHSRunUser.Append(mapuid.String())
|
||||
k.env[xdgRuntimeDir] = innerRuntimeDir.String()
|
||||
k.env[xdgSessionClass] = "user"
|
||||
k.env[xdgSessionType] = "tty"
|
||||
s := outcomeState{
|
||||
ID: id,
|
||||
Identity: config.Identity,
|
||||
UserID: (&Hsu{k: k}).MustIDMsg(msg),
|
||||
EnvPaths: copyPaths(k.syscallDispatcher),
|
||||
|
||||
k.runDirPath = share.sc.RunDirPath
|
||||
k.sys = system.New(k.ctx, msg, k.user.uid.unwrap())
|
||||
k.sys.Ensure(share.sc.SharePath, 0711)
|
||||
// TODO(ophestra): apply pd behaviour here instead of clobbering hst.Config
|
||||
Container: config.Container,
|
||||
}
|
||||
if s.Container.MapRealUID {
|
||||
s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
|
||||
} else {
|
||||
s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
|
||||
}
|
||||
|
||||
// TODO(ophestra): duplicate in shim (params to shim)
|
||||
if err := s.populateLocal(k.syscallDispatcher, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
kp.runDirPath, kp.identity, kp.id = s.sc.RunDirPath, s.identity, s.id
|
||||
k.sys = system.New(k.ctx, msg, s.uid.unwrap())
|
||||
|
||||
{
|
||||
runtimeDir := share.sc.SharePath.Append("runtime")
|
||||
k.sys.Ensure(runtimeDir, 0700)
|
||||
k.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
|
||||
runtimeDirInst := runtimeDir.Append(k.user.identity.String())
|
||||
k.sys.Ensure(runtimeDirInst, 0700)
|
||||
k.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
|
||||
k.container.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
|
||||
k.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
||||
}
|
||||
ops := []outcomeOp{
|
||||
// must run first
|
||||
&spParamsOp{Path: config.Path, Args: config.Args},
|
||||
|
||||
{
|
||||
tmpdir := share.sc.SharePath.Append("tmpdir")
|
||||
k.sys.Ensure(tmpdir, 0700)
|
||||
k.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
|
||||
tmpdirInst := tmpdir.Append(k.user.identity.String())
|
||||
k.sys.Ensure(tmpdirInst, 01700)
|
||||
k.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
|
||||
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
|
||||
k.container.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
|
||||
}
|
||||
// TODO(ophestra): move this late for #8 and #9
|
||||
spFilesystemOp{},
|
||||
|
||||
{
|
||||
username := "chronos"
|
||||
if k.user.username != "" {
|
||||
username = k.user.username
|
||||
spRuntimeOp{},
|
||||
spTmpdirOp{},
|
||||
&spAccountOp{Home: config.Home, Username: config.Username, Shell: config.Shell},
|
||||
}
|
||||
k.container.Dir = k.user.home
|
||||
k.env["HOME"] = k.user.home.String()
|
||||
k.env["USER"] = username
|
||||
k.env[shell] = config.Shell.String()
|
||||
|
||||
k.container.Place(container.AbsFHSEtc.Append("passwd"),
|
||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+k.user.home.String()+":"+config.Shell.String()+"\n"))
|
||||
k.container.Place(container.AbsFHSEtc.Append("group"),
|
||||
[]byte("hakurei:x:"+mapgid.String()+":\n"))
|
||||
}
|
||||
et := config.Enablements.Unwrap()
|
||||
if et&hst.EWayland != 0 {
|
||||
ops = append(ops, &spWaylandOp{sync: &k.sync})
|
||||
}
|
||||
if et&hst.EX11 != 0 {
|
||||
ops = append(ops, &spX11Op{})
|
||||
}
|
||||
if et&hst.EPulse != 0 {
|
||||
ops = append(ops, &spPulseOp{})
|
||||
}
|
||||
if et&hst.EDBus != 0 {
|
||||
ops = append(ops, &spDBusOp{})
|
||||
}
|
||||
|
||||
// pass TERM for proper terminal I/O in initial process
|
||||
if t, ok := k.lookupEnv(term); ok {
|
||||
k.env[term] = t
|
||||
}
|
||||
stateSys := outcomeStateSys{sys: k.sys, outcomeState: &s}
|
||||
for _, op := range ops {
|
||||
if err := op.toSystem(&stateSys, config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements.Unwrap()&hst.EWayland != 0 {
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath *container.Absolute
|
||||
if name, ok := k.lookupEnv(wayland.WaylandDisplay); !ok {
|
||||
msg.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
|
||||
} else if a, err := container.NewAbs(name); err != nil {
|
||||
socketPath = share.sc.RuntimePath.Append(name)
|
||||
// TODO(ophestra): move to shim
|
||||
stateParams := outcomeStateParams{params: &k.container, outcomeState: &s}
|
||||
if s.Container.Env == nil {
|
||||
stateParams.env = make(map[string]string, envAllocSize)
|
||||
} else {
|
||||
socketPath = a
|
||||
stateParams.env = maps.Clone(s.Container.Env)
|
||||
}
|
||||
|
||||
innerPath := innerRuntimeDir.Append(wayland.FallbackName)
|
||||
k.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
|
||||
if !config.DirectWayland { // set up security-context-v1
|
||||
appID := config.ID
|
||||
if appID == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appID = "app.hakurei." + k.id.String()
|
||||
}
|
||||
// downstream socket paths
|
||||
outerPath := share.instance().Append("wayland")
|
||||
k.sys.Wayland(&k.sync, outerPath, socketPath, appID, k.id.String())
|
||||
k.container.Bind(outerPath, innerPath, 0)
|
||||
} else { // bind mount wayland socket (insecure)
|
||||
msg.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||
share.ensureRuntimeDir()
|
||||
k.container.Bind(socketPath, innerPath, 0)
|
||||
k.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements.Unwrap()&hst.EX11 != 0 {
|
||||
if d, ok := k.lookupEnv(display); !ok {
|
||||
return newWithMessage("DISPLAY is not set")
|
||||
} else {
|
||||
socketDir := container.AbsFHSTmp.Append(".X11-unix")
|
||||
|
||||
// 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 *container.Absolute
|
||||
if len(d) > 1 && d[0] == ':' { // `:%d`
|
||||
if n, err := strconv.Atoi(d[1:]); err == nil && n >= 0 {
|
||||
socketPath = socketDir.Append("X" + strconv.Itoa(n))
|
||||
}
|
||||
} else if len(d) > 5 && strings.HasPrefix(d, "unix:") { // `unix:%s`
|
||||
if a, err := container.NewAbs(d[5:]); err == nil {
|
||||
socketPath = a
|
||||
}
|
||||
}
|
||||
if socketPath != nil {
|
||||
if _, err := 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 {
|
||||
k.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute)
|
||||
if !config.Container.HostAbstract {
|
||||
d = "unix:" + socketPath.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
k.sys.ChangeHosts("#" + k.user.uid.String())
|
||||
k.env[display] = d
|
||||
k.container.Bind(socketDir, socketDir, 0)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements.Unwrap()&hst.EPulse != 0 {
|
||||
// PulseAudio runtime directory (usually `/run/user/%d/pulse`)
|
||||
pulseRuntimeDir := share.sc.RuntimePath.Append("pulse")
|
||||
// PulseAudio socket (usually `/run/user/%d/pulse/native`)
|
||||
pulseSocket := pulseRuntimeDir.Append("native")
|
||||
|
||||
if _, err := 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 newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||
}
|
||||
|
||||
if s, err := k.stat(pulseSocket.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
|
||||
}
|
||||
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
|
||||
for _, op := range ops {
|
||||
if err := op.toContainer(&stateParams); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// hard link pulse socket into target-executable share
|
||||
innerPulseRuntimeDir := share.runtime().Append("pulse")
|
||||
innerPulseSocket := innerRuntimeDir.Append("pulse", "native")
|
||||
k.sys.Link(pulseSocket, innerPulseRuntimeDir)
|
||||
k.container.Bind(innerPulseRuntimeDir, innerPulseSocket, 0)
|
||||
k.env[pulseServer] = "unix:" + innerPulseSocket.String()
|
||||
|
||||
// publish current user's pulse cookie for target user
|
||||
var paCookiePath *container.Absolute
|
||||
{
|
||||
const paLocateStep = "locate PulseAudio cookie"
|
||||
|
||||
// from environment
|
||||
if p, ok := k.lookupEnv(pulseCookie); ok {
|
||||
if a, err := container.NewAbs(p); err != nil {
|
||||
return &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
// this takes precedence, do not verify whether the file is accessible
|
||||
paCookiePath = a
|
||||
goto out
|
||||
}
|
||||
// flatten and sort env for deterministic behaviour
|
||||
k.container.Env = make([]string, 0, len(stateParams.env))
|
||||
for key, value := range stateParams.env {
|
||||
if strings.IndexByte(key, '=') != -1 {
|
||||
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
|
||||
Msg: fmt.Sprintf("invalid environment variable %s", key)}
|
||||
}
|
||||
|
||||
// $HOME/.pulse-cookie
|
||||
if p, ok := k.lookupEnv(home); ok {
|
||||
if a, err := container.NewAbs(p); err != nil {
|
||||
return &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
paCookiePath = a.Append(".pulse-cookie")
|
||||
}
|
||||
|
||||
if s, err := k.stat(paCookiePath.String()); err != nil {
|
||||
paCookiePath = nil
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if s.IsDir() {
|
||||
paCookiePath = nil
|
||||
} else {
|
||||
goto out
|
||||
}
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/pulse/cookie
|
||||
if p, ok := k.lookupEnv(xdgConfigHome); ok {
|
||||
if a, err := container.NewAbs(p); err != nil {
|
||||
return &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
paCookiePath = a.Append("pulse", "cookie")
|
||||
}
|
||||
if s, err := k.stat(paCookiePath.String()); err != nil {
|
||||
paCookiePath = nil
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if s.IsDir() {
|
||||
paCookiePath = nil
|
||||
} else {
|
||||
goto out
|
||||
}
|
||||
}
|
||||
out:
|
||||
}
|
||||
|
||||
if paCookiePath != nil {
|
||||
innerDst := hst.AbsTmp.Append("/pulse-cookie")
|
||||
k.env[pulseCookie] = innerDst.String()
|
||||
var payload *[]byte
|
||||
k.container.PlaceP(innerDst, &payload)
|
||||
k.sys.CopyFile(payload, paCookiePath, 256, 256)
|
||||
} else {
|
||||
msg.Verbose("cannot locate PulseAudio cookie (tried " +
|
||||
"$PULSE_COOKIE, " +
|
||||
"$XDG_CONFIG_HOME/pulse/cookie, " +
|
||||
"$HOME/.pulse-cookie)")
|
||||
}
|
||||
}
|
||||
|
||||
if config.Enablements.Unwrap()&hst.EDBus != 0 {
|
||||
// ensure dbus session bus defaults
|
||||
if config.SessionBus == nil {
|
||||
config.SessionBus = dbus.NewConfig(config.ID, true, true)
|
||||
}
|
||||
|
||||
// downstream socket paths
|
||||
sessionPath, systemPath := share.instance().Append("bus"), share.instance().Append("system_bus_socket")
|
||||
|
||||
// configure dbus proxy
|
||||
if err := k.sys.ProxyDBus(
|
||||
config.SessionBus, config.SystemBus,
|
||||
sessionPath, systemPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// share proxy sockets
|
||||
sessionInner := innerRuntimeDir.Append("bus")
|
||||
k.env[dbusSessionBusAddress] = "unix:path=" + sessionInner.String()
|
||||
k.container.Bind(sessionPath, sessionInner, 0)
|
||||
k.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
|
||||
if config.SystemBus != nil {
|
||||
systemInner := container.AbsFHSRun.Append("dbus/system_bus_socket")
|
||||
k.env[dbusSystemBusAddress] = "unix:path=" + systemInner.String()
|
||||
k.container.Bind(systemPath, systemInner, 0)
|
||||
k.sys.UpdatePerm(systemPath, acl.Read, acl.Write)
|
||||
k.container.Env = append(k.container.Env, key+"="+value)
|
||||
}
|
||||
slices.Sort(k.container.Env)
|
||||
}
|
||||
|
||||
// mount root read-only as the final setup Op
|
||||
// TODO(ophestra): move this to spFilesystemOp after #8 and #9
|
||||
k.container.Remount(container.AbsFHSRoot, syscall.MS_RDONLY)
|
||||
|
||||
// append ExtraPerms last
|
||||
@ -592,21 +297,6 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, config *hst.C
|
||||
k.sys.UpdatePermType(system.User, p.Path, perms...)
|
||||
}
|
||||
|
||||
// flatten and sort env for deterministic behaviour
|
||||
k.container.Env = make([]string, 0, len(k.env))
|
||||
for key, value := range k.env {
|
||||
if strings.IndexByte(key, '=') != -1 {
|
||||
return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL,
|
||||
Msg: fmt.Sprintf("invalid environment variable %s", key)}
|
||||
}
|
||||
k.container.Env = append(k.container.Env, key+"="+value)
|
||||
}
|
||||
slices.Sort(k.container.Env)
|
||||
|
||||
if msg.IsVerbose() {
|
||||
msg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
||||
k.user.uid, k.user.username, config.Groups, k.container.Args, len(*k.container.Ops))
|
||||
}
|
||||
|
||||
k.proc = &kp
|
||||
return nil
|
||||
}
|
||||
|
191
internal/app/outcome.go
Normal file
191
internal/app/outcome.go
Normal file
@ -0,0 +1,191 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
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 {
|
||||
// Generated and accounted for by the caller.
|
||||
ID *state.ID
|
||||
// Copied from ID.
|
||||
id *stringPair[state.ID]
|
||||
|
||||
// Copied from the [hst.Config] field of the same name.
|
||||
Identity int
|
||||
// Copied from Identity.
|
||||
identity *stringPair[int]
|
||||
// Returned by [Hsu.MustIDMsg].
|
||||
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
|
||||
*EnvPaths
|
||||
|
||||
// Matched paths to cover. Populated by spFilesystemOp.
|
||||
HidePaths []*container.Absolute
|
||||
|
||||
// Copied via populateLocal.
|
||||
k syscallDispatcher
|
||||
// Copied via populateLocal.
|
||||
msg container.Msg
|
||||
}
|
||||
|
||||
// valid checks outcomeState to be safe for use with outcomeOp.
|
||||
func (s *outcomeState) valid() bool {
|
||||
return s != nil &&
|
||||
s.ID != nil &&
|
||||
s.Container != nil &&
|
||||
s.EnvPaths != nil
|
||||
}
|
||||
|
||||
// populateLocal populates unexported fields from transmitted exported fields.
|
||||
// These fields are cheaper to recompute per-process.
|
||||
func (s *outcomeState) populateLocal(k syscallDispatcher, msg container.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[state.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() *container.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() *container.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 *container.Absolute
|
||||
// Process-specific directory in XDG_RUNTIME_DIR, nil if unused.
|
||||
runtimeSharePath *container.Absolute
|
||||
|
||||
sys *system.I
|
||||
*outcomeState
|
||||
}
|
||||
|
||||
// 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() *container.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() *container.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] after every call to outcomeOp.toContainer completes.
|
||||
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 *container.Absolute
|
||||
|
||||
as hst.ApplyState
|
||||
*outcomeState
|
||||
}
|
||||
|
||||
// TODO(ophestra): register outcomeOp implementations (params to shim)
|
||||
|
||||
// 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, config *hst.Config) 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
|
||||
}
|
@ -41,6 +41,7 @@ type mainState struct {
|
||||
k *outcome
|
||||
container.Msg
|
||||
uintptr
|
||||
*finaliseProcess
|
||||
}
|
||||
|
||||
const (
|
||||
@ -78,15 +79,9 @@ func (ms mainState) beforeExit(isFault bool) {
|
||||
// this also handles wait for a non-fault termination
|
||||
if ms.cmd != nil && ms.cmdWait != nil {
|
||||
waitDone := make(chan struct{})
|
||||
// TODO(ophestra): enforce this limit early so it does not have to be done twice
|
||||
shimTimeoutCompensated := shimWaitTimeout
|
||||
if ms.k.waitDelay > MaxShimWaitDelay {
|
||||
shimTimeoutCompensated += MaxShimWaitDelay
|
||||
} else {
|
||||
shimTimeoutCompensated += ms.k.waitDelay
|
||||
}
|
||||
|
||||
// this ties waitDone to ctx with the additional compensated timeout duration
|
||||
go func() { <-ms.k.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }()
|
||||
go func() { <-ms.k.ctx.Done(); time.Sleep(ms.waitDelay); close(waitDone) }()
|
||||
|
||||
select {
|
||||
case err := <-ms.cmdWait:
|
||||
@ -137,9 +132,9 @@ func (ms mainState) beforeExit(isFault bool) {
|
||||
}
|
||||
|
||||
if ms.uintptr&mainNeedsRevert != 0 {
|
||||
if ok, err := ms.store.Do(ms.k.user.identity.unwrap(), func(c state.Cursor) {
|
||||
if ok, err := ms.store.Do(ms.identity.unwrap(), func(c state.Cursor) {
|
||||
if ms.uintptr&mainNeedsDestroy != 0 {
|
||||
if err := c.Destroy(ms.k.id.unwrap()); err != nil {
|
||||
if err := c.Destroy(ms.id.unwrap()); err != nil {
|
||||
perror(err, "destroy state entry")
|
||||
}
|
||||
}
|
||||
@ -216,23 +211,45 @@ func (ms mainState) fatal(fallback string, ferr error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// finaliseProcess contains information collected during outcome.finalise used in outcome.main.
|
||||
type finaliseProcess struct {
|
||||
// Supplementary group ids.
|
||||
supp []string
|
||||
|
||||
// Copied from [hst.ContainerConfig], without exceeding [MaxShimWaitDelay].
|
||||
waitDelay time.Duration
|
||||
|
||||
// Copied from the RunDirPath field of [hst.Paths].
|
||||
runDirPath *container.Absolute
|
||||
|
||||
// Copied from outcomeState.
|
||||
identity *stringPair[int]
|
||||
|
||||
// Copied from outcomeState.
|
||||
id *stringPair[state.ID]
|
||||
}
|
||||
|
||||
// main carries out outcome and terminates. main does not return.
|
||||
func (k *outcome) main(msg container.Msg) {
|
||||
if !k.active.CompareAndSwap(false, true) {
|
||||
panic("outcome: attempted to run twice")
|
||||
}
|
||||
|
||||
if k.proc == 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}
|
||||
ms := &mainState{Msg: msg, k: k, finaliseProcess: k.proc}
|
||||
|
||||
if err := k.sys.Commit(); err != nil {
|
||||
ms.fatal("cannot commit system setup:", err)
|
||||
}
|
||||
ms.uintptr |= mainNeedsRevert
|
||||
ms.store = state.NewMulti(msg, k.runDirPath.String())
|
||||
ms.store = state.NewMulti(msg, ms.runDirPath.String())
|
||||
|
||||
ctx, cancel := context.WithCancel(k.ctx)
|
||||
defer cancel()
|
||||
@ -253,14 +270,14 @@ func (k *outcome) main(msg container.Msg) {
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_IDENTITY=" + k.user.identity.String(),
|
||||
"HAKUREI_IDENTITY=" + ms.identity.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if len(k.user.supp) > 0 {
|
||||
msg.Verbosef("attaching supplementary group ids %s", k.user.supp)
|
||||
if len(ms.supp) > 0 {
|
||||
msg.Verbosef("attaching supplementary group ids %s", ms.supp)
|
||||
// interpreted by hsu
|
||||
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.user.supp, " "))
|
||||
ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(ms.supp, " "))
|
||||
}
|
||||
|
||||
msg.Verbosef("setuid helper at %s", hsuPath)
|
||||
@ -282,8 +299,8 @@ func (k *outcome) main(msg container.Msg) {
|
||||
go func() {
|
||||
setupErr <- e.Encode(&shimParams{
|
||||
os.Getpid(),
|
||||
k.waitDelay,
|
||||
k.container,
|
||||
ms.waitDelay,
|
||||
&k.container,
|
||||
msg.IsVerbose(),
|
||||
})
|
||||
}()
|
||||
@ -300,9 +317,9 @@ func (k *outcome) main(msg container.Msg) {
|
||||
}
|
||||
|
||||
// shim accepted setup payload, create process state
|
||||
if ok, err := ms.store.Do(k.user.identity.unwrap(), func(c state.Cursor) {
|
||||
if ok, err := ms.store.Do(ms.identity.unwrap(), func(c state.Cursor) {
|
||||
if err := c.Save(&state.State{
|
||||
ID: k.id.unwrap(),
|
||||
ID: ms.id.unwrap(),
|
||||
PID: ms.cmd.Process.Pid,
|
||||
Time: *ms.Time,
|
||||
}, k.ct); err != nil {
|
||||
|
@ -148,13 +148,8 @@ func ShimMain() {
|
||||
z.Params = *params.Container
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
|
||||
// bounds and default enforced in finalise.go
|
||||
z.WaitDelay = params.WaitDelay
|
||||
if z.WaitDelay == 0 {
|
||||
z.WaitDelay = DefaultShimWaitDelay
|
||||
}
|
||||
if z.WaitDelay > MaxShimWaitDelay {
|
||||
z.WaitDelay = MaxShimWaitDelay
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
printMessageError("cannot start container:", err)
|
||||
|
55
internal/app/spaccount.go
Normal file
55
internal/app/spaccount.go
Normal file
@ -0,0 +1,55 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
// spAccountOp sets up user account emulation inside the container.
|
||||
type spAccountOp struct {
|
||||
// Inner directory to use as the home directory of the emulated user.
|
||||
Home *container.Absolute
|
||||
// String matching the default NAME_REGEX value from adduser to use as the username of the emulated user.
|
||||
Username string
|
||||
// Pathname of shell to use for the emulated user.
|
||||
Shell *container.Absolute
|
||||
}
|
||||
|
||||
func (s *spAccountOp) toSystem(*outcomeStateSys, *hst.Config) error {
|
||||
const fallbackUsername = "chronos"
|
||||
|
||||
// do checks here to fail before fork/exec
|
||||
if s.Home == nil || s.Shell == nil {
|
||||
// unreachable
|
||||
return syscall.ENOTRECOVERABLE
|
||||
}
|
||||
if s.Username == "" {
|
||||
s.Username = fallbackUsername
|
||||
} else if !isValidUsername(s.Username) {
|
||||
return newWithMessage(fmt.Sprintf("invalid user name %q", s.Username))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spAccountOp) toContainer(state *outcomeStateParams) error {
|
||||
state.params.Dir = s.Home
|
||||
state.env["HOME"] = s.Home.String()
|
||||
state.env["USER"] = s.Username
|
||||
state.env["SHELL"] = s.Shell.String()
|
||||
|
||||
state.params.
|
||||
Place(container.AbsFHSEtc.Append("passwd"),
|
||||
[]byte(s.Username+":x:"+
|
||||
state.mapuid.String()+":"+
|
||||
state.mapgid.String()+
|
||||
":Hakurei:"+
|
||||
s.Home.String()+":"+
|
||||
s.Shell.String()+"\n")).
|
||||
Place(container.AbsFHSEtc.Append("group"),
|
||||
[]byte("hakurei:x:"+state.mapgid.String()+":\n"))
|
||||
|
||||
return nil
|
||||
}
|
300
internal/app/spcontainer.go
Normal file
300
internal/app/spcontainer.go
Normal file
@ -0,0 +1,300 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// spParamsOp initialises unordered fields of [container.Params] and the optional root filesystem.
|
||||
// This outcomeOp is hardcoded to always run first.
|
||||
type spParamsOp struct {
|
||||
// Copied from the [hst.Config] field of the same name.
|
||||
Path *container.Absolute `json:"path,omitempty"`
|
||||
// Copied from the [hst.Config] field of the same name.
|
||||
Args []string `json:"args"`
|
||||
|
||||
// Value of $TERM, stored during toSystem.
|
||||
Term string
|
||||
// Whether $TERM is set, stored during toSystem.
|
||||
TermSet bool
|
||||
}
|
||||
|
||||
func (s *spParamsOp) toSystem(state *outcomeStateSys, _ *hst.Config) 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.Tty
|
||||
state.params.HostNet = state.Container.HostNet
|
||||
state.params.HostAbstract = state.Container.HostAbstract
|
||||
|
||||
if s.Path == nil {
|
||||
return newWithMessage("invalid program path")
|
||||
}
|
||||
state.params.Path = s.Path
|
||||
|
||||
if len(s.Args) == 0 {
|
||||
state.params.Args = []string{s.Path.String()}
|
||||
} else {
|
||||
state.params.Args = s.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.Container.WaitDelay >= 0
|
||||
|
||||
if state.Container.Multiarch {
|
||||
state.params.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
|
||||
if !state.Container.SeccompCompat {
|
||||
state.params.SeccompPresets |= seccomp.PresetExt
|
||||
}
|
||||
if !state.Container.Devel {
|
||||
state.params.SeccompPresets |= seccomp.PresetDenyDevel
|
||||
}
|
||||
if !state.Container.Userns {
|
||||
state.params.SeccompPresets |= seccomp.PresetDenyNS
|
||||
}
|
||||
if !state.Container.Tty {
|
||||
state.params.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if state.Container.MapRealUID {
|
||||
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 = &ops
|
||||
}
|
||||
|
||||
rootfs, filesystem, _ := resolveRoot(state.Container)
|
||||
state.filesystem = filesystem
|
||||
if rootfs != nil {
|
||||
rootfs.Apply(&state.as)
|
||||
}
|
||||
|
||||
// early mount points
|
||||
state.params.
|
||||
Proc(container.AbsFHSProc).
|
||||
Tmpfs(hst.AbsTmp, 1<<12, 0755)
|
||||
if !state.Container.Device {
|
||||
state.params.DevWritable(container.AbsFHSDev, true)
|
||||
} else {
|
||||
state.params.Bind(container.AbsFHSDev, container.AbsFHSDev, container.BindWritable|container.BindDevice)
|
||||
}
|
||||
// /dev is mounted readonly later on, this prevents /dev/shm from going readonly with it
|
||||
state.params.Tmpfs(container.AbsFHSDev.Append("shm"), 0, 01777)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// spFilesystemOp applies configured filesystems to [container.Params], excluding the optional root filesystem.
|
||||
type spFilesystemOp struct{}
|
||||
|
||||
func (s spFilesystemOp) toSystem(state *outcomeStateSys, _ *hst.Config) 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 */
|
||||
var hidePaths []string
|
||||
hidePaths = append(hidePaths, state.sc.RuntimePath.String(), state.sc.SharePath.String())
|
||||
_, systemBusAddr := dbus.Address()
|
||||
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 == container.FHSRoot {
|
||||
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([]*container.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 := 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 := container.NewAbs(hidePaths[i]); err != nil {
|
||||
var absoluteError *container.AbsoluteError
|
||||
if !errors.As(err, &absoluteError) {
|
||||
return newWithMessageError(absoluteError.Error(), absoluteError)
|
||||
}
|
||||
if absoluteError == nil {
|
||||
return newWithMessage("impossible path checking state reached")
|
||||
}
|
||||
return newWithMessage("invalid path hiding candidate " + strconv.Quote(absoluteError.Pathname))
|
||||
} else {
|
||||
state.HidePaths = append(state.HidePaths, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 state.HidePaths {
|
||||
state.params.Tmpfs(a, 1<<13, 0755)
|
||||
}
|
||||
|
||||
// no more configured paths beyond this point
|
||||
if !state.Container.Device {
|
||||
state.params.Remount(container.AbsFHSDev, syscall.MS_RDONLY)
|
||||
}
|
||||
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() == container.FHSRoot {
|
||||
// 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 container.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
|
||||
}
|
50
internal/app/spdbus.go
Normal file
50
internal/app/spdbus.go
Normal file
@ -0,0 +1,50 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// spDBusOp maintains an xdg-dbus-proxy instance for the container.
|
||||
type spDBusOp struct {
|
||||
// Whether to bind the system bus socket.
|
||||
// Populated during toSystem.
|
||||
ProxySystem bool
|
||||
}
|
||||
|
||||
func (s *spDBusOp) toSystem(state *outcomeStateSys, config *hst.Config) error {
|
||||
if config.SessionBus == nil {
|
||||
config.SessionBus = dbus.NewConfig(config.ID, true, true)
|
||||
}
|
||||
|
||||
// downstream socket paths
|
||||
sessionPath, systemPath := state.instance().Append("bus"), state.instance().Append("system_bus_socket")
|
||||
|
||||
if err := state.sys.ProxyDBus(
|
||||
config.SessionBus, config.SystemBus,
|
||||
sessionPath, systemPath,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state.sys.UpdatePerm(sessionPath, acl.Read, acl.Write)
|
||||
if config.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 := container.AbsFHSRun.Append("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
|
||||
}
|
170
internal/app/sppulse.go
Normal file
170
internal/app/sppulse.go
Normal file
@ -0,0 +1,170 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
const pulseCookieSizeMax = 1 << 8
|
||||
|
||||
// spPulseOp exports the PulseAudio server to the container.
|
||||
type spPulseOp struct {
|
||||
// PulseAudio cookie data, populated during toSystem if a cookie is present.
|
||||
Cookie *[pulseCookieSizeMax]byte
|
||||
}
|
||||
|
||||
func (s *spPulseOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
|
||||
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 newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||
}
|
||||
|
||||
if fi, err := state.k.stat(pulseSocket.String()); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
|
||||
}
|
||||
return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||
} else {
|
||||
if m := fi.Mode(); m&0o006 != 0o006 {
|
||||
return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m))
|
||||
}
|
||||
}
|
||||
|
||||
// hard link pulse socket into target-executable share
|
||||
state.sys.Link(pulseSocket, state.runtime().Append("pulse"))
|
||||
|
||||
// publish current user's pulse cookie for target user
|
||||
var paCookiePath *container.Absolute
|
||||
{
|
||||
const paLocateStep = "locate PulseAudio cookie"
|
||||
|
||||
// from environment
|
||||
if p, ok := state.k.lookupEnv("PULSE_COOKIE"); ok {
|
||||
if a, err := container.NewAbs(p); err != nil {
|
||||
return &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
// this takes precedence, do not verify whether the file is accessible
|
||||
paCookiePath = a
|
||||
goto out
|
||||
}
|
||||
}
|
||||
|
||||
// $HOME/.pulse-cookie
|
||||
if p, ok := state.k.lookupEnv("HOME"); ok {
|
||||
if a, err := container.NewAbs(p); err != nil {
|
||||
return &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
paCookiePath = a.Append(".pulse-cookie")
|
||||
}
|
||||
|
||||
if fi, err := state.k.stat(paCookiePath.String()); err != nil {
|
||||
paCookiePath = nil
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if fi.IsDir() {
|
||||
paCookiePath = nil
|
||||
} else {
|
||||
goto out
|
||||
}
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/pulse/cookie
|
||||
if p, ok := state.k.lookupEnv("XDG_CONFIG_HOME"); ok {
|
||||
if a, err := container.NewAbs(p); err != nil {
|
||||
return &hst.AppError{Step: paLocateStep, Err: err}
|
||||
} else {
|
||||
paCookiePath = a.Append("pulse", "cookie")
|
||||
}
|
||||
if fi, err := state.k.stat(paCookiePath.String()); err != nil {
|
||||
paCookiePath = nil
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
}
|
||||
// fallthrough
|
||||
} else if fi.IsDir() {
|
||||
paCookiePath = nil
|
||||
} else {
|
||||
goto out
|
||||
}
|
||||
}
|
||||
out:
|
||||
}
|
||||
|
||||
if paCookiePath != nil {
|
||||
if b, err := state.k.stat(paCookiePath.String()); err != nil {
|
||||
return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
|
||||
} else {
|
||||
if b.IsDir() {
|
||||
return &hst.AppError{Step: "read PulseAudio cookie", Err: &os.PathError{Op: "stat", Path: paCookiePath.String(), Err: syscall.EISDIR}}
|
||||
}
|
||||
if b.Size() > pulseCookieSizeMax {
|
||||
return newWithMessageError(
|
||||
fmt.Sprintf("PulseAudio cookie at %q exceeds maximum expected size", paCookiePath),
|
||||
&os.PathError{Op: "stat", Path: paCookiePath.String(), Err: syscall.ENOMEM},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var r io.ReadCloser
|
||||
if f, err := state.k.open(paCookiePath.String()); err != nil {
|
||||
return &hst.AppError{Step: "open PulseAudio cookie", Err: err}
|
||||
} else {
|
||||
r = f
|
||||
}
|
||||
|
||||
s.Cookie = new([pulseCookieSizeMax]byte)
|
||||
if n, err := r.Read(s.Cookie[:]); err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
_ = r.Close()
|
||||
return &hst.AppError{Step: "read PulseAudio cookie", Err: err}
|
||||
}
|
||||
state.msg.Verbosef("copied %d bytes from %q", n, paCookiePath)
|
||||
}
|
||||
|
||||
if err := r.Close(); err != nil {
|
||||
return &hst.AppError{Step: "close PulseAudio cookie", Err: err}
|
||||
}
|
||||
} 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.AbsTmp.Append("/pulse-cookie")
|
||||
state.env["PULSE_COOKIE"] = innerDst.String()
|
||||
state.params.Place(innerDst, s.Cookie[:])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *spPulseOp) commonPaths(state *outcomeState) (pulseRuntimeDir, pulseSocket *container.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
|
||||
}
|
44
internal/app/spruntime.go
Normal file
44
internal/app/spruntime.go
Normal file
@ -0,0 +1,44 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
// spRuntimeOp sets up XDG_RUNTIME_DIR inside the container.
|
||||
type spRuntimeOp struct{}
|
||||
|
||||
func (s spRuntimeOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
|
||||
runtimeDir, runtimeDirInst := s.commonPaths(state.outcomeState)
|
||||
state.sys.Ensure(runtimeDir, 0700)
|
||||
state.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
|
||||
state.sys.Ensure(runtimeDirInst, 0700)
|
||||
state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spRuntimeOp) toContainer(state *outcomeStateParams) error {
|
||||
const (
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
xdgSessionClass = "XDG_SESSION_CLASS"
|
||||
xdgSessionType = "XDG_SESSION_TYPE"
|
||||
)
|
||||
|
||||
state.runtimeDir = container.AbsFHSRunUser.Append(state.mapuid.String())
|
||||
state.env[xdgRuntimeDir] = state.runtimeDir.String()
|
||||
state.env[xdgSessionClass] = "user"
|
||||
state.env[xdgSessionType] = "tty"
|
||||
|
||||
_, runtimeDirInst := s.commonPaths(state.outcomeState)
|
||||
state.params.Tmpfs(container.AbsFHSRunUser, 1<<12, 0755)
|
||||
state.params.Bind(runtimeDirInst, state.runtimeDir, container.BindWritable)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spRuntimeOp) commonPaths(state *outcomeState) (runtimeDir, runtimeDirInst *container.Absolute) {
|
||||
runtimeDir = state.sc.SharePath.Append("runtime")
|
||||
runtimeDirInst = runtimeDir.Append(state.identity.String())
|
||||
return
|
||||
}
|
33
internal/app/sptmpdir.go
Normal file
33
internal/app/sptmpdir.go
Normal file
@ -0,0 +1,33 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
// spTmpdirOp sets up TMPDIR inside the container.
|
||||
type spTmpdirOp struct{}
|
||||
|
||||
func (s spTmpdirOp) toSystem(state *outcomeStateSys, _ *hst.Config) error {
|
||||
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 {
|
||||
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
|
||||
_, tmpdirInst := s.commonPaths(state.outcomeState)
|
||||
state.params.Bind(tmpdirInst, container.AbsFHSTmp, container.BindWritable)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s spTmpdirOp) commonPaths(state *outcomeState) (tmpdir, tmpdirInst *container.Absolute) {
|
||||
tmpdir = state.sc.SharePath.Append("tmpdir")
|
||||
tmpdirInst = tmpdir.Append(state.identity.String())
|
||||
return
|
||||
}
|
60
internal/app/spwayland.go
Normal file
60
internal/app/spwayland.go
Normal file
@ -0,0 +1,60 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
// spWaylandOp exports the Wayland display server to the container.
|
||||
type spWaylandOp struct {
|
||||
// Path to host wayland socket. Populated during toSystem if DirectWayland is true.
|
||||
SocketPath *container.Absolute
|
||||
|
||||
// Address to write the security-context-v1 synchronisation fd [os.File] address to.
|
||||
// Only populated for toSystem.
|
||||
sync **os.File
|
||||
}
|
||||
|
||||
func (s *spWaylandOp) toSystem(state *outcomeStateSys, config *hst.Config) error {
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath *container.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 := container.NewAbs(name); err != nil {
|
||||
socketPath = state.sc.RuntimePath.Append(name)
|
||||
} else {
|
||||
socketPath = a
|
||||
}
|
||||
|
||||
if !config.DirectWayland { // set up security-context-v1
|
||||
appID := config.ID
|
||||
if appID == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appID = "app.hakurei." + state.id.String()
|
||||
}
|
||||
// downstream socket paths
|
||||
state.sys.Wayland(s.sync, 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
|
||||
}
|
63
internal/app/spx11.go
Normal file
63
internal/app/spx11.go
Normal file
@ -0,0 +1,63 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/acl"
|
||||
)
|
||||
|
||||
var absX11SocketDir = container.AbsFHSTmp.Append(".X11-unix")
|
||||
|
||||
// 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, config *hst.Config) error {
|
||||
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 *container.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 := container.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 !config.Container.HostAbstract {
|
||||
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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user