hakurei/internal/app/outcome.go
Ophestra 87b5c30ef6
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m22s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m28s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Hakurei (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m29s
message: relocate from container
This package is quite useful. This change allows it to be imported without importing container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-09 05:18:19 +09:00

250 lines
7.9 KiB
Go

package app
import (
"os"
"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
// Matched paths to cover. Populated by spFilesystemOp.
HidePaths []*check.Absolute
// 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
}
// populateEarly populates exported fields via syscallDispatcher.
// This must only be called from the priv side.
func (s *outcomeState) populateEarly(k syscallDispatcher, msg message.Msg, config *hst.Config) {
s.Shim = &shimParams{PrivPID: os.Getpid(), Verbose: msg.IsVerbose(), Ops: fromConfig(config)}
// enforce bounds and default early
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
}
// 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
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() *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
}
// 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
}
// fromConfig returns a corresponding slice of outcomeOp for [hst.Config].
// This function assumes the caller has already called the Validate method on [hst.Config]
// and checked that it returns nil.
func fromConfig(config *hst.Config) (ops []outcomeOp) {
ops = []outcomeOp{
// must run first
&spParamsOp{},
// TODO(ophestra): move this late for #8 and #9
spFilesystemOp{},
spRuntimeOp{},
spTmpdirOp{},
spAccountOp{},
}
et := config.Enablements.Unwrap()
if et&hst.EWayland != 0 {
ops = append(ops, &spWaylandOp{})
}
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{})
}
ops = append(ops, spFinal{})
return
}