All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (push) Successful in 1m45s
Test / Fortify (push) Successful in 2m36s
Test / Sandbox (race detector) (push) Successful in 2m49s
Test / Fpkg (push) Successful in 3m33s
Test / Fortify (race detector) (push) Successful in 4m13s
Test / Flake checks (push) Successful in 1m6s
This ensures a signal gets delivered to the process instead of relying on parent death behaviour. SIGCONT was chosen as it is the only signal an unprivileged process is allowed to send to processes with different credentials. A custom signal handler is installed because the Go runtime does not expose signal information other than which signal was received, and shim must check pid to ensure reasonable behaviour. Signed-off-by: Ophestra <cat@gensokyo.uk>
196 lines
5.6 KiB
Go
196 lines
5.6 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/gob"
|
|
"errors"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"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 shimWaitTimeout = 5 * time.Second
|
|
|
|
func (seal *outcome) Run(rs *fst.RunState) error {
|
|
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
|
|
return errors.New("outcome: 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.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
cmd.Dir = "/" // container init enters final working directory
|
|
// shim runs in the same session as monitor; see shim.go for behaviour
|
|
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
|
|
|
var e *gob.Encoder
|
|
if fd, encoder, err := sandbox.Setup(&cmd.ExtraFiles); err != nil {
|
|
return fmsg.WrapErrorSuffix(err,
|
|
"cannot create shim setup pipe:")
|
|
} else {
|
|
e = encoder
|
|
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, " "))
|
|
}
|
|
|
|
fmsg.Verbosef("setuid helper at %s", fsuPath)
|
|
fmsg.Suspend()
|
|
if err := cmd.Start(); err != nil {
|
|
return fmsg.WrapErrorSuffix(err,
|
|
"cannot start setuid wrapper:")
|
|
}
|
|
rs.SetStart()
|
|
|
|
// this prevents blocking forever on an early failure
|
|
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
|
|
go func() { waitErr <- cmd.Wait(); cancel() }()
|
|
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, fmsg.Load()}) }()
|
|
|
|
select {
|
|
case err := <-setupErr:
|
|
if err != nil {
|
|
fmsg.Resume()
|
|
return fmsg.WrapErrorSuffix(err,
|
|
"cannot transmit shim config:")
|
|
}
|
|
|
|
case <-ctx.Done():
|
|
fmsg.Resume()
|
|
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()) }
|
|
|
|
waitTimeout := make(chan struct{})
|
|
go func() { <-seal.ctx.Done(); time.Sleep(shimWaitTimeout); close(waitTimeout) }()
|
|
|
|
select {
|
|
case rs.WaitErr = <-waitErr:
|
|
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
|
|
if fmsg.Load() {
|
|
switch {
|
|
case rs.Exited():
|
|
fmsg.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
|
|
case rs.CoreDump():
|
|
fmsg.Verbosef("process %d dumped core", cmd.Process.Pid)
|
|
case rs.Signaled():
|
|
fmsg.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
|
|
default:
|
|
fmsg.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
|
|
}
|
|
}
|
|
case <-waitTimeout:
|
|
rs.WaitErr = syscall.ETIMEDOUT
|
|
fmsg.Resume()
|
|
log.Printf("process %d did not terminate", cmd.Process.Pid)
|
|
}
|
|
|
|
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:")
|
|
}
|