2024-09-22 00:29:36 +09:00
|
|
|
package app
|
|
|
|
|
|
|
|
import (
|
2025-01-15 23:39:51 +09:00
|
|
|
"context"
|
2024-09-22 00:29:36 +09:00
|
|
|
"errors"
|
2024-10-11 02:01:03 +09:00
|
|
|
"fmt"
|
2025-02-16 17:26:09 +09:00
|
|
|
"log"
|
2024-09-22 00:29:36 +09:00
|
|
|
"os/exec"
|
2024-10-11 02:01:03 +09:00
|
|
|
"path/filepath"
|
2024-10-16 01:38:59 +09:00
|
|
|
"strings"
|
2025-01-15 23:39:51 +09:00
|
|
|
"time"
|
2024-09-22 00:29:36 +09:00
|
|
|
|
2025-02-18 23:05:37 +09:00
|
|
|
"git.gensokyo.uk/security/fortify/fst"
|
2024-12-20 00:20:02 +09:00
|
|
|
"git.gensokyo.uk/security/fortify/helper"
|
2025-02-16 16:28:46 +09:00
|
|
|
"git.gensokyo.uk/security/fortify/internal/app/shim"
|
2024-12-20 00:20:02 +09:00
|
|
|
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
|
|
|
"git.gensokyo.uk/security/fortify/internal/state"
|
2025-02-17 19:00:43 +09:00
|
|
|
"git.gensokyo.uk/security/fortify/system"
|
2024-09-22 00:29:36 +09:00
|
|
|
)
|
|
|
|
|
2025-01-15 23:39:51 +09:00
|
|
|
const shimSetupTimeout = 5 * time.Second
|
|
|
|
|
2025-02-18 23:05:37 +09:00
|
|
|
func (a *app) Run(ctx context.Context, rs *fst.RunState) error {
|
2025-02-19 01:10:37 +09:00
|
|
|
a.mu.Lock()
|
|
|
|
defer a.mu.Unlock()
|
2024-09-22 00:29:36 +09:00
|
|
|
|
2025-01-15 23:39:51 +09:00
|
|
|
if rs == nil {
|
|
|
|
panic("attempted to pass nil state to run")
|
|
|
|
}
|
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
/*
|
|
|
|
resolve exec paths
|
|
|
|
*/
|
|
|
|
|
2024-11-02 03:03:44 +09:00
|
|
|
shimExec := [2]string{helper.BubblewrapName}
|
2025-02-19 01:10:37 +09:00
|
|
|
if len(a.appSeal.command) > 0 {
|
|
|
|
shimExec[1] = a.appSeal.command[0]
|
2024-10-11 02:01:03 +09:00
|
|
|
}
|
2024-10-14 02:27:02 +09:00
|
|
|
for i, n := range shimExec {
|
2024-10-11 02:01:03 +09:00
|
|
|
if len(n) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if filepath.Base(n) == n {
|
|
|
|
if s, err := exec.LookPath(n); err == nil {
|
2024-10-14 02:27:02 +09:00
|
|
|
shimExec[i] = s
|
2024-10-11 02:01:03 +09:00
|
|
|
} else {
|
2024-10-25 16:12:18 +09:00
|
|
|
return fmsg.WrapError(err,
|
|
|
|
fmt.Sprintf("executable file %q not found in $PATH", n))
|
2024-10-11 02:01:03 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
/*
|
|
|
|
prepare/revert os state
|
|
|
|
*/
|
|
|
|
|
2025-02-19 01:10:37 +09:00
|
|
|
if err := a.appSeal.sys.Commit(ctx); err != nil {
|
2024-10-27 00:46:15 +09:00
|
|
|
return err
|
2024-10-11 02:01:03 +09:00
|
|
|
}
|
2025-02-19 21:05:48 +09:00
|
|
|
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
|
|
|
|
*/
|
2024-10-11 02:01:03 +09:00
|
|
|
|
2025-01-15 23:39:51 +09:00
|
|
|
waitErr := make(chan error, 1)
|
2025-02-19 21:05:48 +09:00
|
|
|
cmd := new(shim.Shim)
|
|
|
|
if startTime, err := cmd.Start(
|
2025-02-19 01:36:07 +09:00
|
|
|
a.appSeal.user.aid.String(),
|
|
|
|
a.appSeal.user.supp,
|
|
|
|
a.appSeal.bwrapSync,
|
2025-01-19 18:05:53 +09:00
|
|
|
); err != nil {
|
2024-10-27 00:46:15 +09:00
|
|
|
return err
|
|
|
|
} else {
|
2025-02-19 21:05:48 +09:00
|
|
|
// whether/when the fsu process was created
|
|
|
|
rs.Time = startTime
|
|
|
|
}
|
2025-01-15 23:39:51 +09:00
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
shimSetupCtx, shimSetupCancel := context.WithDeadline(ctx, time.Now().Add(shimSetupTimeout))
|
|
|
|
defer shimSetupCancel()
|
2024-09-22 00:29:36 +09:00
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
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,
|
2024-10-28 14:21:15 +09:00
|
|
|
}
|
2025-02-19 21:05:48 +09:00
|
|
|
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()) }
|
2024-10-28 14:21:15 +09:00
|
|
|
|
2025-01-15 23:39:51 +09:00
|
|
|
select {
|
2025-02-19 21:05:48 +09:00
|
|
|
case err := <-waitErr: // block until fsu/shim returns
|
2025-01-15 23:39:51 +09:00
|
|
|
if err != nil {
|
|
|
|
var exitError *exec.ExitError
|
|
|
|
if !errors.As(err, &exitError) {
|
|
|
|
// should be unreachable
|
|
|
|
rs.WaitErr = err
|
2024-10-27 00:46:15 +09:00
|
|
|
}
|
|
|
|
|
2025-01-15 23:39:51 +09:00
|
|
|
// store non-zero return code
|
|
|
|
rs.ExitCode = exitError.ExitCode()
|
|
|
|
} else {
|
2025-02-19 21:05:48 +09:00
|
|
|
rs.ExitCode = cmd.Unwrap().ProcessState.ExitCode()
|
2025-01-15 23:39:51 +09:00
|
|
|
}
|
2025-02-16 17:26:09 +09:00
|
|
|
if fmsg.Load() {
|
2025-02-19 21:05:48 +09:00
|
|
|
fmsg.Verbosef("process %d exited with exit code %d", cmd.Unwrap().Process.Pid, rs.ExitCode)
|
2024-10-27 00:46:15 +09:00
|
|
|
}
|
2025-01-15 23:39:51 +09:00
|
|
|
|
|
|
|
// 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
|
2025-02-19 21:05:48 +09:00
|
|
|
case err := <-cmd.WaitFallback():
|
2025-01-15 23:39:51 +09:00
|
|
|
rs.ExitCode = 255
|
2025-02-16 17:26:09 +09:00
|
|
|
log.Printf("cannot terminate shim on faulted setup: %v", err)
|
2025-01-15 23:39:51 +09:00
|
|
|
|
|
|
|
// alternative exit path relying on shim behaviour on monitor process exit
|
|
|
|
case <-ctx.Done():
|
2025-02-16 17:26:09 +09:00
|
|
|
fmsg.Verbose("alternative exit path selected")
|
2024-09-22 00:29:36 +09:00
|
|
|
}
|
|
|
|
|
2024-10-27 00:46:15 +09:00
|
|
|
fmsg.Resume()
|
2025-02-19 01:10:37 +09:00
|
|
|
if a.appSeal.dbusMsg != nil {
|
2025-02-19 21:05:48 +09:00
|
|
|
// dump dbus message buffer
|
2025-02-19 01:10:37 +09:00
|
|
|
a.appSeal.dbusMsg()
|
2024-11-03 03:07:02 +09:00
|
|
|
}
|
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
return earlyStoreErr.equiv("cannot save process state:")
|
2025-01-15 23:39:51 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
// StateStoreError is returned for a failed state save
|
|
|
|
type StateStoreError struct {
|
|
|
|
// whether inner function was called
|
|
|
|
Inner bool
|
2025-02-19 21:05:48 +09:00
|
|
|
// returned by the Do method of [state.Store]
|
2025-01-15 23:39:51 +09:00
|
|
|
DoErr error
|
2025-02-19 21:05:48 +09:00
|
|
|
// returned by the Save/Destroy method of [state.Cursor]
|
2025-01-15 23:39:51 +09:00
|
|
|
InnerErr error
|
2025-02-19 21:05:48 +09:00
|
|
|
// stores an arbitrary error
|
2025-01-15 23:39:51 +09:00
|
|
|
Err error
|
|
|
|
}
|
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2025-01-15 23:39:51 +09:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
// equiv nullifies e for values where this is reached
|
|
|
|
panic("unreachable")
|
2025-01-15 23:39:51 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2025-02-19 21:05:48 +09:00
|
|
|
// A RevertCompoundError encapsulates errors returned by
|
|
|
|
// the Revert method of [system.I].
|
2025-01-15 23:39:51 +09:00
|
|
|
type RevertCompoundError interface {
|
|
|
|
Error() string
|
|
|
|
Unwrap() []error
|
2024-09-22 00:29:36 +09:00
|
|
|
}
|