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 401
|
// -*- 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
}
// no model changed => ignore FDE hooks
resealOpts := ResealKeyToModeenvOptions{ExpectReseal: true, IgnoreFDEHooks: true}
if err := resealKeyToModeenv(dirs.GlobalRootDir, m, resealOpts, 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
}
|