aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/process_linux.go
blob: 818c7d7bdf49e56c1a84b6d9552f99bf964ecd93 (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"
	"encoding/gob"
	"errors"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"syscall"
	"time"

	"hakurei.app/container"
	"hakurei.app/internal"
	"hakurei.app/internal/app/state"
	"hakurei.app/internal/hlog"
	"hakurei.app/system"
)

const shimWaitTimeout = 5 * time.Second

func (seal *outcome) Run(rs *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
	hsuPath := internal.MustHsuPath()

	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 {
						hlog.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.Enablements
						} else {
							log.Printf("state entry %d does not contain config", i)
						}
					}
				}
				ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
				if hlog.Load() {
					if ec > 0 {
						hlog.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, hsuPath)
	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 := container.Setup(&cmd.ExtraFiles); err != nil {
		return hlog.WrapErrSuffix(err,
			"cannot create shim setup pipe:")
	} else {
		e = encoder
		cmd.Env = []string{
			// passed through to shim by hsu
			shimEnv + "=" + strconv.Itoa(fd),
			// interpreted by hsu
			"HAKUREI_APP_ID=" + seal.user.aid.String(),
		}
	}

	if len(seal.user.supp) > 0 {
		hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
		// interpreted by hsu
		cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
	}

	hlog.Verbosef("setuid helper at %s", hsuPath)
	hlog.Suspend()
	if err := cmd.Start(); err != nil {
		return hlog.WrapErrSuffix(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, hlog.Load()}) }()

	select {
	case err := <-setupErr:
		if err != nil {
			hlog.Resume()
			return hlog.WrapErrSuffix(err,
				"cannot transmit shim config:")
		}

	case <-ctx.Done():
		hlog.Resume()
		return hlog.WrapErr(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 hlog.Load() {
			switch {
			case rs.Exited():
				hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
			case rs.CoreDump():
				hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
			case rs.Signaled():
				hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
			default:
				hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
			}
		}
	case <-waitTimeout:
		rs.WaitErr = syscall.ETIMEDOUT
		hlog.Resume()
		log.Printf("process %d did not terminate", cmd.Process.Pid)
	}

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