aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/outcome/outcome.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-10-29 04:32:43 +0900
committerOphestra <cat@gensokyo.uk>2025-10-29 04:33:13 +0900
commita0b4e47acc228e48165966d8e139277b8f6f450e (patch)
treed363a6781536ce946aa445271ca1d3df587e6031 /internal/outcome/outcome.go
parenta52f7038e5a607dd0901244abb791c96deaf9c2d (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.go303
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
+}