fortify/internal/app/process.go
Ophestra c47ce10061
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 1m52s
Test / Fortify (push) Successful in 2m45s
Test / Sandbox (race detector) (push) Successful in 2m52s
Test / Fpkg (push) Successful in 3m38s
Test / Fortify (race detector) (push) Successful in 4m14s
Test / Flake checks (push) Successful in 1m6s
app: improve shim process management
This ensures SIGKILL gets delivered to the process instead of relying on the parent exiting.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-04-05 00:09:13 +09:00

224 lines
5.9 KiB
Go

package app
import (
"context"
"encoding/gob"
"errors"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"git.gensokyo.uk/security/fortify/fst"
"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/sandbox"
"git.gensokyo.uk/security/fortify/system"
)
const shimSetupTimeout = 5 * time.Second
func (seal *outcome) Run(rs *fst.RunState) error {
// there is some kind of action at a distance caused by scheduling if this is not also set
// causing xdg-dbus-proxy and potentially other helpers spuriously getting sent SIGKILL
runtime.LockOSThread()
if !seal.f.CompareAndSwap(false, true) {
// run does much more than just starting a process; calling it twice, even if the first call fails, will result
// in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the
// other Run a chance to return
panic("attempted to run twice")
}
if rs == nil {
panic("invalid state")
}
// read comp value early to allow for early failure
fsuPath := internal.MustFsuPath()
if err := seal.sys.Commit(seal.ctx); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath)
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
defer func() {
var revertErr error
storeErr := new(StateStoreError)
storeErr.Inner, storeErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
revertErr = func() error {
storeErr.InnerErr = deferredStoreFunc(c)
var rt system.Enablement
ec := system.Process
if states, err := c.Load(); err != nil {
// revert per-process state here to limit damage
storeErr.OpErr = err
return seal.sys.Revert((*system.Criteria)(&ec))
} else {
if l := len(states); l == 0 {
ec |= system.User
} else {
fmsg.Verbosef("found %d instances, cleaning up without user-scoped operations", 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)
}
}
}
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
if fmsg.Load() {
if ec > 0 {
fmsg.Verbose("reverting operations scope", system.TypeString(ec))
}
}
return seal.sys.Revert((*system.Criteria)(&ec))
}()
})
storeErr.save(revertErr, store.Close())
rs.RevertErr = storeErr.equiv("error during cleanup:")
}()
ctx, cancel := context.WithCancel(seal.ctx)
defer cancel()
cmd := exec.CommandContext(ctx, fsuPath)
cmd.SysProcAttr = new(syscall.SysProcAttr)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Dir = "/" // container init enters final working directory
var encoder *gob.Encoder
if fd, e, err := sandbox.Setup(&cmd.ExtraFiles); err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot create shim setup pipe:")
} else {
encoder = e
cmd.Env = []string{
// passed through to shim by fsu
shimEnv + "=" + strconv.Itoa(fd),
// interpreted by fsu
"FORTIFY_APP_ID=" + seal.user.aid.String(),
}
}
if len(seal.user.supp) > 0 {
fmsg.Verbosef("attaching supplementary group ids %s", seal.user.supp)
// interpreted by fsu
cmd.Env = append(cmd.Env, "FORTIFY_GROUPS="+strings.Join(seal.user.supp, " "))
}
{
startErr := make(chan error, 1)
go func() {
runtime.LockOSThread()
fmsg.Verbosef("setuid helper at %s", fsuPath)
fmsg.Suspend()
killShim := make(chan struct{})
// the parent-death signal is delivered despite the child having a different uid
cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
cmd.Cancel = func() error { close(killShim); return nil }
startErr <- fmsg.WrapErrorSuffix(cmd.Start(),
"cannot start setuid wrapper:")
<-killShim
}()
if err := <-startErr; err != nil {
return err
}
rs.StoreTime(time.Now().UTC())
}
waitErr := make(chan error, 1)
go func() { waitErr <- cmd.Wait(); cancel() }()
{
encodeErr := make(chan error, 1)
go func() {
encodeErr <- encoder.Encode(&shimParams{
Container: seal.container,
Home: seal.user.data,
Verbose: fmsg.Load(),
})
}()
select {
case err := <-encodeErr:
if err != nil {
return fmsg.WrapErrorSuffix(err,
"cannot transmit shim config:")
}
case <-time.After(shimSetupTimeout):
return fmsg.WrapError(syscall.ETIMEDOUT,
"deadline exceeded waiting for shim")
case <-ctx.Done():
return fmsg.WrapError(syscall.ECANCELED,
"shim setup canceled")
}
}
// returned after blocking on waitErr
var earlyStoreErr = new(StateStoreError)
{
// shim accepted setup payload, create process state
sd := state.State{
ID: seal.id.unwrap(),
PID: cmd.Process.Pid,
Time: *rs.Time,
}
earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
})
}
// state in store at this point, destroy defunct state entry on return
deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.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.ProcessState.ExitCode()
}
if fmsg.Load() {
fmsg.Verbosef("process %d exited with exit code %d", cmd.Process.Pid, rs.ExitCode)
}
case <-ctx.Done():
fmsg.Verbose("shim process canceled")
}
fmsg.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
return earlyStoreErr.equiv("cannot save process state:")
}