diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-04-12 10:54:24 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-04-12 10:54:24 +0900 |
| commit | 0d7c1a9a4356614f035225aeb24e66421879a99b (patch) | |
| tree | c490cfd0f75fbf0218045e277812fb5b6173d4e3 /internal/app/setuid/process.go | |
| parent | ae6f5ede1928c9d0a2d480b6a924914e8599529f (diff) | |
app: rename app implementation package
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/setuid/process.go')
| -rw-r--r-- | internal/app/setuid/process.go | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/internal/app/setuid/process.go b/internal/app/setuid/process.go new file mode 100644 index 00000000..e730225e --- /dev/null +++ b/internal/app/setuid/process.go @@ -0,0 +1,195 @@ +package setuid + +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:") +} |
