From 2978a6f046f52861a7ac749d1e414758caab8bd5 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 19 Feb 2025 12:33:51 +0900 Subject: app: separate appSeal finalise method Signed-off-by: Ophestra --- internal/app/app.go | 21 ++++ internal/app/process.go | 268 ++++++++++++++++++++++++++++++++++++++++++++++++ internal/app/seal.go | 40 ++------ internal/app/start.go | 268 ------------------------------------------------ 4 files changed, 300 insertions(+), 297 deletions(-) create mode 100644 internal/app/process.go delete mode 100644 internal/app/start.go diff --git a/internal/app/app.go b/internal/app/app.go index eb463924..6696e189 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,6 +6,7 @@ import ( "git.gensokyo.uk/security/fortify/fst" "git.gensokyo.uk/security/fortify/internal/app/shim" + "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/sys" ) @@ -55,3 +56,23 @@ func (a *app) String() string { return fmt.Sprintf("(unsealed app %s)", a.id) } + +func (a *app) Seal(config *fst.Config) (err error) { + a.mu.Lock() + defer a.mu.Unlock() + + if a.appSeal != nil { + panic("app sealed twice") + } + if config == nil { + return fmsg.WrapError(ErrConfig, + "attempted to seal app with nil config") + } + + seal := new(appSeal) + err = seal.finalise(a.sys, config, a.id.String()) + if err == nil { + a.appSeal = seal + } + return +} 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 +} diff --git a/internal/app/seal.go b/internal/app/seal.go index 251ee2d6..34879637 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -18,6 +18,7 @@ import ( "git.gensokyo.uk/security/fortify/internal" "git.gensokyo.uk/security/fortify/internal/fmsg" "git.gensokyo.uk/security/fortify/internal/state" + "git.gensokyo.uk/security/fortify/internal/sys" "git.gensokyo.uk/security/fortify/system" ) @@ -112,23 +113,7 @@ type sealedExtraPerm struct { ensure bool } -// Seal seals the app launch context -func (a *app) Seal(config *fst.Config) error { - a.mu.Lock() - defer a.mu.Unlock() - - if a.appSeal != nil { - panic("app sealed twice") - } - - if config == nil { - return fmsg.WrapError(ErrConfig, - "attempted to seal app with nil config") - } - - // create seal - seal := new(appSeal) - +func (seal *appSeal) finalise(sys sys.State, config *fst.Config, id string) error { // encode initial configuration for state tracking ct := new(bytes.Buffer) if err := gob.NewEncoder(ct).Encode(config); err != nil { @@ -137,11 +122,10 @@ func (a *app) Seal(config *fst.Config) error { } seal.ct = ct - // fetch system constants - seal.Paths = a.sys.Paths() + seal.Paths = sys.Paths() // pass through config values - seal.id = a.id.String() + seal.id = id seal.appID = config.ID seal.command = config.Command @@ -151,7 +135,7 @@ func (a *app) Seal(config *fst.Config) error { if config.Confinement.Sandbox != nil && config.Confinement.Sandbox.MapRealUID { // some programs fail to connect to dbus session running as a different uid, so a // separate workaround is introduced to map priv-side caller uid in namespace - mapuid = a.sys.Geteuid() + mapuid = sys.Geteuid() } seal.mapuid = newInt(mapuid) seal.innerRuntimeDir = path.Join("/run/user", seal.mapuid.String()) @@ -184,7 +168,7 @@ func (a *app) Seal(config *fst.Config) error { } // invoke fsu for full uid - if u, err := a.sys.Uid(seal.user.aid.unwrap()); err != nil { + if u, err := sys.Uid(seal.user.aid.unwrap()); err != nil { return err } else { seal.user.uid = newInt(u) @@ -193,7 +177,7 @@ func (a *app) Seal(config *fst.Config) error { // resolve supplementary group ids from names seal.user.supp = make([]string, len(config.Confinement.Groups)) for i, name := range config.Confinement.Groups { - if g, err := a.sys.LookupGroup(name); err != nil { + if g, err := sys.LookupGroup(name); err != nil { return fmsg.WrapError(err, fmt.Sprintf("unknown group %q", name)) } else { @@ -236,7 +220,7 @@ func (a *app) Seal(config *fst.Config) error { AutoEtc: true, } // bind entries in / - if d, err := a.sys.ReadDir("/"); err != nil { + if d, err := sys.ReadDir("/"); err != nil { return err } else { b := make([]*fst.FilesystemConfig, 0, len(d)) @@ -258,7 +242,7 @@ func (a *app) Seal(config *fst.Config) error { // hide nscd from sandbox if present nscd := "/var/run/nscd" - if _, err := a.sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { + if _, err := sys.Stat(nscd); !errors.Is(err, fs.ErrNotExist) { conf.Override = append(conf.Override, nscd) } // bind GPU stuff @@ -271,7 +255,7 @@ func (a *app) Seal(config *fst.Config) error { config.Confinement.Sandbox = conf } seal.directWayland = config.Confinement.Sandbox.DirectWayland - if b, err := config.Confinement.Sandbox.Bwrap(a.sys); err != nil { + if b, err := config.Confinement.Sandbox.Bwrap(sys); err != nil { return err } else { seal.container = b @@ -297,7 +281,7 @@ func (a *app) Seal(config *fst.Config) error { seal.Enablements = config.Confinement.Enablements // this method calls all share methods in sequence - if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, a.sys); err != nil { + if err := seal.setupShares([2]*dbus.Config{config.Confinement.SessionBus, config.Confinement.SystemBus}, sys); err != nil { return err } @@ -305,7 +289,5 @@ func (a *app) Seal(config *fst.Config) error { fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, command: %s", seal.user.uid, seal.user.username, config.Confinement.Groups, config.Command) - // seal app and release lock - a.appSeal = seal return nil } diff --git a/internal/app/start.go b/internal/app/start.go deleted file mode 100644 index 4cbe6168..00000000 --- a/internal/app/start.go +++ /dev/null @@ -1,268 +0,0 @@ -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 -} -- cgit v1.3.1