Just returning an error after a successful call of commit will leave garbage behind with no way for the caller to clean them. This change ensures revert is always called after successful commit with at least per-process state enabled. Signed-off-by: Ophestra <cat@gensokyo.uk>
277 lines
6.8 KiB
Go
277 lines
6.8 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.gensokyo.uk/security/fortify/fst"
|
|
"git.gensokyo.uk/security/fortify/helper"
|
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
"git.gensokyo.uk/security/fortify/internal/state"
|
|
"git.gensokyo.uk/security/fortify/system"
|
|
)
|
|
|
|
const shimSetupTimeout = 5 * time.Second
|
|
|
|
func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
if rs == nil {
|
|
panic("attempted to pass nil state to run")
|
|
}
|
|
|
|
/*
|
|
resolve exec paths
|
|
*/
|
|
|
|
shimExec := [2]string{helper.BubblewrapName}
|
|
if len(a.appSeal.command) > 0 {
|
|
shimExec[1] = a.appSeal.command[0]
|
|
}
|
|
for i, n := range shimExec {
|
|
if len(n) == 0 {
|
|
continue
|
|
}
|
|
if filepath.Base(n) == n {
|
|
if s, err := exec.LookPath(n); err == nil {
|
|
shimExec[i] = s
|
|
} else {
|
|
return fmsg.WrapError(err,
|
|
fmt.Sprintf("executable file %q not found in $PATH", n))
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
prepare/revert os state
|
|
*/
|
|
|
|
if err := a.appSeal.sys.Commit(ctx); err != nil {
|
|
return err
|
|
}
|
|
store := state.NewMulti(a.sys.Paths().RunDirPath)
|
|
deferredStoreFunc := func(c state.Cursor) error { return nil }
|
|
defer func() {
|
|
var revertErr error
|
|
storeErr := new(StateStoreError)
|
|
storeErr.Inner, storeErr.DoErr = store.Do(a.appSeal.user.aid.unwrap(), func(c state.Cursor) {
|
|
revertErr = func() error {
|
|
storeErr.InnerErr = deferredStoreFunc(c)
|
|
|
|
/*
|
|
revert app setup transaction
|
|
*/
|
|
|
|
rt, ec := new(system.Enablements), new(system.Criteria)
|
|
ec.Enablements = new(system.Enablements)
|
|
ec.Set(system.Process)
|
|
if states, err := c.Load(); err != nil {
|
|
// revert per-process state here to limit damage
|
|
return errors.Join(err, a.appSeal.sys.Revert(ec))
|
|
} else {
|
|
if l := len(states); l == 0 {
|
|
fmsg.Verbose("no other launchers active, will clean up globals")
|
|
ec.Set(system.User)
|
|
} else {
|
|
fmsg.Verbosef("found %d active launchers, cleaning up without globals", l)
|
|
}
|
|
|
|
// accumulate enablements of remaining launchers
|
|
for i, s := range states {
|
|
if s.Config != nil {
|
|
*rt |= s.Config.Confinement.Enablements
|
|
} else {
|
|
log.Printf("state entry %d does not contain config", i)
|
|
}
|
|
}
|
|
}
|
|
// invert accumulated enablements for cleanup
|
|
for i := system.Enablement(0); i < system.Enablement(system.ELen); i++ {
|
|
if !rt.Has(i) {
|
|
ec.Set(i)
|
|
}
|
|
}
|
|
if fmsg.Load() {
|
|
labels := make([]string, 0, system.ELen+1)
|
|
for i := system.Enablement(0); i < system.Enablement(system.ELen+2); i++ {
|
|
if ec.Has(i) {
|
|
labels = append(labels, system.TypeString(i))
|
|
}
|
|
}
|
|
if len(labels) > 0 {
|
|
fmsg.Verbose("reverting operations type", strings.Join(labels, ", "))
|
|
}
|
|
}
|
|
|
|
err := a.appSeal.sys.Revert(ec)
|
|
if err != nil {
|
|
err = err.(RevertCompoundError)
|
|
}
|
|
return err
|
|
}()
|
|
})
|
|
storeErr.Err = errors.Join(revertErr, store.Close())
|
|
rs.RevertErr = storeErr.equiv("error returned during cleanup:")
|
|
}()
|
|
|
|
/*
|
|
shim process lifecycle
|
|
*/
|
|
|
|
waitErr := make(chan error, 1)
|
|
cmd := new(shim.Shim)
|
|
if startTime, err := cmd.Start(
|
|
a.appSeal.user.aid.String(),
|
|
a.appSeal.user.supp,
|
|
a.appSeal.bwrapSync,
|
|
); err != nil {
|
|
return err
|
|
} else {
|
|
// whether/when the fsu process was created
|
|
rs.Time = startTime
|
|
}
|
|
|
|
shimSetupCtx, shimSetupCancel := context.WithDeadline(ctx, time.Now().Add(shimSetupTimeout))
|
|
defer shimSetupCancel()
|
|
|
|
go func() {
|
|
waitErr <- cmd.Unwrap().Wait()
|
|
// cancel shim setup in case shim died before receiving payload
|
|
shimSetupCancel()
|
|
}()
|
|
|
|
if err := cmd.Serve(shimSetupCtx, &shim.Payload{
|
|
Argv: a.appSeal.command,
|
|
Exec: shimExec,
|
|
Bwrap: a.appSeal.container,
|
|
Home: a.appSeal.user.data,
|
|
|
|
Verbose: fmsg.Load(),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// shim accepted setup payload, create process state
|
|
sd := state.State{
|
|
ID: a.id.unwrap(),
|
|
PID: cmd.Unwrap().Process.Pid,
|
|
Time: *rs.Time,
|
|
}
|
|
var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
|
|
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(a.appSeal.user.aid.unwrap(), func(c state.Cursor) { earlyStoreErr.InnerErr = c.Save(&sd, a.appSeal.ct) })
|
|
// destroy defunct state entry
|
|
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(a.id.unwrap()) }
|
|
|
|
select {
|
|
case err := <-waitErr: // block until fsu/shim returns
|
|
if err != nil {
|
|
var exitError *exec.ExitError
|
|
if !errors.As(err, &exitError) {
|
|
// should be unreachable
|
|
rs.WaitErr = err
|
|
}
|
|
|
|
// store non-zero return code
|
|
rs.ExitCode = exitError.ExitCode()
|
|
} else {
|
|
rs.ExitCode = cmd.Unwrap().ProcessState.ExitCode()
|
|
}
|
|
if fmsg.Load() {
|
|
fmsg.Verbosef("process %d exited with exit code %d", cmd.Unwrap().Process.Pid, rs.ExitCode)
|
|
}
|
|
|
|
// this is reached when a fault makes an already running shim impossible to continue execution
|
|
// however a kill signal could not be delivered (should actually always happen like that since fsu)
|
|
// the effects of this is similar to the alternative exit path and ensures shim death
|
|
case err := <-cmd.WaitFallback():
|
|
rs.ExitCode = 255
|
|
log.Printf("cannot terminate shim on faulted setup: %v", err)
|
|
|
|
// alternative exit path relying on shim behaviour on monitor process exit
|
|
case <-ctx.Done():
|
|
fmsg.Verbose("alternative exit path selected")
|
|
}
|
|
|
|
fmsg.Resume()
|
|
if a.appSeal.dbusMsg != nil {
|
|
// dump dbus message buffer
|
|
a.appSeal.dbusMsg()
|
|
}
|
|
|
|
return earlyStoreErr.equiv("cannot save process state:")
|
|
}
|
|
|
|
// StateStoreError is returned for a failed state save
|
|
type StateStoreError struct {
|
|
// whether inner function was called
|
|
Inner bool
|
|
// returned by the Do method of [state.Store]
|
|
DoErr error
|
|
// returned by the Save/Destroy method of [state.Cursor]
|
|
InnerErr error
|
|
// stores an arbitrary error
|
|
Err error
|
|
}
|
|
|
|
// save saves exactly one arbitrary error in [StateStoreError].
|
|
func (e *StateStoreError) save(err error) {
|
|
if err == nil || e.Err != nil {
|
|
panic("invalid call to save")
|
|
}
|
|
e.Err = err
|
|
}
|
|
|
|
func (e *StateStoreError) equiv(a ...any) error {
|
|
if e.Inner && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
|
|
return nil
|
|
} else {
|
|
return fmsg.WrapErrorSuffix(e, a...)
|
|
}
|
|
}
|
|
|
|
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.Err != nil {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// equiv nullifies e for values where this is reached
|
|
panic("unreachable")
|
|
}
|
|
|
|
func (e *StateStoreError) Unwrap() (errs []error) {
|
|
errs = make([]error, 0, 3)
|
|
if e.DoErr != nil {
|
|
errs = append(errs, e.DoErr)
|
|
}
|
|
if e.InnerErr != nil {
|
|
errs = append(errs, e.InnerErr)
|
|
}
|
|
if e.Err != nil {
|
|
errs = append(errs, e.Err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// A RevertCompoundError encapsulates errors returned by
|
|
// the Revert method of [system.I].
|
|
type RevertCompoundError interface {
|
|
Error() string
|
|
Unwrap() []error
|
|
}
|