-
Notifications
You must be signed in to change notification settings - Fork 601
/
Copy pathcmdline.go
400 lines (364 loc) · 14.1 KB
/
cmdline.go
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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package boot
import (
"errors"
"fmt"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/strutil"
)
const (
// ModeRun indicates the regular operating system mode of the device.
ModeRun = "run"
// ModeInstall is a mode in which a new system is installed on the
// device.
ModeInstall = "install"
// ModeRecover is a mode in which the device boots into the recovery
// system.
ModeRecover = "recover"
// ModeFactoryReset is a mode in which the device performs a factory
// reset.
ModeFactoryReset = "factory-reset"
// ModeRunCVM is Azure CVM specific run mode fde + classic debs
ModeRunCVM = "cloudimg-rootfs"
)
var (
validModes = []string{ModeInstall, ModeRecover, ModeFactoryReset, ModeRun, ModeRunCVM}
)
// ModeAndRecoverySystemFromKernelCommandLine returns the current system mode
// and the recovery system label as passed in the kernel command line by the
// bootloader.
func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) {
m, err := kcmdline.KeyValues("snapd_recovery_mode", "snapd_recovery_system")
if err != nil {
return "", "", err
}
var modeOk bool
mode, modeOk = m["snapd_recovery_mode"]
// no mode specified gets interpreted as install
if modeOk {
if mode == "" {
mode = ModeInstall
} else if !strutil.ListContains(validModes, mode) {
return "", "", fmt.Errorf("cannot use unknown mode %q", mode)
}
}
sysLabel = m["snapd_recovery_system"]
switch {
case mode == "" && sysLabel == "":
return "", "", fmt.Errorf("cannot detect mode nor recovery system to use")
case mode == "" && sysLabel != "":
return "", "", fmt.Errorf("cannot specify system label without a mode")
case mode == ModeInstall && sysLabel == "":
return "", "", fmt.Errorf("cannot specify install mode without system label")
case mode == ModeRun && sysLabel != "":
// XXX: should we silently ignore the label? at least log for now
logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel)
sysLabel = ""
}
return mode, sysLabel, nil
}
var errBootConfigNotManaged = errors.New("boot config is not managed")
func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) {
bl, err := bootloader.Find(where, opts)
if err != nil {
return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err)
}
mbl, ok := bl.(bootloader.TrustedAssetsBootloader)
if !ok {
// the bootloader cannot manage its scripts
return nil, errBootConfigNotManaged
}
return mbl, nil
}
// bootVarsForTrustedCommandLineFromGadget returns a set of boot
// variables that carry the command line arguments defined by the
// gadget and some system options (cmdlineApped). This is only useful
// if snapd is managing the boot config.
func bootVarsForTrustedCommandLineFromGadget(gadgetDirOrSnapPath, cmdlineAppend string, defaultCmdline string, model gadget.Model) (map[string]string, error) {
extraOrFull, full, removeArgs, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath, model)
if err != nil {
return nil, fmt.Errorf("cannot use kernel command line from gadget: %v", err)
}
logger.Debugf("trusted command line: from gadget: %q, from options: %q",
extraOrFull, cmdlineAppend)
extraOrFull = strutil.JoinNonEmpty([]string{extraOrFull, cmdlineAppend}, " ")
keepDefaultArgs := kcmdline.RemoveMatchingFilter(defaultCmdline, removeArgs)
// gadget has the kernel command line
args := map[string]string{
"snapd_extra_cmdline_args": "",
"snapd_full_cmdline_args": "",
}
if full {
args["snapd_full_cmdline_args"] = extraOrFull
} else {
args["snapd_full_cmdline_args"] = strutil.JoinNonEmpty(append(keepDefaultArgs, extraOrFull), " ")
}
if len(args["snapd_full_cmdline_args"]) == 0 {
// grub.cfg tests if snapd_full_cmdline_args is set by looking if it is not empty.
// Here, it should be set, but empty. So adding a space will force grub.cfg to use it.
args["snapd_full_cmdline_args"] = " "
}
return args, nil
}
const (
currentEdition = iota
candidateEdition
)
func composeCommandLine(currentOrCandidate int, mode, system, gadgetDirOrSnapPath string, model gadget.Model) (string, error) {
if mode != ModeRun && mode != ModeRecover && mode != ModeFactoryReset {
return "", fmt.Errorf("internal error: unsupported command line mode %q", mode)
}
// get the run mode bootloader under the native run partition layout
opts := &bootloader.Options{
Role: bootloader.RoleRunMode,
NoSlashBoot: true,
}
bootloaderRootDir := InitramfsUbuntuBootDir
components := bootloader.CommandLineComponents{
ModeArg: "snapd_recovery_mode=run",
}
if mode == ModeRecover || mode == ModeFactoryReset {
if system == "" {
return "", fmt.Errorf("internal error: system is unset")
}
// dealing with recovery system bootloader
opts.Role = bootloader.RoleRecovery
bootloaderRootDir = InitramfsUbuntuSeedDir
// recovery mode & system command line arguments
modeArg := "snapd_recovery_mode=recover"
if mode == ModeFactoryReset {
modeArg = "snapd_recovery_mode=factory-reset"
}
components = bootloader.CommandLineComponents{
ModeArg: modeArg,
SystemArg: fmt.Sprintf("snapd_recovery_system=%v", system),
}
}
mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts)
if err != nil {
if err == errBootConfigNotManaged {
return "", nil
}
return "", err
}
if gadgetDirOrSnapPath != "" {
extraOrFull, full, removeArgs, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath, model)
components.RemoveArgs = removeArgs
if err != nil {
return "", fmt.Errorf("cannot use kernel command line from gadget: %v", err)
}
// gadget provides some part of the kernel command line
if full {
components.FullArgs = extraOrFull
} else {
components.ExtraArgs = extraOrFull
}
}
if currentOrCandidate == currentEdition {
return mbl.CommandLine(components)
} else {
return mbl.CandidateCommandLine(components)
}
}
// ComposeRecoveryCommandLine composes the kernel command line used when booting
// a given system in recover mode.
func ComposeRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) {
if model.Grade() == asserts.ModelGradeUnset {
return "", nil
}
return composeCommandLine(currentEdition, ModeRecover, system, gadgetDirOrSnapPath, model)
}
// ComposeCommandLine composes the kernel command line used when booting the
// system in run mode.
func ComposeCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) {
if model.Grade() == asserts.ModelGradeUnset {
return "", nil
}
return composeCommandLine(currentEdition, ModeRun, "", gadgetDirOrSnapPath, model)
}
// ComposeCandidateCommandLine composes the kernel command line used when
// booting the system in run mode with the current built-in edition of managed
// boot assets.
func ComposeCandidateCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) {
if model.Grade() == asserts.ModelGradeUnset {
return "", nil
}
return composeCommandLine(candidateEdition, ModeRun, "", gadgetDirOrSnapPath, model)
}
// ComposeCandidateRecoveryCommandLine composes the kernel command line used
// when booting the given system in recover mode with the current built-in
// edition of managed boot assets.
func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) {
if model.Grade() == asserts.ModelGradeUnset {
return "", nil
}
return composeCommandLine(candidateEdition, ModeRecover, system, gadgetDirOrSnapPath, model)
}
// observeSuccessfulCommandLine observes a successful boot with a command line
// and takes an action based on the contents of the modeenv. The current kernel
// command lines in the modeenv can have up to 2 entries when the managed
// bootloader boot config gets updated.
func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
// TODO:UC20 only care about run mode for now
if m.Mode != "run" {
return m, nil
}
switch len(m.CurrentKernelCommandLines) {
case 0:
// maybe a compatibility scenario, no command lines tracked in
// modeenv yet, this can happen when having booted with a newer
// snapd
return observeSuccessfulCommandLineCompatBoot(model, m)
case 1:
// no command line update
return m, nil
default:
return observeSuccessfulCommandLineUpdate(m)
}
}
// observeSuccessfulCommandLineUpdate observes a successful boot with a command
// line which is expected to be listed among the current kernel command line
// entries carried in the modeenv. One of those entries must match the current
// kernel command line of a running system and will be recorded alone as in use.
func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) {
newM, err := m.Copy()
if err != nil {
return nil, err
}
// get the current command line
cmdlineBootedWith, err := kcmdline.KernelCommandLine()
if err != nil {
return nil, err
}
if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) {
return nil, fmt.Errorf("current command line content %q not matching any expected entry",
cmdlineBootedWith)
}
newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith}
return newM, nil
}
// observeSuccessfulCommandLineCompatBoot observes a successful boot with a
// kernel command line, where the list of current kernel command lines in the
// modeenv is unpopulated. This handles a compatibility scenario with systems
// that were installed using a previous version of snapd. It verifies that the
// expected kernel command line matches the one the system booted with and
// populates modeenv kernel command line list accordingly.
func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
// since this is a compatibility scenario, the kernel command line
// arguments would not have come from the gadget before either
cmdlineExpected, err := ComposeCommandLine(model, "")
if err != nil {
return nil, err
}
if cmdlineExpected == "" {
// there is no particular command line expected for this model
// and system bootloader, indicating that the command line is
// not being tracked
return m, nil
}
cmdlineBootedWith, err := kcmdline.KernelCommandLine()
if err != nil {
return nil, err
}
if cmdlineExpected != cmdlineBootedWith {
return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith)
}
newM, err := m.Copy()
if err != nil {
return nil, err
}
newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected}
return newM, nil
}
type commandLineUpdateReason int
const (
commandLineUpdateReasonSnapd commandLineUpdateReason = iota
commandLineUpdateReasonGadget
)
// observeCommandLineUpdate observes a pending kernel command line change caused
// by an update of boot config or the gadget snap. When needed, the modeenv is
// updated with a candidate command line and the encryption keys are resealed.
// This helper should be called right before updating the managed boot config.
func observeCommandLineUpdate(model *asserts.Model, reason commandLineUpdateReason, gadgetSnapOrDir, cmdlineOpt string) (updated bool, err error) {
// TODO:UC20: consider updating a recovery system command line
m, err := loadModeenv()
if err != nil {
return false, err
}
if len(m.CurrentKernelCommandLines) == 0 {
return false, fmt.Errorf("internal error: current kernel command lines is unset")
}
// this is the current expected command line which was recorded by
// bootstate
cmdline := m.CurrentKernelCommandLines[0]
// this is the new expected command line
var candidateCmdline string
switch reason {
case commandLineUpdateReasonSnapd:
// pending boot config update
candidateCmdline, err = ComposeCandidateCommandLine(model, gadgetSnapOrDir)
case commandLineUpdateReasonGadget:
// pending gadget update
candidateCmdline, err = ComposeCommandLine(model, gadgetSnapOrDir)
}
if err != nil {
return false, err
}
// Add part coming from options
candidateCmdline = strutil.JoinNonEmpty(
[]string{candidateCmdline, cmdlineOpt}, " ")
if cmdline == candidateCmdline {
// command line is the same or no actual change in modeenv
return false, nil
}
logger.Debugf("kernel commandline changes from %q to %q", cmdline, candidateCmdline)
// actual change of the command line content
m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline}
if err := m.Write(); err != nil {
return false, err
}
expectReseal := true
if err := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal, nil); err != nil {
return false, err
}
return true, nil
}
// kernelCommandLinesForResealWithFallback provides the list of kernel command
// lines for use during reseal. During normal operation, the command lines will
// be listed in the modeenv.
func kernelCommandLinesForResealWithFallback(modeenv *Modeenv) (cmdlines []string, err error) {
if len(modeenv.CurrentKernelCommandLines) > 0 {
return modeenv.CurrentKernelCommandLines, nil
}
// fallback for when reseal is called before mark boot successful set a
// default during snapd update, since this is a compatibility scenario
// there would be no kernel command lines arguments coming from the
// gadget either
gadgetDir := ""
cmdline, err := composeCommandLine(currentEdition, ModeRun, "", gadgetDir, modeenv.ModelForSealing())
if err != nil {
return nil, err
}
return []string{cmdline}, nil
}