diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-10-29 04:32:43 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-10-29 04:33:13 +0900 |
| commit | a0b4e47acc228e48165966d8e139277b8f6f450e (patch) | |
| tree | d363a6781536ce946aa445271ca1d3df587e6031 /internal/outcome/outcome.go | |
| parent | a52f7038e5a607dd0901244abb791c96deaf9c2d (diff) | |
internal/outcome: rename from app
This is less ambiguous, and more accurately describes the purpose of the package.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/outcome/outcome.go')
| -rw-r--r-- | internal/outcome/outcome.go | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go new file mode 100644 index 00000000..2397b725 --- /dev/null +++ b/internal/outcome/outcome.go @@ -0,0 +1,303 @@ +// Package outcome implements the outcome of the privileged and container sides of a hakurei container. +package outcome + +import ( + "errors" + "maps" + "strconv" + + "hakurei.app/container" + "hakurei.app/container/check" + "hakurei.app/hst" + "hakurei.app/internal/env" + "hakurei.app/message" + "hakurei.app/system" + "hakurei.app/system/acl" +) + +// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil. +// It should be large enough to fit all insertions by outcomeOp.toContainer. +const envAllocSize = 1 << 6 + +func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} } + +// stringPair stores a value and its string representation. +type stringPair[T comparable] struct { + v T + s string +} + +func (s *stringPair[T]) unwrap() T { return s.v } +func (s *stringPair[T]) String() string { return s.s } + +// outcomeState is copied to the shim process and available while applying outcomeOp. +// This is transmitted from the priv side to the shim, so exported fields should be kept to a minimum. +type outcomeState struct { + // Params only used by the shim process. Populated by populateEarly. + Shim *shimParams + + // Generated and accounted for by the caller. + ID *hst.ID + // Copied from ID. + id *stringPair[hst.ID] + + // Copied from the [hst.Config] field of the same name. + Identity int + // Copied from Identity. + identity *stringPair[int] + // Returned by [Hsu.MustID]. + UserID int + // Target init namespace uid resolved from UserID and identity. + uid *stringPair[int] + + // Included as part of [hst.Config], transmitted as-is unless permissive defaults. + Container *hst.ContainerConfig + + // Mapped credentials within container user namespace. + Mapuid, Mapgid int + // Copied from their respective exported values. + mapuid, mapgid *stringPair[int] + + // Copied from [EnvPaths] per-process. + sc hst.Paths + *env.Paths + + // Copied via populateLocal. + k syscallDispatcher + // Copied via populateLocal. + msg message.Msg +} + +// valid checks outcomeState to be safe for use with outcomeOp. +func (s *outcomeState) valid() bool { + return s != nil && + s.Shim.valid() && + s.ID != nil && + s.Container != nil && + s.Paths != nil +} + +// newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher. +func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *hst.Config, hsu *Hsu) *outcomeState { + s := outcomeState{ + Shim: &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()}, + ID: id, + Identity: config.Identity, + UserID: hsu.MustID(msg), + Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }), + Container: config.Container, + } + + // enforce bounds and default early + if s.Container.WaitDelay < 0 { + s.Shim.WaitDelay = 0 + } else if s.Container.WaitDelay == 0 { + s.Shim.WaitDelay = hst.WaitDelayDefault + } else if s.Container.WaitDelay > hst.WaitDelayMax { + s.Shim.WaitDelay = hst.WaitDelayMax + } else { + s.Shim.WaitDelay = s.Container.WaitDelay + } + + if s.Container.Flags&hst.FMapRealUID != 0 { + s.Mapuid, s.Mapgid = k.getuid(), k.getgid() + } else { + s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg) + } + + return &s +} + +// populateLocal populates unexported fields from transmitted exported fields. +// These fields are cheaper to recompute per-process. +func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error { + if !s.valid() || k == nil || msg == nil { + return newWithMessage("impossible outcome state reached") + } + + if s.k != nil || s.msg != nil { + panic("attempting to call populateLocal twice") + } + s.k = k + s.msg = msg + + s.id = &stringPair[hst.ID]{*s.ID, s.ID.String()} + + s.Copy(&s.sc, s.UserID) + msg.Verbosef("process share directory at %q, runtime directory at %q", s.sc.SharePath, s.sc.RunDirPath) + + s.identity = newInt(s.Identity) + s.mapuid, s.mapgid = newInt(s.Mapuid), newInt(s.Mapgid) + s.uid = newInt(HsuUid(s.UserID, s.identity.unwrap())) + + return nil +} + +// instancePath returns a path formatted for outcomeStateSys.instance. +// This method must only be called from outcomeOp.toContainer if +// outcomeOp.toSystem has already called outcomeStateSys.instance. +func (s *outcomeState) instancePath() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) } + +// runtimePath returns a path formatted for outcomeStateSys.runtime. +// This method must only be called from outcomeOp.toContainer if +// outcomeOp.toSystem has already called outcomeStateSys.runtime. +func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) } + +// outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only. +// Implementations of outcomeOp must not access fields other than sys unless explicitly stated. +type outcomeStateSys struct { + // Whether XDG_RUNTIME_DIR is used post hsu. + useRuntimeDir bool + // Process-specific directory in TMPDIR, nil if unused. + sharePath *check.Absolute + // Process-specific directory in XDG_RUNTIME_DIR, nil if unused. + runtimeSharePath *check.Absolute + + // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. + appId string + // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. + et hst.Enablement + + // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. + directWayland bool + // Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only. + extraPerms []hst.ExtraPermConfig + // Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only. + sessionBus, systemBus *hst.BusConfig + + sys *system.I + *outcomeState +} + +// newSys returns the address of a new outcomeStateSys embedding the current outcomeState. +func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys { + return &outcomeStateSys{ + appId: config.ID, et: config.Enablements.Unwrap(), + directWayland: config.DirectWayland, extraPerms: config.ExtraPerms, + sessionBus: config.SessionBus, systemBus: config.SystemBus, + sys: sys, outcomeState: s, + } +} + +// newParams returns the address of a new outcomeStateParams embedding the current outcomeState. +func (s *outcomeState) newParams() *outcomeStateParams { + stateParams := outcomeStateParams{params: new(container.Params), outcomeState: s} + if s.Container.Env == nil { + stateParams.env = make(map[string]string, envAllocSize) + } else { + stateParams.env = maps.Clone(s.Container.Env) + } + return &stateParams +} + +// ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required. +func (state *outcomeStateSys) ensureRuntimeDir() { + if state.useRuntimeDir { + return + } + state.useRuntimeDir = true + state.sys.Ensure(state.sc.RunDirPath, 0700) + state.sys.UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute) + state.sys.Ensure(state.sc.RuntimePath, 0700) // ensure this dir in case XDG_RUNTIME_DIR is unset + state.sys.UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute) +} + +// instance returns the pathname to a process-specific directory within TMPDIR. +// This directory must only hold entries bound to [system.Process]. +func (state *outcomeStateSys) instance() *check.Absolute { + if state.sharePath != nil { + return state.sharePath + } + state.sharePath = state.instancePath() + state.sys.Ephemeral(system.Process, state.sharePath, 0711) + return state.sharePath +} + +// runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR. +// This directory must only hold entries bound to [system.Process]. +func (state *outcomeStateSys) runtime() *check.Absolute { + if state.runtimeSharePath != nil { + return state.runtimeSharePath + } + state.ensureRuntimeDir() + state.runtimeSharePath = state.runtimePath() + state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700) + state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute) + return state.runtimeSharePath +} + +// outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only. +type outcomeStateParams struct { + // Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used. + params *container.Params + // Collapsed into the Env slice in [container.Params] by the final outcomeOp. + env map[string]string + + // Filesystems with the optional root sliced off if present. Populated by spParamsOp. + // Safe for use by spFilesystemOp. + filesystem []hst.FilesystemConfigJSON + + // Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid. + // Populated by spRuntimeOp. + runtimeDir *check.Absolute + + as hst.ApplyState + *outcomeState +} + +// errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission. +var errNotEnabled = errors.New("op not enabled in the configuration") + +// An outcomeOp inflicts an outcome on [system.I] and contains enough information to +// inflict it on [container.Params] in a separate process. +// An implementation of outcomeOp must store cross-process states in exported fields only. +type outcomeOp interface { + // toSystem inflicts the current outcome on [system.I] in the priv side process. + toSystem(state *outcomeStateSys) error + + // toContainer inflicts the current outcome on [container.Params] in the shim process. + // The implementation must not write to the Env field of [container.Params] as it will be overwritten + // by flattened env map. + toContainer(state *outcomeStateParams) error +} + +// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops. +// This function assumes the caller has already called the Validate method on [hst.Config] +// and checked that it returns nil. +func (state *outcomeStateSys) toSystem() error { + if state.Shim == nil || state.Shim.Ops != nil { + return newWithMessage("invalid ops state reached") + } + + ops := [...]outcomeOp{ + // must run first + &spParamsOp{}, + + &spRuntimeOp{}, + spTmpdirOp{}, + spAccountOp{}, + + // optional via enablements + &spWaylandOp{}, + &spX11Op{}, + &spPulseOp{}, + &spDBusOp{}, + + // must run last + &spFilesystemOp{}, + } + + state.Shim.Ops = make([]outcomeOp, 0, len(ops)) + for _, op := range ops { + if err := op.toSystem(state); err != nil { + // this error is used internally to exclude this outcomeOp from transmission + if errors.Is(err, errNotEnabled) { + continue + } + + return err + } + state.Shim.Ops = append(state.Shim.Ops, op) + } + return nil +} |
