aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/process.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-02-19 12:33:51 +0900
committerOphestra <cat@gensokyo.uk>2025-02-19 12:33:51 +0900
commit2978a6f046f52861a7ac749d1e414758caab8bd5 (patch)
treeeb5b7d129197320c145ca0a8e54b9cd1bcb98fcd /internal/app/process.go
parentdfd94675236844ebcec9ea51f2547ad4fd049357 (diff)
app: separate appSeal finalise method
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/process.go')
-rw-r--r--internal/app/process.go268
1 files changed, 268 insertions, 0 deletions
diff --git a/internal/app/process.go b/internal/app/process.go
new file mode 100644
index 00000000..4cbe6168
--- /dev/null
+++ b/internal/app/process.go
@@ -0,0 +1,268 @@
+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))
+ }
+ }
+ }
+
+ // startup will go ahead, commit system setup
+ if err := a.appSeal.sys.Commit(ctx); err != nil {
+ return err
+ }
+ a.appSeal.needRevert = true
+
+ // start shim via manager
+ a.shim = new(shim.Shim)
+ waitErr := make(chan error, 1)
+ if startTime, err := a.shim.Start(
+ a.appSeal.user.aid.String(),
+ a.appSeal.user.supp,
+ a.appSeal.bwrapSync,
+ ); err != nil {
+ return err
+ } else {
+ // shim process created
+ rs.Start = true
+
+ shimSetupCtx, shimSetupCancel := context.WithDeadline(ctx, time.Now().Add(shimSetupTimeout))
+ defer shimSetupCancel()
+
+ // start waiting for shim
+ go func() {
+ waitErr <- a.shim.Unwrap().Wait()
+ // cancel shim setup in case shim died before receiving payload
+ shimSetupCancel()
+ }()
+
+ // send payload
+ if err = a.shim.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: a.shim.Unwrap().Process.Pid,
+ Time: *startTime,
+ }
+
+ // register process state
+ var err0 = new(StateStoreError)
+ err0.Inner, err0.DoErr = a.appSeal.store.Do(a.appSeal.user.aid.unwrap(), func(c state.Cursor) {
+ err0.InnerErr = c.Save(&sd, a.appSeal.ct)
+ })
+ a.appSeal.stateInStore = true
+ if err = err0.equiv("cannot save process state:"); err != nil {
+ return err
+ }
+ }
+
+ select {
+ // wait for process and resolve exit code
+ case err := <-waitErr:
+ 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 = a.shim.Unwrap().ProcessState.ExitCode()
+ }
+ if fmsg.Load() {
+ fmsg.Verbosef("process %d exited with exit code %d", a.shim.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 := <-a.shim.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")
+ }
+
+ // child process exited, resume output
+ fmsg.Resume()
+
+ // print queued up dbus messages
+ if a.appSeal.dbusMsg != nil {
+ a.appSeal.dbusMsg()
+ }
+
+ // update store and revert app setup transaction
+ e := new(StateStoreError)
+ e.Inner, e.DoErr = a.appSeal.store.Do(a.appSeal.user.aid.unwrap(), func(b state.Cursor) {
+ e.InnerErr = func() error {
+ // destroy defunct state entry
+ if cmd := a.shim.Unwrap(); cmd != nil && a.appSeal.stateInStore {
+ if err := b.Destroy(a.id.unwrap()); err != nil {
+ return err
+ }
+ }
+
+ // enablements of remaining launchers
+ rt, ec := new(system.Enablements), new(system.Criteria)
+ ec.Enablements = new(system.Enablements)
+ ec.Set(system.Process)
+ if states, err := b.Load(); err != nil {
+ return err
+ } else {
+ if l := len(states); l == 0 {
+ // cleanup globals as the final launcher
+ 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 capabilities of other 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 labelled", strings.Join(labels, ", "))
+ }
+ }
+
+ if a.appSeal.needRevert {
+ if err := a.appSeal.sys.Revert(ec); err != nil {
+ return err.(RevertCompoundError)
+ }
+ }
+
+ return nil
+ }()
+ })
+
+ e.Err = a.appSeal.store.Close()
+ return e.equiv("error returned during cleanup:", e)
+}
+
+// StateStoreError is returned for a failed state save
+type StateStoreError struct {
+ // whether inner function was called
+ Inner bool
+ // error returned by state.Store Do method
+ DoErr error
+ // error returned by state.Backend Save method
+ InnerErr error
+ // any other errors needing to be tracked
+ Err error
+}
+
+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()
+ }
+
+ return "(nil)"
+}
+
+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
+}
+
+type RevertCompoundError interface {
+ Error() string
+ Unwrap() []error
+}