aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/process.go
blob: 6c337142c459e12be9ce42c9e6722600000e6dd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package app

import (
	"context"
	"errors"
	"log"
	"os/exec"
	"strings"
	"time"

	"git.gensokyo.uk/security/fortify/fst"
	"git.gensokyo.uk/security/fortify/internal"
	"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 (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
		panic("attempted to run twice")
	}

	if rs == nil {
		panic("invalid state")
	}

	// read comp values early to allow for early failure
	fmsg.Verbosef("version %s", internal.Version())
	fmsg.Verbosef("setuid helper at %s", internal.MustFsuPath())

	/*
		prepare/revert os state
	*/

	if err := seal.sys.Commit(seal.ctx); err != nil {
		return err
	}
	store := state.NewMulti(seal.runDirPath)
	deferredStoreFunc := func(c state.Cursor) error { return nil }
	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)

				/*
					revert app setup transaction
				*/

				rt, ec := new(system.Enablements), new(system.Criteria)
				ec.Enablements = new(system.Enablements)
				ec.Set(system.Process)
				if states, err := c.Load(); err != nil {
					// revert per-process state here to limit damage
					storeErr.OpErr = err
					return seal.sys.Revert(ec)
				} else {
					if l := len(states); l == 0 {
						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 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)
						}
					}
				}
				// 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 type", strings.Join(labels, ", "))
					}
				}

				return seal.sys.Revert(ec)
			}()
		})
		storeErr.save([]error{revertErr, store.Close()})
		rs.RevertErr = storeErr.equiv("error returned during cleanup:")
	}()

	/*
		shim process lifecycle
	*/

	waitErr := make(chan error, 1)
	cmd := new(shim.Shim)
	if startTime, err := cmd.Start(
		seal.user.aid.String(),
		seal.user.supp,
	); err != nil {
		return err
	} else {
		// whether/when the fsu process was created
		rs.Time = startTime
	}

	ctx, cancel := context.WithTimeout(seal.ctx, shimSetupTimeout)
	defer cancel()

	go func() {
		waitErr <- cmd.Unwrap().Wait()
		// cancel shim setup in case shim died before receiving payload
		cancel()
	}()

	if err := cmd.Serve(ctx, &shim.Params{
		Container: seal.container,
		Home:      seal.user.data,

		Verbose: fmsg.Load(),
	}); err != nil {
		return err
	}

	// shim accepted setup payload, create process state
	sd := state.State{
		ID:   seal.id.unwrap(),
		PID:  cmd.Unwrap().Process.Pid,
		Time: *rs.Time,
	}
	var earlyStoreErr = new(StateStoreError) // returned after blocking on waitErr
	earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.aid.unwrap(), func(c state.Cursor) {
		earlyStoreErr.InnerErr = c.Save(&sd, seal.ct)
	})
	// destroy defunct state entry
	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.Unwrap().ProcessState.ExitCode()
		}
		if fmsg.Load() {
			fmsg.Verbosef("process %d exited with exit code %d", cmd.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 := <-cmd.Fallback():
		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 <-seal.ctx.Done():
		fmsg.Verbose("alternative exit path selected")
	}

	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:")
}