All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m37s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m34s
This makes no sense to be part of the global state. Signed-off-by: Ophestra <cat@gensokyo.uk>
284 lines
9.2 KiB
Go
284 lines
9.2 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"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
|
|
|
|
// 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) {
|
|
s.Shim = &shimParams{PrivPID: os.Getpid(), Verbose: msg.IsVerbose()}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// outcomeState 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,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|