internal/app: do not return from shim start
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Sandbox (push) Successful in 2m37s
Test / Hakurei (push) Successful in 3m32s
Test / Hpkg (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m37s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m20s

The whole RunState ugliness and the other horrendous error handling conditions for internal/app come from an old design proposal for maintaining all app containers under the same daemon process for a user. The proposal was ultimately rejected but the implementation remained. It is removed here to alleviate internal/app from much of its ugliness and unreadability.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-09-24 13:26:30 +09:00
parent f09133a224
commit b99c63337d
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
10 changed files with 388 additions and 509 deletions

View File

@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -52,29 +51,29 @@ func buildCommand(out io.Writer) command.Command {
{ {
var ( var (
dbusConfigSession string flagDBusConfigSession string
dbusConfigSystem string flagDBusConfigSystem string
mpris bool flagDBusMpris bool
dbusVerbose bool flagDBusVerbose bool
fid string flagID string
aid int flagIdentity int
groups command.RepeatableFlag flagGroups command.RepeatableFlag
homeDir string flagHomeDir string
userName string flagUserName string
wayland, x11, dBus, pulse bool flagWayland, flagX11, flagDBus, flagPulse bool
) )
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error { c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
// initialise config from flags // initialise config from flags
config := &hst.Config{ config := &hst.Config{
ID: fid, ID: flagID,
Args: args, Args: args,
} }
if aid < 0 || aid > 9999 { if flagIdentity < 0 || flagIdentity > 9999 {
log.Fatalf("aid %d out of range", aid) log.Fatalf("identity %d out of range", flagIdentity)
} }
// resolve home/username from os when flag is unset // resolve home/username from os when flag is unset
@ -82,13 +81,7 @@ func buildCommand(out io.Writer) command.Command {
passwd *user.User passwd *user.User
passwdOnce sync.Once passwdOnce sync.Once
passwdFunc = func() { passwdFunc = func() {
var us string us := strconv.Itoa(sys.MustUid(std, flagIdentity))
if uid, err := std.Uid(aid); err != nil {
fatal("cannot obtain uid from setuid wrapper:", err)
} else {
us = strconv.Itoa(uid)
}
if u, err := user.LookupId(us); err != nil { if u, err := user.LookupId(us); err != nil {
hlog.Verbosef("cannot look up uid %s", us) hlog.Verbosef("cannot look up uid %s", us)
passwd = &user.User{ passwd = &user.User{
@ -104,21 +97,21 @@ func buildCommand(out io.Writer) command.Command {
} }
) )
if homeDir == "os" { if flagHomeDir == "os" {
passwdOnce.Do(passwdFunc) passwdOnce.Do(passwdFunc)
homeDir = passwd.HomeDir flagHomeDir = passwd.HomeDir
} }
if userName == "chronos" { if flagUserName == "chronos" {
passwdOnce.Do(passwdFunc) passwdOnce.Do(passwdFunc)
userName = passwd.Username flagUserName = passwd.Username
} }
config.Identity = aid config.Identity = flagIdentity
config.Groups = groups config.Groups = flagGroups
config.Username = userName config.Username = flagUserName
if a, err := container.NewAbs(homeDir); err != nil { if a, err := container.NewAbs(flagHomeDir); err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
return err return err
} else { } else {
@ -126,43 +119,43 @@ func buildCommand(out io.Writer) command.Command {
} }
var e system.Enablement var e system.Enablement
if wayland { if flagWayland {
e |= system.EWayland e |= system.EWayland
} }
if x11 { if flagX11 {
e |= system.EX11 e |= system.EX11
} }
if dBus { if flagDBus {
e |= system.EDBus e |= system.EDBus
} }
if pulse { if flagPulse {
e |= system.EPulse e |= system.EPulse
} }
config.Enablements = hst.NewEnablements(e) config.Enablements = hst.NewEnablements(e)
// parse D-Bus config file from flags if applicable // parse D-Bus config file from flags if applicable
if dBus { if flagDBus {
if dbusConfigSession == "builtin" { if flagDBusConfigSession == "builtin" {
config.SessionBus = dbus.NewConfig(fid, true, mpris) config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
} else { } else {
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil { if conf, err := dbus.NewConfigFromFile(flagDBusConfigSession); err != nil {
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err) log.Fatalf("cannot load session bus proxy config from %q: %s", flagDBusConfigSession, err)
} else { } else {
config.SessionBus = conf config.SessionBus = conf
} }
} }
// system bus proxy is optional // system bus proxy is optional
if dbusConfigSystem != "nil" { if flagDBusConfigSystem != "nil" {
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil { if conf, err := dbus.NewConfigFromFile(flagDBusConfigSystem); err != nil {
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err) log.Fatalf("cannot load system bus proxy config from %q: %s", flagDBusConfigSystem, err)
} else { } else {
config.SystemBus = conf config.SystemBus = conf
} }
} }
// override log from configuration // override log from configuration
if dbusVerbose { if flagDBusVerbose {
if config.SessionBus != nil { if config.SessionBus != nil {
config.SessionBus.Log = true config.SessionBus.Log = true
} }
@ -176,39 +169,40 @@ func buildCommand(out io.Writer) command.Command {
runApp(config) runApp(config)
panic("unreachable") panic("unreachable")
}). }).
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"), Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
"Path to session bus proxy config file, or \"builtin\" for defaults"). "Path to session bus proxy config file, or \"builtin\" for defaults").
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"), Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
"Path to system bus proxy config file, or \"nil\" to disable"). "Path to system bus proxy config file, or \"nil\" to disable").
Flag(&mpris, "mpris", command.BoolFlag(false), Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
"Allow owning MPRIS D-Bus path, has no effect if custom config is available"). "Allow owning MPRIS D-Bus path, has no effect if custom config is available").
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false), Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
"Force buffered logging in the D-Bus proxy"). "Force buffered logging in the D-Bus proxy").
Flag(&fid, "id", command.StringFlag(""), Flag(&flagID, "id", command.StringFlag(""),
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier"). "Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
Flag(&aid, "a", command.IntFlag(0), Flag(&flagIdentity, "a", command.IntFlag(0),
"Application identity"). "Application identity").
Flag(nil, "g", &groups, Flag(nil, "g", &flagGroups,
"Groups inherited by all container processes"). "Groups inherited by all container processes").
Flag(&homeDir, "d", command.StringFlag("os"), Flag(&flagHomeDir, "d", command.StringFlag("os"),
"Container home directory"). "Container home directory").
Flag(&userName, "u", command.StringFlag("chronos"), Flag(&flagUserName, "u", command.StringFlag("chronos"),
"Passwd user name within sandbox"). "Passwd user name within sandbox").
Flag(&wayland, "wayland", command.BoolFlag(false), Flag(&flagWayland, "wayland", command.BoolFlag(false),
"Enable connection to Wayland via security-context-v1"). "Enable connection to Wayland via security-context-v1").
Flag(&x11, "X", command.BoolFlag(false), Flag(&flagX11, "X", command.BoolFlag(false),
"Enable direct connection to X11"). "Enable direct connection to X11").
Flag(&dBus, "dbus", command.BoolFlag(false), Flag(&flagDBus, "dbus", command.BoolFlag(false),
"Enable proxied connection to D-Bus"). "Enable proxied connection to D-Bus").
Flag(&pulse, "pulse", command.BoolFlag(false), Flag(&flagPulse, "pulse", command.BoolFlag(false),
"Enable direct connection to PulseAudio") "Enable direct connection to PulseAudio")
} }
var showFlagShort bool {
var flagShort bool
c.NewCommand("show", "Show live or local app configuration", func(args []string) error { c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
switch len(args) { switch len(args) {
case 0: // system case 0: // system
printShowSystem(os.Stdout, showFlagShort, flagJSON) printShowSystem(os.Stdout, flagShort, flagJSON)
case 1: // instance case 1: // instance
name := args[0] name := args[0]
@ -216,19 +210,22 @@ func buildCommand(out io.Writer) command.Command {
if config == nil { if config == nil {
config = tryPath(name) config = tryPath(name)
} }
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON) printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON)
default: default:
log.Fatal("show requires 1 argument") log.Fatal("show requires 1 argument")
} }
return errSuccess return errSuccess
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information") }).Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information")
}
var psFlagShort bool {
var flagShort bool
c.NewCommand("ps", "List active instances", func(args []string) error { c.NewCommand("ps", "List active instances", func(args []string) error {
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON) printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), flagShort, flagJSON)
return errSuccess return errSuccess
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id") }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}
c.Command("version", "Display version information", func(args []string) error { c.Command("version", "Display version information", func(args []string) error {
fmt.Println(internal.Version()) fmt.Println(internal.Version())
@ -259,34 +256,15 @@ func runApp(config *hst.Config) {
defer stop() // unreachable defer stop() // unreachable
a := app.MustNew(ctx, std) a := app.MustNew(ctx, std)
rs := new(app.RunState)
if sa, err := a.Seal(config); err != nil { if sa, err := a.Seal(config); err != nil {
hlog.BeforeExit() hlog.BeforeExit()
fatal("cannot seal app:", err) if m, ok := container.GetErrorMessage(err); ok {
} else {
hlog.BeforeExit()
os.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
}
*(*int)(nil) = 0 // not reached
}
// fatal prints the error message according to [container.GetErrorMessage], or fallback
// prepended to err if an error message is not available, followed by a call to [os.Exit](1).
func fatal(fallback string, err error) {
// this indicates the error message has already reached stderr, outside the current process's control;
// this is only reached when hsu fails for any reason, as a second error message following hsu is confusing
if errors.Is(err, sys.ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return
}
m, ok := container.GetErrorMessage(err)
if !ok {
log.Fatalln(fallback, err)
return
}
log.Fatal(m) log.Fatal(m)
} else {
log.Fatalln("cannot seal app:", err)
}
} else {
sa.Main()
panic("unreachable")
}
} }

View File

@ -13,6 +13,7 @@ import (
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system/dbus" "hakurei.app/system/dbus"
) )
@ -20,13 +21,9 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
t := newPrinter(output) t := newPrinter(output)
defer t.MustFlush() defer t.MustFlush()
info := &hst.Info{Paths: std.Paths()} info := &hst.Info{
Paths: std.Paths(),
// get hid by querying uid of identity 0 User: sys.MustGetUserID(std),
if uid, err := std.Uid(0); err != nil {
fatal("cannot obtain uid from setuid wrapper:", err)
} else {
info.User = (uid / 10000) - 100
} }
if flagJSON { if flagJSON {

View File

@ -1,184 +0,0 @@
package app
import (
"errors"
"log"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/hlog"
"hakurei.app/internal/sys"
)
// PrintRunStateErr prints an error message via [log] if runErr is not nil, and returns an appropriate exit code.
//
// TODO(ophestra): remove this function once RunState has been replaced
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
code = rs.ExitStatus()
if runErr != nil {
if rs.Time == nil {
// no process has been created
printMessageError("cannot start app:", runErr)
} else {
if m, ok := container.GetErrorMessage(runErr); !ok {
// catch-all for unexpected errors
log.Println("run returned error:", runErr)
} else {
var se *StateStoreError
if !errors.As(runErr, &se) {
// this could only be returned from a shim setup failure path
log.Print(m)
} else {
// InnerErr is returned by c.Save(&sd, seal.ct), and are always unwrapped
printMessageError("error returned during revert:",
&hst.AppError{Step: "save process state", Err: se.InnerErr})
}
}
}
if code == 0 {
code = 126
}
}
if rs.RevertErr != nil {
var stateStoreError *StateStoreError
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
printMessageError("cannot clean up:", rs.RevertErr)
goto out
}
if stateStoreError.Errs != nil {
if len(stateStoreError.Errs) == 2 { // storeErr.save(revertErr, store.Close())
if stateStoreError.Errs[0] != nil { // revertErr is MessageError joined by errors.Join
var joinedErrors interface {
Unwrap() []error
error
}
if !errors.As(stateStoreError.Errs[0], &joinedErrors) {
printMessageError("cannot revert:", stateStoreError.Errs[0])
} else {
for _, err := range joinedErrors.Unwrap() {
if err != nil {
printMessageError("cannot revert:", err)
}
}
}
}
if stateStoreError.Errs[1] != nil { // store.Close() is joined by errors.Join
log.Printf("cannot close store: %v", stateStoreError.Errs[1])
}
} else {
log.Printf("fault during cleanup: %v", errors.Join(stateStoreError.Errs...))
}
}
if stateStoreError.OpErr != nil {
log.Printf("blind revert due to store fault: %v", stateStoreError.OpErr)
}
if stateStoreError.DoErr != nil {
printMessageError("state store operation unsuccessful:", stateStoreError.DoErr)
}
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
printMessageError("cannot destroy state entry:", stateStoreError.InnerErr)
}
out:
if code == 0 {
code = 128
}
}
if rs.WaitErr != nil {
hlog.Verbosef("wait: %v", rs.WaitErr)
}
return
}
// TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal
func printMessageError(fallback string, err error) {
// this indicates the error message has already reached stderr, outside the current process's control;
// this is only reached when hsu fails for any reason, as a second error message following hsu is confusing
if errors.Is(err, sys.ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
return
}
m, ok := container.GetErrorMessage(err)
if !ok {
log.Println(fallback, err)
return
}
log.Print(m)
}
// StateStoreError is returned for a failed state save.
type StateStoreError struct {
// whether inner function was called
Inner bool
// returned by the Save/Destroy method of [state.Cursor]
InnerErr error
// returned by the Do method of [state.Store]
DoErr error
// stores an arbitrary store operation error
OpErr error
// stores arbitrary errors
Errs []error
}
// save saves arbitrary errors in [StateStoreError.Errs] once.
func (e *StateStoreError) save(errs ...error) {
if len(errs) == 0 || e.Errs != nil {
panic("invalid call to save")
}
e.Errs = errs
}
// equiv returns an error that [StateStoreError] is equivalent to, including nil.
func (e *StateStoreError) equiv(step string) error {
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Errs...) == nil {
return nil
} else {
return &hst.AppError{Step: step, Err: e}
}
}
func (e *StateStoreError) Error() string {
if e.Inner && e.InnerErr != nil {
return e.InnerErr.Error()
}
if e.DoErr != nil {
return e.DoErr.Error()
}
if e.OpErr != nil {
return e.OpErr.Error()
}
if err := errors.Join(e.Errs...); err != nil {
return err.Error()
}
// equiv nullifies e for values where this is reached
panic("unreachable")
}
func (e *StateStoreError) Unwrap() (errs []error) {
errs = make([]error, 0, 3+len(e.Errs))
if e.InnerErr != nil {
errs = append(errs, e.InnerErr)
}
if e.DoErr != nil {
errs = append(errs, e.DoErr)
}
if e.OpErr != nil {
errs = append(errs, e.OpErr)
}
for _, err := range e.Errs {
if err != nil {
errs = append(errs, err)
}
}
return
}

View File

@ -13,7 +13,6 @@ import (
"time" "time"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/app/state" "hakurei.app/internal/app/state"
"hakurei.app/internal/hlog" "hakurei.app/internal/hlog"
@ -23,70 +22,151 @@ import (
// duration to wait for shim to exit, after container WaitDelay has elapsed. // duration to wait for shim to exit, after container WaitDelay has elapsed.
const shimWaitTimeout = 5 * time.Second const shimWaitTimeout = 5 * time.Second
// ErrShimTimeout is returned when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed. // mainState holds persistent state bound to [Outcome.Main].
// This is different from the container failing to terminate within its timeout period, as that is enforced type mainState struct {
// by the shim. This error is instead returned when there is a lockup in shim preventing it from completing. // done is whether beforeExit has been called already.
var ErrShimTimeout = errors.New("shim did not exit") done bool
// RunState stores the outcome of a call to [Outcome.Run].
type RunState struct {
// Time is the exact point in time where the process was created. // Time is the exact point in time where the process was created.
// Location must be set to UTC. // Location must be set to UTC.
// //
// Time is nil if no process was ever created. // Time is nil if no process was ever created.
Time *time.Time Time *time.Time
// RevertErr is stored by the deferred revert call.
RevertErr error
// WaitErr is the generic error value created by the standard library.
WaitErr error
syscall.WaitStatus seal *Outcome
store state.Store
cancel context.CancelFunc
cmd *exec.Cmd
cmdWait chan error
uintptr
} }
// setStart stores the current time in [RunState] once. const (
func (rs *RunState) setStart() { // mainNeedsRevert indicates the call to Commit has succeeded.
if rs.Time != nil { mainNeedsRevert uintptr = 1 << iota
panic("attempted to store time twice") // mainNeedsDestroy indicates the instance state entry is present in the store.
} mainNeedsDestroy
now := time.Now().UTC() )
rs.Time = &now
}
// Run commits deferred system setup and starts the container. // beforeExit must be called immediately before a call to [os.Exit].
func (seal *Outcome) Run(rs *RunState) error { func (ms mainState) beforeExit(isFault bool) {
if !seal.f.CompareAndSwap(false, true) { if ms.done {
// Run does much more than just starting a process; calling it twice, even if the first call fails, will result panic("attempting to call beforeExit twice")
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the }
// other Run a chance to return ms.done = true
return errors.New("outcome: attempted to run twice") defer hlog.BeforeExit()
if isFault && ms.cancel != nil {
ms.cancel()
} }
if rs == nil { var hasErr bool
panic("invalid state") // updates hasErr but does not terminate
perror := func(err error, message string) {
hasErr = true
printMessageError("cannot "+message+":", err)
} }
exitCode := 1
// read comp value early to allow for early failure
hsuPath := internal.MustHsuPath()
if err := seal.sys.Commit(); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath.String())
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() { defer func() {
var revertErr error if hasErr {
storeErr := new(StateStoreError) os.Exit(exitCode)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) { }
revertErr = func() error { }()
storeErr.InnerErr = deferredStoreFunc(c)
// this also handles wait for a non-fault termination
if ms.cmd != nil && ms.cmdWait != nil {
waitDone := make(chan struct{})
// TODO(ophestra): enforce this limit early so it does not have to be done twice
shimTimeoutCompensated := shimWaitTimeout
if ms.seal.waitDelay > MaxShimWaitDelay {
shimTimeoutCompensated += MaxShimWaitDelay
} else {
shimTimeoutCompensated += ms.seal.waitDelay
}
// this ties waitDone to ctx with the additional compensated timeout duration
go func() { <-ms.seal.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }()
select {
case err := <-ms.cmdWait:
wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
if ok {
if v := wstatus.ExitStatus(); v != 0 {
hasErr = true
exitCode = v
}
}
if hlog.Load() {
if !ok {
if err != nil {
hlog.Verbosef("wait: %v", err)
}
} else {
switch {
case wstatus.Exited():
hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
case wstatus.CoreDump():
hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
case wstatus.Signaled():
hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
}
}
}
case <-waitDone:
hlog.Resume()
// this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
// This is different from the container failing to terminate within its timeout period, as that is enforced
// by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
}
hlog.Resume()
if ms.seal.sync != nil {
if err := ms.seal.sync.Close(); err != nil {
perror(err, "close wayland security context")
}
}
if ms.seal.dbusMsg != nil {
ms.seal.dbusMsg()
}
}
if ms.uintptr&mainNeedsRevert != 0 {
if ok, err := ms.store.Do(ms.seal.user.identity.unwrap(), func(c state.Cursor) {
if ms.uintptr&mainNeedsDestroy != 0 {
if err := c.Destroy(ms.seal.id.unwrap()); err != nil {
perror(err, "destroy state entry")
}
}
var rt system.Enablement var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil { if states, err := c.Load(); err != nil {
// it is impossible to continue from this point;
// revert per-process state here to limit damage // revert per-process state here to limit damage
storeErr.OpErr = err ec := system.Process
return seal.sys.Revert((*system.Criteria)(&ec)) if revertErr := ms.seal.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
var joinError interface {
Unwrap() []error
error
}
if !errors.As(revertErr, &joinError) || joinError == nil {
perror(revertErr, "revert system setup")
} else { } else {
for _, v := range joinError.Unwrap() {
perror(v, "revert system setup step")
}
}
}
perror(err, "load instance states")
} else {
ec := system.Process
if l := len(states); l == 0 { if l := len(states); l == 0 {
ec |= system.User ec |= system.User
} else { } else {
@ -101,7 +181,7 @@ func (seal *Outcome) Run(rs *RunState) error {
log.Printf("state entry %d does not contain config", i) log.Printf("state entry %d does not contain config", i)
} }
} }
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse) ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if hlog.Load() { if hlog.Load() {
if ec > 0 { if ec > 0 {
@ -109,27 +189,70 @@ func (seal *Outcome) Run(rs *RunState) error {
} }
} }
return seal.sys.Revert((*system.Criteria)(&ec)) if err = ms.seal.sys.Revert((*system.Criteria)(&ec)); err != nil {
}() perror(err, "revert system setup")
}) }
storeErr.save(revertErr, store.Close()) }
rs.RevertErr = storeErr.equiv("clean up") }); err != nil {
}() if ok {
perror(err, "unlock state store")
} else {
perror(err, "open state store")
}
}
} else if ms.uintptr&mainNeedsDestroy != 0 {
panic("unreachable")
}
if ms.store != nil {
if err := ms.store.Close(); err != nil {
perror(err, "close state store")
}
}
}
// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
func (ms mainState) fatal(fallback string, ferr error) {
printMessageError(fallback, ferr)
ms.beforeExit(true)
os.Exit(1)
}
// Main commits deferred system setup, runs the container, reverts changes to the system, and terminates the program.
// Main does not return.
func (seal *Outcome) Main() {
if !seal.f.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice")
}
// read comp value early for early failure
hsuPath := internal.MustHsuPath()
// ms.beforeExit required beyond this point
ms := &mainState{seal: seal}
if err := seal.sys.Commit(); err != nil {
ms.fatal("cannot commit system setup:", err)
}
ms.uintptr |= mainNeedsRevert
ms.store = state.NewMulti(seal.runDirPath.String())
ctx, cancel := context.WithCancel(seal.ctx) ctx, cancel := context.WithCancel(seal.ctx)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, hsuPath) ms.cancel = cancel
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = container.FHSRoot // container init enters final working directory ms.cmd = exec.CommandContext(ctx, hsuPath)
ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
ms.cmd.Dir = container.FHSRoot // container init enters final working directory
// shim runs in the same session as monitor; see shim.go for behaviour // shim runs in the same session as monitor; see shim.go for behaviour
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder var e *gob.Encoder
if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil { if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
return &hst.AppError{Step: "create shim setup pipe", Err: err} ms.fatal("cannot create shim setup pipe:", err)
} else { } else {
e = encoder e = encoder
cmd.Env = []string{ ms.cmd.Env = []string{
// passed through to shim by hsu // passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd), shimEnv + "=" + strconv.Itoa(fd),
// interpreted by hsu // interpreted by hsu
@ -140,19 +263,25 @@ func (seal *Outcome) Run(rs *RunState) error {
if len(seal.user.supp) > 0 { if len(seal.user.supp) > 0 {
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp) hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by hsu // interpreted by hsu
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " ")) ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
} }
hlog.Verbosef("setuid helper at %s", hsuPath) hlog.Verbosef("setuid helper at %s", hsuPath)
hlog.Suspend() hlog.Suspend()
if err := cmd.Start(); err != nil { if err := ms.cmd.Start(); err != nil {
return &hst.AppError{Step: "start setuid wrapper", Err: err} ms.fatal("cannot start setuid wrapper:", err)
} }
rs.setStart() startTime := time.Now().UTC()
ms.cmdWait = make(chan error, 1)
// this ties context back to the life of the process
go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
ms.Time = &startTime
// this prevents blocking forever on an early failure // unfortunately the I/O here cannot be directly canceled;
waitErr, setupErr := make(chan error, 1), make(chan error, 1) // the cancellation path leads to fatal in this case so that is fine
go func() { waitErr <- cmd.Wait(); cancel() }() select {
case err := <-func() (setupErr chan error) {
setupErr = make(chan error, 1)
go func() { go func() {
setupErr <- e.Encode(&shimParams{ setupErr <- e.Encode(&shimParams{
os.Getpid(), os.Getpid(),
@ -161,81 +290,51 @@ func (seal *Outcome) Run(rs *RunState) error {
hlog.Load(), hlog.Load(),
}) })
}() }()
return
select { }():
case err := <-setupErr:
if err != nil { if err != nil {
hlog.Resume() hlog.Resume()
return &hst.AppError{Step: "transmit shim config", Err: err} ms.fatal("cannot transmit shim config:", err)
} }
case <-ctx.Done(): case <-ctx.Done():
hlog.Resume() hlog.Resume()
return newWithMessageError("shim setup canceled", syscall.ECANCELED) ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
} }
// returned after blocking on waitErr
var earlyStoreErr = new(StateStoreError)
{
// shim accepted setup payload, create process state // shim accepted setup payload, create process state
sd := state.State{ if ok, err := ms.store.Do(seal.user.identity.unwrap(), func(c state.Cursor) {
if err := c.Save(&state.State{
ID: seal.id.unwrap(), ID: seal.id.unwrap(),
PID: cmd.Process.Pid, PID: ms.cmd.Process.Pid,
Time: *rs.Time, Time: *ms.Time,
}, seal.ct); err != nil {
ms.fatal("cannot save state entry:", err)
} }
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) { }); err != nil {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) if ok {
}) ms.uintptr |= mainNeedsDestroy
} ms.fatal("cannot unlock state store:", err)
// state in store at this point, destroy defunct state entry on return
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) }
waitTimeout := make(chan struct{})
// TODO(ophestra): enforce this limit early so it does not have to be done twice
shimTimeoutCompensated := shimWaitTimeout
if seal.waitDelay > MaxShimWaitDelay {
shimTimeoutCompensated += MaxShimWaitDelay
} else { } else {
shimTimeoutCompensated += seal.waitDelay ms.fatal("cannot open state store:", err)
}
go func() { <-seal.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitTimeout) }()
select {
case rs.WaitErr = <-waitErr:
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
if hlog.Load() {
switch {
case rs.Exited():
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
case rs.CoreDump():
hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
case rs.Signaled():
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
default:
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
} }
} }
// state in store at this point, destroy defunct state entry on termination
ms.uintptr |= mainNeedsDestroy
case <-waitTimeout: // beforeExit ties shim process to context
rs.WaitErr = ErrShimTimeout ms.beforeExit(false)
hlog.Resume() os.Exit(0)
// TODO(ophestra): verify this behaviour in vm tests }
log.Printf("process %d did not terminate", cmd.Process.Pid)
} // printMessageError prints the error message according to [container.GetErrorMessage],
// or fallback prepended to err if an error message is not available.
hlog.Resume() func printMessageError(fallback string, err error) {
if seal.sync != nil { m, ok := container.GetErrorMessage(err)
if err := seal.sync.Close(); err != nil { if !ok {
log.Printf("cannot close wayland security context: %v", err) log.Println(fallback, err)
} return
} }
if seal.dbusMsg != nil {
seal.dbusMsg() log.Print(m)
}
return earlyStoreErr.equiv("save process state")
} }

View File

@ -119,7 +119,7 @@ type hsuUser struct {
username string username string
} }
func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error { func (seal *Outcome) finalise(ctx context.Context, k sys.State, config *hst.Config) error {
const ( const (
home = "HOME" home = "HOME"
shell = "SHELL" shell = "SHELL"
@ -176,19 +176,17 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
home: config.Home, home: config.Home,
username: config.Username, username: config.Username,
} }
if seal.user.username == "" { if seal.user.username == "" {
seal.user.username = "chronos" seal.user.username = "chronos"
} else if !isValidUsername(seal.user.username) { } else if !isValidUsername(seal.user.username) {
return newWithMessage(fmt.Sprintf("invalid user name %q", seal.user.username)) return newWithMessage(fmt.Sprintf("invalid user name %q", seal.user.username))
} }
if u, err := sys.Uid(seal.user.identity.unwrap()); err != nil { seal.user.uid = newInt(sys.MustUid(k, seal.user.identity.unwrap()))
return err
} else {
seal.user.uid = newInt(u)
}
seal.user.supp = make([]string, len(config.Groups)) seal.user.supp = make([]string, len(config.Groups))
for i, name := range config.Groups { for i, name := range config.Groups {
if g, err := sys.LookupGroup(name); err != nil { if g, err := k.LookupGroup(name); err != nil {
return newWithMessageError(fmt.Sprintf("unknown group %q", name), err) return newWithMessageError(fmt.Sprintf("unknown group %q", name), err)
} else { } else {
seal.user.supp[i] = g.Gid seal.user.supp[i] = g.Gid
@ -201,7 +199,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
if config.Shell == nil { if config.Shell == nil {
config.Shell = container.AbsFHSRoot.Append("bin", "sh") config.Shell = container.AbsFHSRoot.Append("bin", "sh")
s, _ := sys.LookupEnv(shell) s, _ := k.LookupEnv(shell)
if a, err := container.NewAbs(s); err == nil { if a, err := container.NewAbs(s); err == nil {
config.Shell = a config.Shell = a
} }
@ -210,7 +208,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// hsu clears the environment so resolve paths early // hsu clears the environment so resolve paths early
if config.Path == nil { if config.Path == nil {
if len(config.Args) > 0 { if len(config.Args) > 0 {
if p, err := sys.LookPath(config.Args[0]); err != nil { if p, err := k.LookPath(config.Args[0]); err != nil {
return &hst.AppError{Step: "look up executable file", Err: err} return &hst.AppError{Step: "look up executable file", Err: err}
} else if config.Path, err = container.NewAbs(p); err != nil { } else if config.Path, err = container.NewAbs(p); err != nil {
return newWithMessageError(err.Error(), err) return newWithMessageError(err.Error(), err)
@ -246,7 +244,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// hide nscd from container if present // hide nscd from container if present
nscd := container.AbsFHSVar.Append("run/nscd") nscd := container.AbsFHSVar.Append("run/nscd")
if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { if _, err := k.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) {
conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}}) conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}})
} }
@ -274,7 +272,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
{ {
var uid, gid int var uid, gid int
var err error var err error
seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid) seal.container, seal.env, err = newContainer(config.Container, k, seal.id.String(), &uid, &gid)
seal.waitDelay = config.Container.WaitDelay seal.waitDelay = config.Container.WaitDelay
if err != nil { if err != nil {
return &hst.AppError{Step: "initialise container configuration", Err: err} return &hst.AppError{Step: "initialise container configuration", Err: err}
@ -298,7 +296,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
seal.env[xdgSessionClass] = "user" seal.env[xdgSessionClass] = "user"
seal.env[xdgSessionType] = "tty" seal.env[xdgSessionType] = "tty"
share := &shareHost{seal: seal, sc: sys.Paths()} share := &shareHost{seal: seal, sc: k.Paths()}
seal.runDirPath = share.sc.RunDirPath seal.runDirPath = share.sc.RunDirPath
seal.sys = system.New(seal.ctx, seal.user.uid.unwrap()) seal.sys = system.New(seal.ctx, seal.user.uid.unwrap())
seal.sys.Ensure(share.sc.SharePath.String(), 0711) seal.sys.Ensure(share.sc.SharePath.String(), 0711)
@ -342,14 +340,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// pass TERM for proper terminal I/O in initial process // pass TERM for proper terminal I/O in initial process
if t, ok := sys.LookupEnv(term); ok { if t, ok := k.LookupEnv(term); ok {
seal.env[term] = t seal.env[term] = t
} }
if config.Enablements.Unwrap()&system.EWayland != 0 { if config.Enablements.Unwrap()&system.EWayland != 0 {
// outer wayland socket (usually `/run/user/%d/wayland-%d`) // outer wayland socket (usually `/run/user/%d/wayland-%d`)
var socketPath *container.Absolute var socketPath *container.Absolute
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { if name, ok := k.LookupEnv(wayland.WaylandDisplay); !ok {
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName)
} else if a, err := container.NewAbs(name); err != nil { } else if a, err := container.NewAbs(name); err != nil {
@ -380,7 +378,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
if config.Enablements.Unwrap()&system.EX11 != 0 { if config.Enablements.Unwrap()&system.EX11 != 0 {
if d, ok := sys.LookupEnv(display); !ok { if d, ok := k.LookupEnv(display); !ok {
return newWithMessage("DISPLAY is not set") return newWithMessage("DISPLAY is not set")
} else { } else {
socketDir := container.AbsFHSTmp.Append(".X11-unix") socketDir := container.AbsFHSTmp.Append(".X11-unix")
@ -398,7 +396,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
} }
if socketPath != nil { if socketPath != nil {
if _, err := sys.Stat(socketPath.String()); err != nil { if _, err := k.Stat(socketPath.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err}
} }
@ -422,14 +420,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
// PulseAudio socket (usually `/run/user/%d/pulse/native`) // PulseAudio socket (usually `/run/user/%d/pulse/native`)
pulseSocket := pulseRuntimeDir.Append("native") pulseSocket := pulseRuntimeDir.Append("native")
if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { if _, err := k.Stat(pulseRuntimeDir.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err}
} }
return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
} }
if s, err := sys.Stat(pulseSocket.String()); err != nil { if s, err := k.Stat(pulseSocket.String()); err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err}
} }
@ -453,7 +451,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
const paLocateStep = "locate PulseAudio cookie" const paLocateStep = "locate PulseAudio cookie"
// from environment // from environment
if p, ok := sys.LookupEnv(pulseCookie); ok { if p, ok := k.LookupEnv(pulseCookie); ok {
if a, err := container.NewAbs(p); err != nil { if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err} return &hst.AppError{Step: paLocateStep, Err: err}
} else { } else {
@ -464,14 +462,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// $HOME/.pulse-cookie // $HOME/.pulse-cookie
if p, ok := sys.LookupEnv(home); ok { if p, ok := k.LookupEnv(home); ok {
if a, err := container.NewAbs(p); err != nil { if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err} return &hst.AppError{Step: paLocateStep, Err: err}
} else { } else {
paCookiePath = a.Append(".pulse-cookie") paCookiePath = a.Append(".pulse-cookie")
} }
if s, err := sys.Stat(paCookiePath.String()); err != nil { if s, err := k.Stat(paCookiePath.String()); err != nil {
paCookiePath = nil paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err} return &hst.AppError{Step: "access PulseAudio cookie", Err: err}
@ -485,13 +483,13 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co
} }
// $XDG_CONFIG_HOME/pulse/cookie // $XDG_CONFIG_HOME/pulse/cookie
if p, ok := sys.LookupEnv(xdgConfigHome); ok { if p, ok := k.LookupEnv(xdgConfigHome); ok {
if a, err := container.NewAbs(p); err != nil { if a, err := container.NewAbs(p); err != nil {
return &hst.AppError{Step: paLocateStep, Err: err} return &hst.AppError{Step: paLocateStep, Err: err}
} else { } else {
paCookiePath = a.Append("pulse", "cookie") paCookiePath = a.Append("pulse", "cookie")
} }
if s, err := sys.Stat(paCookiePath.String()); err != nil { if s, err := k.Stat(paCookiePath.String()); err != nil {
paCookiePath = nil paCookiePath = nil
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
return &hst.AppError{Step: "access PulseAudio cookie", Err: err} return &hst.AppError{Step: "access PulseAudio cookie", Err: err}

View File

@ -27,27 +27,27 @@ type multiStore struct {
lock sync.RWMutex lock sync.RWMutex
} }
func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) { func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
// load or initialise new backend // load or initialise new backend
b := new(multiBackend) b := new(multiBackend)
b.lock.Lock() b.lock.Lock()
if v, ok := s.backends.LoadOrStore(aid, b); ok { if v, ok := s.backends.LoadOrStore(identity, b); ok {
b = v.(*multiBackend) b = v.(*multiBackend)
} else { } else {
b.path = path.Join(s.base, strconv.Itoa(aid)) b.path = path.Join(s.base, strconv.Itoa(identity))
// ensure directory // ensure directory
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) { if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
s.backends.CompareAndDelete(aid, b) s.backends.CompareAndDelete(identity, b)
return false, err return false, err
} }
// open locker file // open locker file
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil { if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
s.backends.CompareAndDelete(aid, b) s.backends.CompareAndDelete(identity, b)
return false, err return false, err
} else { } else {
b.lockfile = l b.lockfile = l

View File

@ -17,7 +17,7 @@ type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns. // Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process. // Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns. // Cursor provided to f becomes invalid as soon as f returns.
Do(aid int, f func(c Cursor)) (ok bool, err error) Do(identity int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of aids known to the store. // List queries the store and returns a list of aids known to the store.
// Note that some or all returned aids might not have any active apps. // Note that some or all returned aids might not have any active apps.

View File

@ -3,6 +3,7 @@ package sys
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"strconv" "strconv"
@ -11,6 +12,7 @@ import (
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/hst" "hakurei.app/hst"
"hakurei.app/internal" "hakurei.app/internal"
"hakurei.app/internal/hlog"
) )
// Hsu caches responses from cmd/hsu. // Hsu caches responses from cmd/hsu.
@ -79,3 +81,24 @@ func (h *Hsu) Uid(identity int) (int, error) {
} }
return u.uid, u.err return u.uid, u.err
} }
// MustUid calls [State.Uid] and terminates on error.
func MustUid(s State, identity int) int {
uid, err := s.Uid(identity)
if err == nil {
return uid
}
const fallback = "cannot obtain uid from setuid wrapper:"
if errors.Is(err, ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return -0xdeadbeef
} else if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
return -0xdeadbeef
} else {
log.Fatalln(fallback, err)
return -0xdeadbeef
}
}

View File

@ -49,14 +49,8 @@ type State interface {
Uid(identity int) (int, error) Uid(identity int) (int, error)
} }
// GetUserID obtains user id from hsu by querying uid of identity 0. // MustGetUserID obtains user id from hsu by querying uid of identity 0.
func GetUserID(os State) (int, error) { func MustGetUserID(os State) int { return (MustUid(os, 0) / 10000) - 100 }
if uid, err := os.Uid(0); err != nil {
return -1, err
} else {
return (uid / 10000) - 100, nil
}
}
// CopyPaths is a generic implementation of [hst.Paths]. // CopyPaths is a generic implementation of [hst.Paths].
func CopyPaths(os State, v *hst.Paths, userid int) { func CopyPaths(os State, v *hst.Paths, userid int) {

View File

@ -1,9 +1,7 @@
package sys package sys
import ( import (
"errors"
"io/fs" "io/fs"
"log"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
@ -41,30 +39,6 @@ func (s *Std) Printf(format string, v ...any) { hlog.Verbosef(form
const xdgRuntimeDir = "XDG_RUNTIME_DIR" const xdgRuntimeDir = "XDG_RUNTIME_DIR"
func (s *Std) Paths() hst.Paths { func (s *Std) Paths() hst.Paths {
s.pathsOnce.Do(func() { s.pathsOnce.Do(func() { CopyPaths(s, &s.paths, MustGetUserID(s)) })
if userid, err := GetUserID(s); err != nil {
// TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal
hlog.BeforeExit()
const fallback = "cannot obtain user id from hsu:"
// this indicates the error message has already reached stderr, outside the current process's control;
// this is only reached when hsu fails for any reason, as a second error message following hsu is confusing
if errors.Is(err, ErrHsuAccess) {
hlog.Verbose("*"+fallback, err)
os.Exit(1)
return
}
m, ok := container.GetErrorMessage(err)
if !ok {
log.Fatalln(fallback, err)
return
}
log.Fatal(m)
} else {
CopyPaths(s, &s.paths, userid)
}
})
return s.paths return s.paths
} }