Files
snapd/boot/cmdline.go

401 lines
14 KiB
Go

// -*- 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
}