aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/outcome/outcome.go
blob: 3252db9c7d38e7d8402fd7472ed36d3d12e4c0db (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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// Package outcome implements the outcome of the privileged and container sides of a hakurei container.
package outcome

import (
	"errors"
	"maps"
	"strconv"

	"hakurei.app/container"
	"hakurei.app/container/check"
	"hakurei.app/hst"
	"hakurei.app/internal/acl"
	"hakurei.app/internal/env"
	"hakurei.app/internal/info"
	"hakurei.app/internal/system"
	"hakurei.app/internal/wayland"
	"hakurei.app/message"
)

// Info returns the address to a populated [hst.Info].
//
// This must not be called from within package outcome.
func Info() *hst.Info {
	hi := hst.Info{WaylandVersion: wayland.Version,
		Version: info.Version(), User: new(Hsu).MustID(nil)}
	env.CopyPaths().Copy(&hi.Paths, hi.User)
	return &hi
}

// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
// It should be large enough to fit all insertions by outcomeOp.toContainer.
const envAllocSize = 1 << 6

func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }

// stringPair stores a value and its string representation.
type stringPair[T comparable] struct {
	v T
	s string
}

func (s *stringPair[T]) unwrap() T      { return s.v }
func (s *stringPair[T]) String() string { return s.s }

// outcomeState is copied to the shim process and available while applying outcomeOp.
// This is transmitted from the priv side to the shim, so exported fields should be kept to a minimum.
type outcomeState struct {
	// Params only used by the shim process. Populated by populateEarly.
	Shim *shimParams

	// Generated and accounted for by the caller.
	ID *hst.ID
	// Copied from ID.
	id *stringPair[hst.ID]

	// Copied from the [hst.Config] field of the same name.
	Identity int
	// Copied from Identity.
	identity *stringPair[int]
	// Returned by [Hsu.MustID].
	UserID int
	// Target init namespace uid resolved from UserID and identity.
	uid *stringPair[int]

	// Included as part of [hst.Config], transmitted as-is unless permissive defaults.
	Container *hst.ContainerConfig

	// Mapped credentials within container user namespace.
	Mapuid, Mapgid int
	// Copied from their respective exported values.
	mapuid, mapgid *stringPair[int]

	// Copied from [EnvPaths] per-process.
	sc hst.Paths
	*env.Paths

	// Copied via populateLocal.
	k syscallDispatcher
	// Copied via populateLocal.
	msg message.Msg
}

// valid checks outcomeState to be safe for use with outcomeOp.
func (s *outcomeState) valid() bool {
	return s != nil &&
		s.Shim.valid() &&
		s.ID != nil &&
		s.Container != nil &&
		s.Paths != nil
}

// newOutcomeState returns the address of a new outcomeState with its exported fields populated via syscallDispatcher.
func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *hst.Config, hsu *Hsu) *outcomeState {
	s := outcomeState{
		Shim:      &shimParams{PrivPID: k.getpid(), Verbose: msg.IsVerbose()},
		ID:        id,
		Identity:  config.Identity,
		UserID:    hsu.MustID(msg),
		Paths:     env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string { v, _ := k.lookupEnv(key); return v }),
		Container: config.Container,
	}

	// enforce bounds and default early
	if s.Container.WaitDelay < 0 {
		s.Shim.WaitDelay = 0
	} else if s.Container.WaitDelay == 0 {
		s.Shim.WaitDelay = hst.WaitDelayDefault
	} else if s.Container.WaitDelay > hst.WaitDelayMax {
		s.Shim.WaitDelay = hst.WaitDelayMax
	} else {
		s.Shim.WaitDelay = s.Container.WaitDelay
	}

	if s.Container.Flags&hst.FMapRealUID != 0 {
		s.Mapuid, s.Mapgid = k.getuid(), k.getgid()
	} else {
		s.Mapuid, s.Mapgid = k.overflowUid(msg), k.overflowGid(msg)
	}

	return &s
}

// populateLocal populates unexported fields from transmitted exported fields.
// These fields are cheaper to recompute per-process.
func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error {
	if !s.valid() || k == nil || msg == nil {
		return newWithMessage("impossible outcome state reached")
	}

	if s.k != nil || s.msg != nil {
		panic("attempting to call populateLocal twice")
	}
	s.k = k
	s.msg = msg

	s.id = &stringPair[hst.ID]{*s.ID, s.ID.String()}

	s.Copy(&s.sc, s.UserID)
	msg.Verbosef("process share directory at %q, runtime directory at %q", s.sc.SharePath, s.sc.RunDirPath)

	s.identity = newInt(s.Identity)
	s.mapuid, s.mapgid = newInt(s.Mapuid), newInt(s.Mapgid)
	s.uid = newInt(hst.ToUser(s.UserID, s.identity.unwrap()))

	return nil
}

// instancePath returns a path formatted for outcomeStateSys.instance.
// This method must only be called from outcomeOp.toContainer if
// outcomeOp.toSystem has already called outcomeStateSys.instance.
func (s *outcomeState) instancePath() *check.Absolute { return s.sc.SharePath.Append(s.id.String()) }

// runtimePath returns a path formatted for outcomeStateSys.runtime.
// This method must only be called from outcomeOp.toContainer if
// outcomeOp.toSystem has already called outcomeStateSys.runtime.
func (s *outcomeState) runtimePath() *check.Absolute { return s.sc.RunDirPath.Append(s.id.String()) }

// outcomeStateSys wraps outcomeState and [system.I]. Used on the priv side only.
// Implementations of outcomeOp must not access fields other than sys unless explicitly stated.
type outcomeStateSys struct {
	// Whether XDG_RUNTIME_DIR is used post hsu.
	useRuntimeDir bool
	// Process-specific directory in TMPDIR, nil if unused.
	sharePath *check.Absolute
	// Process-specific directory in XDG_RUNTIME_DIR, nil if unused.
	runtimeSharePath *check.Absolute

	// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
	appId string
	// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
	et hst.Enablement

	// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
	directWayland bool
	// Copied from [hst.Config]. Safe for read by spPulseOp.toSystem only.
	directPulse bool
	// Copied header from [hst.Config]. Safe for read by spFilesystemOp.toSystem only.
	extraPerms []hst.ExtraPermConfig
	// Copied address from [hst.Config]. Safe for read by spDBusOp.toSystem only.
	sessionBus, systemBus *hst.BusConfig

	sys *system.I
	*outcomeState
}

// newSys returns the address of a new outcomeStateSys embedding the current outcomeState.
func (s *outcomeState) newSys(config *hst.Config, sys *system.I) *outcomeStateSys {
	return &outcomeStateSys{
		appId: config.ID, et: config.Enablements.Unwrap(),
		directWayland: config.DirectWayland, directPulse: config.DirectPulse,
		extraPerms: config.ExtraPerms,
		sessionBus: config.SessionBus, systemBus: config.SystemBus,
		sys: sys, outcomeState: s,
	}
}

// newParams returns the address of a new outcomeStateParams embedding the current outcomeState.
func (s *outcomeState) newParams() *outcomeStateParams {
	stateParams := outcomeStateParams{params: new(container.Params), outcomeState: s}
	if s.Container.Env == nil {
		stateParams.env = make(map[string]string, envAllocSize)
	} else {
		stateParams.env = maps.Clone(s.Container.Env)
	}
	return &stateParams
}

// ensureRuntimeDir must be called if access to paths within XDG_RUNTIME_DIR is required.
func (state *outcomeStateSys) ensureRuntimeDir() {
	if state.useRuntimeDir {
		return
	}
	state.useRuntimeDir = true
	state.sys.
		// ensure this dir in case XDG_RUNTIME_DIR is unset
		Ensure(state.sc.RuntimePath, 0700).UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute).
		Ensure(state.sc.RunDirPath, 0700).UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute)
}

// instance returns the pathname to a process-specific directory within TMPDIR.
// This directory must only hold entries bound to [system.Process].
func (state *outcomeStateSys) instance() *check.Absolute {
	if state.sharePath != nil {
		return state.sharePath
	}
	state.sharePath = state.instancePath()
	state.sys.Ephemeral(system.Process, state.sharePath, 0711)
	return state.sharePath
}

// runtime returns the pathname to a process-specific directory within XDG_RUNTIME_DIR.
// This directory must only hold entries bound to [system.Process].
func (state *outcomeStateSys) runtime() *check.Absolute {
	if state.runtimeSharePath != nil {
		return state.runtimeSharePath
	}
	state.ensureRuntimeDir()
	state.runtimeSharePath = state.runtimePath()
	state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700)
	state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute)
	return state.runtimeSharePath
}

// outcomeStateParams wraps outcomeState and [container.Params]. Used on the shim side only.
type outcomeStateParams struct {
	// Overrides the embedded [container.Params] in [container.Container]. The Env field must not be used.
	params *container.Params
	// Collapsed into the Env slice in [container.Params] by the final outcomeOp.
	env map[string]string

	// Filesystems with the optional root sliced off if present. Populated by spParamsOp.
	// Safe for use by spFilesystemOp.
	filesystem []hst.FilesystemConfigJSON

	// Inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` via mapped uid.
	// Populated by spRuntimeOp.
	runtimeDir *check.Absolute

	as hst.ApplyState
	*outcomeState
}

// errNotEnabled is returned by outcomeOp.toSystem and used internally to exclude an outcomeOp from transmission.
var errNotEnabled = errors.New("op not enabled in the configuration")

// An outcomeOp inflicts an outcome on [system.I] and contains enough information to
// inflict it on [container.Params] in a separate process.
// An implementation of outcomeOp must store cross-process states in exported fields only.
type outcomeOp interface {
	// toSystem inflicts the current outcome on [system.I] in the priv side process.
	toSystem(state *outcomeStateSys) error

	// toContainer inflicts the current outcome on [container.Params] in the shim process.
	// The implementation must not write to the Env field of [container.Params] as it will be overwritten
	// by flattened env map.
	toContainer(state *outcomeStateParams) error
}

// toSystem calls the outcomeOp.toSystem method on all outcomeOp implementations and populates shimParams.Ops.
// This function assumes the caller has already called the Validate method on [hst.Config]
// and checked that it returns nil.
func (state *outcomeStateSys) toSystem() error {
	if state.Shim == nil || state.Shim.Ops != nil {
		return newWithMessage("invalid ops state reached")
	}

	ops := [...]outcomeOp{
		// must run first
		&spParamsOp{},

		&spRuntimeOp{},
		spTmpdirOp{},
		spAccountOp{},

		// optional via enablements
		&spWaylandOp{},
		&spX11Op{},
		&spPulseOp{},
		&spDBusOp{},

		// must run last
		&spFilesystemOp{},
	}

	state.Shim.Ops = make([]outcomeOp, 0, len(ops))
	for _, op := range ops {
		if err := op.toSystem(state); err != nil {
			// this error is used internally to exclude this outcomeOp from transmission
			if errors.Is(err, errNotEnabled) {
				continue
			}

			return err
		}
		state.Shim.Ops = append(state.Shim.Ops, op)
	}
	return nil
}