diff options
Diffstat (limited to 'internal/app/finalise.go')
| -rw-r--r-- | internal/app/finalise.go | 486 |
1 files changed, 88 insertions, 398 deletions
diff --git a/internal/app/finalise.go b/internal/app/finalise.go index 39e1649c..46edc950 100644 --- a/internal/app/finalise.go +++ b/internal/app/finalise.go @@ -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 - - waitDelay time.Duration - container *container.Params - env map[string]string - sync *os.File - active atomic.Bool + sys *system.I + ctx context.Context - syscallDispatcher -} + container container.Params -// 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 + // TODO(ophestra): move this to the system op + sync *os.File - seal *outcome - sc hst.Paths -} + // Populated during outcome.finalise. + proc *finaliseProcess -// 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) -} + // Whether the current process is in outcome.main. + active atomic.Bool -// 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 + syscallDispatcher } -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" - - k.runDirPath = share.sc.RunDirPath - k.sys = system.New(k.ctx, msg, k.user.uid.unwrap()) - k.sys.Ensure(share.sc.SharePath, 0711) + s := outcomeState{ + ID: id, + Identity: config.Identity, + UserID: (&Hsu{k: k}).MustIDMsg(msg), + EnvPaths: copyPaths(k.syscallDispatcher), - { - 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) + // 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) } - { - 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): 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()) { - username := "chronos" - if k.user.username != "" { - username = k.user.username - } - k.container.Dir = k.user.home - k.env["HOME"] = k.user.home.String() - k.env["USER"] = username - k.env[shell] = config.Shell.String() + ops := []outcomeOp{ + // must run first + &spParamsOp{Path: config.Path, Args: config.Args}, - 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")) - } - - // pass TERM for proper terminal I/O in initial process - if t, ok := k.lookupEnv(term); ok { - k.env[term] = t - } + // TODO(ophestra): move this late for #8 and #9 + spFilesystemOp{}, - 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) - } else { - socketPath = a + spRuntimeOp{}, + spTmpdirOp{}, + &spAccountOp{Home: config.Home, Username: config.Username, Shell: config.Shell}, } - 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) + et := config.Enablements.Unwrap() + if et&hst.EWayland != 0 { + ops = append(ops, &spWaylandOp{sync: &k.sync}) } - } - - 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 et&hst.EX11 != 0 { + ops = append(ops, &spX11Op{}) } - } - - 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 et&hst.EPulse != 0 { + ops = append(ops, &spPulseOp{}) } - - 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)) - } + if et&hst.EDBus != 0 { + ops = append(ops, &spDBusOp{}) } - // 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 - } - } - - // $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 - } + stateSys := outcomeStateSys{sys: k.sys, outcomeState: &s} + for _, op := range ops { + if err := op.toSystem(&stateSys, config); err != nil { + return err } - - // $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) + // 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 { - 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) + stateParams.env = maps.Clone(s.Container.Env) } - - // 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 + for _, op := range ops { + if err := op.toContainer(&stateParams); 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) + // 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)} + } + 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 } |
