diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 0ce3b71..c3b9d85 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -82,8 +82,7 @@ func buildCommand(out io.Writer) command.Command { passwdFunc = func() { var us string if uid, err := std.Uid(aid); err != nil { - hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:") - os.Exit(1) + fatal("cannot obtain uid from setuid wrapper:", err) } else { us = strconv.Itoa(uid) } @@ -260,11 +259,33 @@ func runApp(config *hst.Config) { rs := new(app.RunState) if sa, err := a.Seal(config); err != nil { - hlog.PrintBaseError(err, "cannot seal app:") - internal.Exit(1) + hlog.BeforeExit() + fatal("cannot seal app:", err) } else { - internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs))) + 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) { + m, ok := container.GetErrorMessage(err) + if !ok { + log.Fatal(fallback, err) + return + } + + // 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 we do not want a second error message following hsu + // TODO(ophestra): handle the hsu error here instead of relying on a magic string + if m == "\x00" { + hlog.Verbose("*"+fallback, err) + os.Exit(1) + return + } + + log.Fatal(m) +} diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 15798a2..9e1ede9 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "log" - "os" "slices" "strconv" "strings" @@ -14,7 +13,6 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" "hakurei.app/system/dbus" ) @@ -26,8 +24,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) { // get hid by querying uid of identity 0 if uid, err := std.Uid(0); err != nil { - hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:") - os.Exit(1) + fatal("cannot obtain uid from setuid wrapper:", err) } else { info.User = (uid / 10000) - 100 } diff --git a/internal/app/app.go b/internal/app/app.go index 2039213..dd34cf6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -12,6 +12,7 @@ import ( "hakurei.app/internal/sys" ) +// New returns the address of a newly initialised [App] struct. func New(ctx context.Context, os sys.State) (*App, error) { a := new(App) a.sys = os @@ -24,6 +25,7 @@ func New(ctx context.Context, os sys.State) (*App, error) { return a, err } +// MustNew calls [New] and panics if an error is returned. func MustNew(ctx context.Context, os sys.State) *App { a, err := New(ctx, os) if err != nil { @@ -32,6 +34,7 @@ func MustNew(ctx context.Context, os sys.State) *App { return a } +// An App keeps track of the hakurei container lifecycle. type App struct { outcome *Outcome @@ -46,7 +49,7 @@ func (a *App) ID() state.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.un func (a *App) String() string { if a == nil { - return "(invalid app)" + return "" } a.mu.RLock() @@ -54,12 +57,12 @@ func (a *App) String() string { if a.outcome != nil { if a.outcome.user.uid == nil { - return fmt.Sprintf("(sealed app %s with invalid uid)", a.id) + return "" } - return fmt.Sprintf("(sealed app %s as uid %s)", a.id, a.outcome.user.uid) + return fmt.Sprintf("sealed app %s as uid %s", a.id, a.outcome.user.uid) } - return fmt.Sprintf("(unsealed app %s)", a.id) + return fmt.Sprintf("unsealed app %s", a.id) } // Seal determines the [Outcome] of [hst.Config]. @@ -69,7 +72,7 @@ func (a *App) Seal(config *hst.Config) (*Outcome, error) { defer a.mu.Unlock() if a.outcome != nil { - panic("app sealed twice") + panic("attempting to seal app twice") } seal := new(Outcome) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index ab94b1c..8b54b4e 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -11,7 +11,6 @@ import ( "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" - "hakurei.app/internal/hlog" "hakurei.app/internal/sys" "hakurei.app/system" ) @@ -37,8 +36,11 @@ func TestApp(t *testing.T) { ) if !t.Run("seal", func(t *testing.T) { if sa, err := a.Seal(tc.config); err != nil { - hlog.PrintBaseError(err, "got generic error:") - t.Errorf("Seal: error = %v", err) + if s, ok := container.GetErrorMessage(err); !ok { + t.Errorf("Seal: error = %v", err) + } else { + t.Errorf("Seal: %s", s) + } return } else { gotSys, gotContainer = app.AppIParams(a, sa) diff --git a/internal/app/container.go b/internal/app/container.go index cdefe17..68ba55e 100644 --- a/internal/app/container.go +++ b/internal/app/container.go @@ -11,7 +11,6 @@ import ( "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/hst" - "hakurei.app/internal/hlog" "hakurei.app/internal/sys" "hakurei.app/system/dbus" ) @@ -23,7 +22,7 @@ const preallocateOpsCount = 1 << 5 // Note that remaining container setup must be queued by the caller. func newContainer(s *hst.ContainerConfig, os sys.State, prefix string, uid, gid *int) (*container.Params, map[string]string, error) { if s == nil { - return nil, nil, hlog.WrapErr(syscall.EBADE, "invalid container configuration") + return nil, nil, newWithMessage("invalid container configuration") } params := &container.Params{ diff --git a/internal/app/errors.go b/internal/app/errors.go index b9bd6f5..7a95508 100644 --- a/internal/app/errors.go +++ b/internal/app/errors.go @@ -4,48 +4,33 @@ import ( "errors" "log" + "hakurei.app/container" "hakurei.app/internal/hlog" ) +// 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 { - hlog.PrintBaseError(runErr, "cannot start app:") + // no process has been created + printMessageError("cannot start app:", runErr) } else { - var e *hlog.BaseError - if !hlog.AsBaseError(runErr, &e) { - log.Println("wait failed:", runErr) + if m, ok := container.GetErrorMessage(runErr); !ok { + // catch-all for unexpected errors + log.Println("run returned error:", runErr) } else { - // Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError var se *StateStoreError if !errors.As(runErr, &se) { - // does not need special handling - log.Print(e.Message()) + // this could only be returned from a shim setup failure path + log.Print(m) } else { - // inner error are either unwrapped store errors - // or joined errors returned by *appSealTx revert - // wrapped in *app.BaseError - var ej RevertCompoundError - if !errors.As(se.InnerErr, &ej) { - // does not require special handling - log.Print(e.Message()) - } else { - errs := ej.Unwrap() - - // every error here is wrapped in *app.BaseError - for _, ei := range errs { - var eb *hlog.BaseError - if !errors.As(ei, &eb) { - // unreachable - log.Println("invalid error type returned by revert:", ei) - } else { - // print inner *app.BaseError message - log.Print(eb.Message()) - } - } - } + // InnerErr is returned by c.Save(&sd, seal.ct), and are always unwrapped + printMessageError("error returned during revert:", + &FinaliseError{Step: "save process state", Err: se.InnerErr}) } } } @@ -58,43 +43,45 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) { if rs.RevertErr != nil { var stateStoreError *StateStoreError if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil { - hlog.PrintBaseError(rs.RevertErr, "generic fault during cleanup:") + printMessageError("cannot clean up:", rs.RevertErr) goto out } - if stateStoreError.Err != nil { - if len(stateStoreError.Err) == 2 { - if stateStoreError.Err[0] != nil { - if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok { - hlog.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:") + 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 joinedErrs.Unwrap() { + for _, err := range joinedErrors.Unwrap() { if err != nil { - hlog.PrintBaseError(err, "fault during revert:") + printMessageError("cannot revert:", err) } } } } - if stateStoreError.Err[1] != nil { - log.Printf("cannot close store: %v", stateStoreError.Err[1]) + 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.Err...)) + 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) + log.Printf("blind revert due to store fault: %v", stateStoreError.OpErr) } if stateStoreError.DoErr != nil { - hlog.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:") + printMessageError("state store operation unsuccessful:", stateStoreError.DoErr) } if stateStoreError.Inner && stateStoreError.InnerErr != nil { - hlog.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:") + printMessageError("cannot destroy state entry:", stateStoreError.InnerErr) } out: @@ -108,7 +95,18 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) { return } -// StateStoreError is returned for a failed state save +// TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal +func printMessageError(fallback string, err error) { + if m, ok := container.GetErrorMessage(err); ok { + if m != "\x00" { + log.Print(m) + } + } else { + log.Println(fallback, err) + } +} + +// StateStoreError is returned for a failed state save. type StateStoreError struct { // whether inner function was called Inner bool @@ -119,22 +117,23 @@ type StateStoreError struct { // stores an arbitrary store operation error OpErr error // stores arbitrary errors - Err []error + Errs []error } -// save saves arbitrary errors in [StateStoreError] once. +// save saves arbitrary errors in [StateStoreError.Errs] once. func (e *StateStoreError) save(errs ...error) { - if len(errs) == 0 || e.Err != nil { + if len(errs) == 0 || e.Errs != nil { panic("invalid call to save") } - e.Err = errs + e.Errs = errs } -func (e *StateStoreError) equiv(a ...any) error { - if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil { +// 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 hlog.WrapErrSuffix(e, a...) + return &FinaliseError{Step: step, Err: e} } } @@ -148,7 +147,7 @@ func (e *StateStoreError) Error() string { if e.OpErr != nil { return e.OpErr.Error() } - if err := errors.Join(e.Err...); err != nil { + if err := errors.Join(e.Errs...); err != nil { return err.Error() } @@ -157,7 +156,7 @@ func (e *StateStoreError) Error() string { } func (e *StateStoreError) Unwrap() (errs []error) { - errs = make([]error, 0, 3) + errs = make([]error, 0, 3+len(e.Errs)) if e.InnerErr != nil { errs = append(errs, e.InnerErr) } @@ -167,15 +166,10 @@ func (e *StateStoreError) Unwrap() (errs []error) { if e.OpErr != nil { errs = append(errs, e.OpErr) } - if err := errors.Join(e.Err...); err != nil { - errs = append(errs, err) + for _, err := range e.Errs { + if err != nil { + errs = append(errs, err) + } } return } - -// A RevertCompoundError encapsulates errors returned by -// the Revert method of [system.I]. -type RevertCompoundError interface { - Error() string - Unwrap() []error -} diff --git a/internal/app/process.go b/internal/app/process.go index b89f0fc..f67327f 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -106,7 +106,7 @@ func (seal *Outcome) Run(rs *RunState) error { }() }) storeErr.save(revertErr, store.Close()) - rs.RevertErr = storeErr.equiv("error during cleanup:") + rs.RevertErr = storeErr.equiv("clean up") }() ctx, cancel := context.WithCancel(seal.ctx) @@ -119,8 +119,7 @@ func (seal *Outcome) Run(rs *RunState) error { var e *gob.Encoder if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil { - return hlog.WrapErrSuffix(err, - "cannot create shim setup pipe:") + return &FinaliseError{Step: "create shim setup pipe", Err: err} } else { e = encoder cmd.Env = []string{ @@ -140,8 +139,7 @@ func (seal *Outcome) Run(rs *RunState) error { hlog.Verbosef("setuid helper at %s", hsuPath) hlog.Suspend() if err := cmd.Start(); err != nil { - return hlog.WrapErrSuffix(err, - "cannot start setuid wrapper:") + return &FinaliseError{Step: "start setuid wrapper", Err: err} } rs.setStart() @@ -161,14 +159,12 @@ func (seal *Outcome) Run(rs *RunState) error { case err := <-setupErr: if err != nil { hlog.Resume() - return hlog.WrapErrSuffix(err, - "cannot transmit shim config:") + return &FinaliseError{Step: "transmit shim config", Err: err} } case <-ctx.Done(): hlog.Resume() - return hlog.WrapErr(syscall.ECANCELED, - "shim setup canceled") + return newWithMessageError("shim setup canceled", syscall.ECANCELED) } // returned after blocking on waitErr @@ -225,5 +221,5 @@ func (seal *Outcome) Run(rs *RunState) error { seal.dbusMsg() } - return earlyStoreErr.equiv("cannot save process state:") + return earlyStoreErr.equiv("save process state") } diff --git a/internal/app/seal.go b/internal/app/seal.go index 047c3a7..0552a54 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -8,8 +8,8 @@ import ( "fmt" "io" "io/fs" + "net" "os" - "path" "slices" "strconv" "strings" @@ -28,35 +28,36 @@ import ( "hakurei.app/system/wayland" ) -const ( - home = "HOME" - shell = "SHELL" +// A FinaliseError is returned while finalising a [hst.Config] outcome. +type FinaliseError struct { + Step string + Err error + Msg string +} - xdgConfigHome = "XDG_CONFIG_HOME" - xdgRuntimeDir = "XDG_RUNTIME_DIR" - xdgSessionClass = "XDG_SESSION_CLASS" - xdgSessionType = "XDG_SESSION_TYPE" +func (e *FinaliseError) Error() string { return e.Err.Error() } +func (e *FinaliseError) Unwrap() error { return e.Err } +func (e *FinaliseError) Message() string { + if e.Msg != "" { + return e.Msg + } - term = "TERM" - display = "DISPLAY" + switch { + case errors.As(e.Err, new(*os.PathError)), + errors.As(e.Err, new(*os.LinkError)), + errors.As(e.Err, new(*os.SyscallError)), + errors.As(e.Err, new(*net.OpError)): + return "cannot " + e.Error() - pulseServer = "PULSE_SERVER" - pulseCookie = "PULSE_COOKIE" + default: + return "cannot " + e.Step + ": " + e.Error() + } +} - dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" - dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" -) - -var ( - ErrIdent = errors.New("invalid identity") - ErrName = errors.New("invalid username") - - ErrXDisplay = errors.New(display + " unset") - - ErrPulseCookie = errors.New("pulse cookie not present") - ErrPulseSocket = errors.New("pulse socket not present") - ErrPulseMode = errors.New("unexpected pulse socket mode") -) +func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) } +func newWithMessageError(msg string, err error) error { + return &FinaliseError{Step: "finalise", Err: err, Msg: msg} +} // An Outcome is the runnable state of a hakurei container via [hst.Config]. type Outcome struct { @@ -146,35 +147,55 @@ type hsuUser struct { } func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error { + const ( + home = "HOME" + shell = "SHELL" + + xdgConfigHome = "XDG_CONFIG_HOME" + xdgRuntimeDir = "XDG_RUNTIME_DIR" + xdgSessionClass = "XDG_SESSION_CLASS" + xdgSessionType = "XDG_SESSION_TYPE" + + term = "TERM" + display = "DISPLAY" + + pulseServer = "PULSE_SERVER" + pulseCookie = "PULSE_COOKIE" + + dbusSessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" + dbusSystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" + ) + if ctx == nil { + // unreachable panic("invalid call to finalise") } if seal.ctx != nil { + // unreachable panic("attempting to finalise twice") } seal.ctx = ctx if config == nil { - return hlog.WrapErr(syscall.EINVAL, syscall.EINVAL.Error()) + // unreachable + return newWithMessage("invalid configuration") } if config.Home == nil { - return hlog.WrapErr(os.ErrInvalid, "invalid path to home directory") + return newWithMessage("invalid path to home directory") } { // encode initial configuration for state tracking ct := new(bytes.Buffer) if err := gob.NewEncoder(ct).Encode(config); err != nil { - return hlog.WrapErrSuffix(err, - "cannot encode initial config:") + return &FinaliseError{Step: "encode initial config", Err: err} } seal.ct = ct } // allowed identity range 0 to 9999, this is checked again in hsu if config.Identity < 0 || config.Identity > 9999 { - return hlog.WrapErr(ErrIdent, - fmt.Sprintf("identity %d out of range", config.Identity)) + return newWithMessage(fmt.Sprintf("identity %d out of range", config.Identity)) } seal.user = hsuUser{ @@ -185,8 +206,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if seal.user.username == "" { seal.user.username = "chronos" } else if !isValidUsername(seal.user.username) { - return hlog.WrapErr(ErrName, - 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 { return err @@ -196,8 +216,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.user.supp = make([]string, len(config.Groups)) for i, name := range config.Groups { if g, err := sys.LookupGroup(name); err != nil { - return hlog.WrapErr(err, - fmt.Sprintf("unknown group %q", name)) + return newWithMessageError(fmt.Sprintf("unknown group %q", name), err) } else { seal.user.supp[i] = g.Gid } @@ -219,9 +238,9 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if config.Path == nil { if len(config.Args) > 0 { if p, err := sys.LookPath(config.Args[0]); err != nil { - return hlog.WrapErr(err, err.Error()) + return &FinaliseError{Step: "look up executable file", Err: err} } else if config.Path, err = container.NewAbs(p); err != nil { - return hlog.WrapErr(err, err.Error()) + return newWithMessageError(err.Error(), err) } } else { config.Path = config.Shell @@ -272,10 +291,10 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // late nil checks for pd behaviour if config.Shell == nil { - return hlog.WrapErr(syscall.EINVAL, "invalid shell path") + return newWithMessage("invalid shell path") } if config.Path == nil { - return hlog.WrapErr(syscall.EINVAL, "invalid program path") + return newWithMessage("invalid program path") } var mapuid, mapgid *stringPair[int] @@ -285,8 +304,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid) seal.waitDelay = config.Container.WaitDelay if err != nil { - return hlog.WrapErrSuffix(err, - "cannot initialise container configuration:") + return &FinaliseError{Step: "initialise container configuration", Err: err} } if len(config.Args) == 0 { config.Args = []string{config.Path.String()} @@ -390,8 +408,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if config.Enablements.Unwrap()&system.EX11 != 0 { if d, ok := sys.LookupEnv(display); !ok { - return hlog.WrapErr(ErrXDisplay, - "DISPLAY is not set") + return newWithMessage("DISPLAY is not set") } else { socketDir := container.AbsFHSTmp.Append(".X11-unix") @@ -410,8 +427,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if socketPath != nil { if _, err := sys.Stat(socketPath.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access X11 socket %q:", socketPath)) + return &FinaliseError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} } } else { seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute) @@ -435,24 +451,19 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir)) + return &FinaliseError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} } - return hlog.WrapErr(ErrPulseSocket, - 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 !errors.Is(err, fs.ErrNotExist) { - return hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket)) + return &FinaliseError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} } - return hlog.WrapErr(ErrPulseSocket, - fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir)) + return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir)) } else { if m := s.Mode(); m&0o006 != 0o006 { - return hlog.WrapErr(ErrPulseMode, - fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m) + return newWithMessage(fmt.Sprintf("unexpected permissions on %q: %s", pulseSocket, m)) } } @@ -464,15 +475,75 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.env[pulseServer] = "unix:" + innerPulseSocket.String() // publish current user's pulse cookie for target user - if src, err := discoverPulseCookie(sys); err != nil { - // not fatal - hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message())) - } else { + var paCookiePath *container.Absolute + { + const paLocateStep = "locate PulseAudio cookie" + + // from environment + if p, ok := sys.LookupEnv(pulseCookie); ok { + if a, err := container.NewAbs(p); err != nil { + return &FinaliseError{Step: paLocateStep, Err: err} + } else { + // this takes precedence, do not verify whether the file is accessible + paCookiePath = a + goto out + } + } + + // $HOME/.pulse-cookie + if p, ok := sys.LookupEnv(home); ok { + if a, err := container.NewAbs(p); err != nil { + return &FinaliseError{Step: paLocateStep, Err: err} + } else { + paCookiePath = a.Append(".pulse-cookie") + } + + if s, err := sys.Stat(paCookiePath.String()); err != nil { + paCookiePath = nil + if !errors.Is(err, fs.ErrNotExist) { + return &FinaliseError{Step: "access PulseAudio cookie", Err: err} + } + // fallthrough + } else if s.IsDir() { + paCookiePath = nil + } else { + goto out + } + } + + // $XDG_CONFIG_HOME/pulse/cookie + if p, ok := sys.LookupEnv(xdgConfigHome); ok { + if a, err := container.NewAbs(p); err != nil { + return &FinaliseError{Step: paLocateStep, Err: err} + } else { + paCookiePath = a.Append("pulse", "cookie") + } + if s, err := sys.Stat(paCookiePath.String()); err != nil { + paCookiePath = nil + if !errors.Is(err, fs.ErrNotExist) { + return &FinaliseError{Step: "access PulseAudio cookie", Err: err} + } + // fallthrough + } else if s.IsDir() { + paCookiePath = nil + } else { + goto out + } + } + out: + } + + if paCookiePath != nil { innerDst := hst.AbsTmp.Append("/pulse-cookie") seal.env[pulseCookie] = innerDst.String() var payload *[]byte seal.container.PlaceP(innerDst, &payload) - seal.sys.CopyFile(payload, src, 256, 256) + seal.sys.CopyFile(payload, paCookiePath.String(), 256, 256) + } else { + hlog.Verbose("cannot locate PulseAudio cookie (tried " + + "$PULSE_COOKIE, " + + "$XDG_CONFIG_HOME/pulse/cookie, " + + "$HOME/.pulse-cookie)") } } @@ -538,8 +609,8 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.container.Env = make([]string, 0, len(seal.env)) for k, v := range seal.env { if strings.IndexByte(k, '=') != -1 { - return hlog.WrapErr(syscall.EINVAL, - fmt.Sprintf("invalid environment variable %s", k)) + return &FinaliseError{Step: "flatten environment", Err: syscall.EINVAL, + Msg: fmt.Sprintf("invalid environment variable %s", k)} } seal.container.Env = append(seal.container.Env, k+"="+v) } @@ -552,42 +623,3 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co return nil } - -// discoverPulseCookie attempts various standard methods to discover the current user's PulseAudio authentication cookie -func discoverPulseCookie(sys sys.State) (string, error) { - if p, ok := sys.LookupEnv(pulseCookie); ok { - return p, nil - } - - // dotfile $HOME/.pulse-cookie - if p, ok := sys.LookupEnv(home); ok { - p = path.Join(p, ".pulse-cookie") - if s, err := sys.Stat(p); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return p, hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) - } - // not found, try next method - } else if !s.IsDir() { - return p, nil - } - } - - // $XDG_CONFIG_HOME/pulse/cookie - if p, ok := sys.LookupEnv(xdgConfigHome); ok { - p = path.Join(p, "pulse", "cookie") - if s, err := sys.Stat(p); err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return p, hlog.WrapErrSuffix(err, - fmt.Sprintf("cannot access PulseAudio cookie %q:", p)) - } - // not found, try next method - } else if !s.IsDir() { - return p, nil - } - } - - return "", hlog.WrapErr(ErrPulseCookie, - fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", - pulseCookie, xdgConfigHome, home)) -} diff --git a/internal/app/shim.go b/internal/app/shim.go index 960506f..5eaf833 100644 --- a/internal/app/shim.go +++ b/internal/app/shim.go @@ -155,11 +155,11 @@ func ShimMain() { } if err := z.Start(); err != nil { - hlog.PrintBaseError(err, "cannot start container:") + printMessageError("cannot start container:", err) os.Exit(1) } if err := z.Serve(); err != nil { - hlog.PrintBaseError(err, "cannot configure container:") + printMessageError("cannot configure container:", err) } if err := seccomp.Load( diff --git a/internal/hlog/errors.go b/internal/hlog/errors.go deleted file mode 100644 index d731f57..0000000 --- a/internal/hlog/errors.go +++ /dev/null @@ -1,81 +0,0 @@ -package hlog - -import ( - "fmt" - "log" - "reflect" - "strings" -) - -// baseError implements a basic error container -type baseError struct { - Err error -} - -func (e *baseError) Error() string { return e.Err.Error() } -func (e *baseError) Unwrap() error { return e.Err } - -// BaseError implements an error container with a user-facing message -type BaseError struct { - message string - baseError -} - -// Message returns a user-facing error message -func (e *BaseError) Message() string { return e.message } - -// WrapErr wraps an error with a corresponding message. -func WrapErr(err error, a ...any) error { - if err == nil { - return nil - } - return wrapErr(err, fmt.Sprintln(a...)) -} - -// WrapErrSuffix wraps an error with a corresponding message with err at the end of the message. -func WrapErrSuffix(err error, a ...any) error { - if err == nil { - return nil - } - return wrapErr(err, fmt.Sprintln(append(a, err)...)) -} - -// WrapErrFunc wraps an error with a corresponding message returned by f. -func WrapErrFunc(err error, f func(err error) string) error { - if err == nil { - return nil - } - return wrapErr(err, f(err)) -} - -func wrapErr(err error, message string) *BaseError { - return &BaseError{message, baseError{err}} -} - -var ( - baseErrorType = reflect.TypeFor[*BaseError]() -) - -func AsBaseError(err error, target **BaseError) bool { - v := reflect.ValueOf(err) - if !v.CanConvert(baseErrorType) { - return false - } - - *target = v.Convert(baseErrorType).Interface().(*BaseError) - return true -} - -func PrintBaseError(err error, fallback string) { - var e *BaseError - - if AsBaseError(err, &e) { - if msg := e.Message(); strings.TrimSpace(msg) != "" { - log.Print(msg) - return - } - Verbose("*"+fallback, err) - return - } - log.Println(fallback, err) -} diff --git a/internal/hlog/msg.go b/internal/hlog/msg.go index dd37fdb..753f642 100644 --- a/internal/hlog/msg.go +++ b/internal/hlog/msg.go @@ -2,11 +2,9 @@ package hlog type Output struct{} -func (Output) IsVerbose() bool { return Load() } -func (Output) Verbose(v ...any) { Verbose(v...) } -func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) } -func (Output) WrapErr(err error, a ...any) error { return WrapErr(err, a...) } -func (Output) PrintBaseErr(err error, fallback string) { PrintBaseError(err, fallback) } -func (Output) Suspend() { Suspend() } -func (Output) Resume() bool { return Resume() } -func (Output) BeforeExit() { BeforeExit() } +func (Output) IsVerbose() bool { return Load() } +func (Output) Verbose(v ...any) { Verbose(v...) } +func (Output) Verbosef(format string, v ...any) { Verbosef(format, v...) } +func (Output) Suspend() { Suspend() } +func (Output) Resume() bool { return Resume() } +func (Output) BeforeExit() { BeforeExit() } diff --git a/internal/sys/std.go b/internal/sys/std.go index 828cbc4..c46bdd5 100644 --- a/internal/sys/std.go +++ b/internal/sys/std.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io/fs" + "log" "os" "os/exec" "os/user" @@ -51,7 +52,14 @@ const xdgRuntimeDir = "XDG_RUNTIME_DIR" func (s *Std) Paths() hst.Paths { s.pathsOnce.Do(func() { if userid, err := GetUserID(s); err != nil { - hlog.PrintBaseError(err, "cannot obtain user id from hsu:") + // TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal + if m, ok := container.GetErrorMessage(err); ok { + if m != "\x00" { + log.Print(m) + } + } else { + log.Println("cannot obtain user id from hsu:", err) + } hlog.BeforeExit() s.Exit(1) } else { @@ -61,6 +69,16 @@ func (s *Std) Paths() hst.Paths { return s.paths } +// this is a temporary placeholder until this package is removed +type wrappedError struct { + Err error + Msg string +} + +func (e *wrappedError) Error() string { return e.Err.Error() } +func (e *wrappedError) Unwrap() error { return e.Err } +func (e *wrappedError) Message() string { return e.Msg } + func (s *Std) Uid(identity int) (int, error) { s.uidOnce.Do(func() { s.uidCopy = make(map[int]struct { @@ -103,12 +121,13 @@ func (s *Std) Uid(identity int) (int, error) { if p, u.err = cmd.Output(); u.err == nil { u.uid, u.err = strconv.Atoi(string(p)) if u.err != nil { - u.err = hlog.WrapErr(u.err, "invalid uid string from hsu") + u.err = &wrappedError{u.err, "invalid uid string from hsu"} } } else if errors.As(u.err, &exitError) && exitError != nil && exitError.ExitCode() == 1 { - u.err = hlog.WrapErr(syscall.EACCES, "") // hsu prints to stderr in this case + // hsu prints an error message in this case + u.err = &wrappedError{syscall.EACCES, "\x00"} // this drops the message, handled in cmd/hakurei/command.go } else if os.IsNotExist(u.err) { - u.err = hlog.WrapErr(os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", hsuPath)) + u.err = &wrappedError{os.ErrNotExist, fmt.Sprintf("the setuid helper is missing: %s", hsuPath)} } return u.uid, u.err }