package app import ( "errors" "maps" "strconv" "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/hst" "hakurei.app/internal/app/state" "hakurei.app/message" "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 { // Params only used by the shim process. Populated by populateEarly. Shim *shimParams // 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 // Copied via populateLocal. k syscallDispatcher // Copied via populateLocal. msg message.Msg } // valid checks outcomeState to be safe for use with outcomeOp. func (s *outcomeState) valid() bool { return s != nil && s.Shim.valid() && s.ID != nil && s.Container != nil && s.EnvPaths != nil } // newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher. func newOutcomeState(k syscallDispatcher, msg message.Msg, id *state.ID, config *hst.Config, hsu *Hsu) *outcomeState { s := outcomeState{ Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()}, ID: id, Identity: config.Identity, UserID: hsu.MustIDMsg(msg), EnvPaths: copyPaths(k), Container: config.Container, } // enforce bounds and default early if s.Container.WaitDelay < 0 { s.Shim.WaitDelay = 0 } else if s.Container.WaitDelay == 0 { s.Shim.WaitDelay = hst.WaitDelayDefault } else if s.Container.WaitDelay > hst.WaitDelayMax { s.Shim.WaitDelay = hst.WaitDelayMax } else { s.Shim.WaitDelay = s.Container.WaitDelay } if s.Container.MapRealUID { s.Mapuid, s.Mapgid = k.getuid(), k.getgid() } else { s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg) } return &s } // populateLocal populates unexported fields from transmitted exported fields. // These fields are cheaper to recompute per-process. func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error { if !s.valid() || k == nil || msg == nil { return newWithMessage("impossible outcome state reached") } if s.k != nil || s.msg != nil { panic("attempting to call populateLocal twice") } s.k = k s.msg = msg s.id = &stringPair[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() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) } // runtimePath returns a path formatted for outcomeStateSys.runtime. // This method must only be called from outcomeOp.toContainer if // outcomeOp.toSystem has already called outcomeStateSys.runtime. func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) } // outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only. // Implementations of outcomeOp must not access fields other than sys unless explicitly stated. type outcomeStateSys struct { // Whether XDG_RUNTIME_DIR is used post hsu. useRuntimeDir bool // Process-specific directory in TMPDIR, nil if unused. sharePath *check.Absolute // Process-specific directory in XDG_RUNTIME_DIR, nil if unused. runtimeSharePath *check.Absolute // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. appId string // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. et hst.Enablement // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. directWayland bool // Copied header from [hst.Config]. Safe for read by spFinalOp.toSystem only. extraPerms []*hst.ExtraPermConfig // Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only. sessionBus, systemBus *hst.BusConfig sys *system.I *outcomeState } // newSys returns the address of a new outcomeStateSys embedding the current outcomeState. func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys { return &outcomeStateSys{ appId: config.ID, et: config.Enablements.Unwrap(), directWayland: config.DirectWayland, extraPerms: config.ExtraPerms, sessionBus: config.SessionBus, systemBus: config.SystemBus, sys: sys, outcomeState: s, } } // newParams returns the address of a new outcomeStateParams embedding the current outcomeState. func (s *outcomeState) newParams() *outcomeStateParams { stateParams := outcomeStateParams{params: new(container.Params), outcomeState: s} if s.Container.Env == nil { stateParams.env = make(map[string]string, envAllocSize) } else { stateParams.env = maps.Clone(s.Container.Env) } return &stateParams } // ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required. func (state *outcomeStateSys) ensureRuntimeDir() { if state.useRuntimeDir { return } state.useRuntimeDir = true state.sys.Ensure(state.sc.RunDirPath, 0700) state.sys.UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute) state.sys.Ensure(state.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset state.sys.UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute) } // instance returns the pathname to a process-specific directory within TMPDIR. // This directory must only hold entries bound to [system.Process]. func (state *outcomeStateSys) instance() *check.Absolute { if state.sharePath != nil { return state.sharePath } state.sharePath = state.instancePath() state.sys.Ephemeral(system.Process, state.sharePath, 0711) return state.sharePath } // runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR. // This directory must only hold entries bound to [system.Process]. func (state *outcomeStateSys) runtime() *check.Absolute { if state.runtimeSharePath != nil { return state.runtimeSharePath } state.ensureRuntimeDir() state.runtimeSharePath = state.runtimePath() state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700) state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute) return state.runtimeSharePath } // outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only. type outcomeStateParams struct { // Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used. params *container.Params // Collapsed into the Env slice in [container.Params] by the final outcomeOp. env map[string]string // Filesystems with the optional root sliced off if present. Populated by spParamsOp. // Safe for use by spFilesystemOp. filesystem []hst.FilesystemConfigJSON // Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid. // Populated by spRuntimeOp. runtimeDir *check.Absolute as hst.ApplyState *outcomeState } // errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission. var errNotEnabled = errors.New("op not enabled in the configuration") // An outcomeOp inflicts an outcome on [system.I] and contains enough information to // inflict it on [container.Params] in a separate process. // An implementation of outcomeOp must store cross-process states in exported fields only. type outcomeOp interface { // toSystem inflicts the current outcome on [system.I] in the priv side process. toSystem(state *outcomeStateSys) error // toContainer inflicts the current outcome on [container.Params] in the shim process. // The implementation must not write to the Env field of [container.Params] as it will be overwritten // by flattened env map. toContainer(state *outcomeStateParams) error } // toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops. // This function assumes the caller has already called the Validate method on [hst.Config] // and checked that it returns nil. func (state *outcomeStateSys) toSystem() error { if state.Shim == nil || state.Shim.Ops != nil { return newWithMessage("invalid ops state reached") } ops := [...]outcomeOp{ // must run first &spParamsOp{}, // TODO(ophestra): move this late for #8 and #9 &spFilesystemOp{}, spRuntimeOp{}, spTmpdirOp{}, spAccountOp{}, // optional via enablements &spWaylandOp{}, &spX11Op{}, &spPulseOp{}, &spDBusOp{}, spFinalOp{}, } state.Shim.Ops = make([]outcomeOp, 0, len(ops)) for _, op := range ops { if err := op.toSystem(state); err != nil { // this error is used internally to exclude this outcomeOp from transmission if errors.Is(err, errNotEnabled) { continue } return err } state.Shim.Ops = append(state.Shim.Ops, op) } return nil }