github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/boot/cmdline.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package boot
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  
    26  	"github.com/snapcore/snapd/asserts"
    27  	"github.com/snapcore/snapd/bootloader"
    28  	"github.com/snapcore/snapd/dirs"
    29  	"github.com/snapcore/snapd/logger"
    30  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/strutil"
    32  )
    33  
    34  const (
    35  	// ModeRun indicates the regular operating system mode of the device.
    36  	ModeRun = "run"
    37  	// ModeInstall is a mode in which a new system is installed on the
    38  	// device.
    39  	ModeInstall = "install"
    40  	// ModeRecover is a mode in which the device boots into the recovery
    41  	// system.
    42  	ModeRecover = "recover"
    43  )
    44  
    45  var (
    46  	validModes = []string{ModeInstall, ModeRecover, ModeRun}
    47  )
    48  
    49  // ModeAndRecoverySystemFromKernelCommandLine returns the current system mode
    50  // and the recovery system label as passed in the kernel command line by the
    51  // bootloader.
    52  func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) {
    53  	m, err := osutil.KernelCommandLineKeyValues("snapd_recovery_mode", "snapd_recovery_system")
    54  	if err != nil {
    55  		return "", "", err
    56  	}
    57  	var modeOk bool
    58  	mode, modeOk = m["snapd_recovery_mode"]
    59  
    60  	// no mode specified gets interpreted as install
    61  	if modeOk {
    62  		if mode == "" {
    63  			mode = ModeInstall
    64  		} else if !strutil.ListContains(validModes, mode) {
    65  			return "", "", fmt.Errorf("cannot use unknown mode %q", mode)
    66  		}
    67  	}
    68  
    69  	sysLabel = m["snapd_recovery_system"]
    70  
    71  	switch {
    72  	case mode == "" && sysLabel == "":
    73  		return "", "", fmt.Errorf("cannot detect mode nor recovery system to use")
    74  	case mode == "" && sysLabel != "":
    75  		return "", "", fmt.Errorf("cannot specify system label without a mode")
    76  	case mode == ModeInstall && sysLabel == "":
    77  		return "", "", fmt.Errorf("cannot specify install mode without system label")
    78  	case mode == ModeRun && sysLabel != "":
    79  		// XXX: should we silently ignore the label? at least log for now
    80  		logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel)
    81  		sysLabel = ""
    82  	}
    83  	return mode, sysLabel, nil
    84  }
    85  
    86  var errBootConfigNotManaged = errors.New("boot config is not managed")
    87  
    88  func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) {
    89  	bl, err := bootloader.Find(where, opts)
    90  	if err != nil {
    91  		return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err)
    92  	}
    93  	mbl, ok := bl.(bootloader.TrustedAssetsBootloader)
    94  	if !ok {
    95  		// the bootloader cannot manage its scripts
    96  		return nil, errBootConfigNotManaged
    97  	}
    98  	return mbl, nil
    99  }
   100  
   101  const (
   102  	currentEdition = iota
   103  	candidateEdition
   104  )
   105  
   106  func composeCommandLine(model *asserts.Model, currentOrCandidate int, mode, system string) (string, error) {
   107  	if model.Grade() == asserts.ModelGradeUnset {
   108  		return "", nil
   109  	}
   110  	if mode != ModeRun && mode != ModeRecover {
   111  		return "", fmt.Errorf("internal error: unsupported command line mode %q", mode)
   112  	}
   113  	// get the run mode bootloader under the native run partition layout
   114  	opts := &bootloader.Options{
   115  		Role:        bootloader.RoleRunMode,
   116  		NoSlashBoot: true,
   117  	}
   118  	bootloaderRootDir := InitramfsUbuntuBootDir
   119  	modeArg := "snapd_recovery_mode=run"
   120  	systemArg := ""
   121  	if mode == ModeRecover {
   122  		if system == "" {
   123  			return "", fmt.Errorf("internal error: system is unset")
   124  		}
   125  		// dealing with recovery system bootloader
   126  		opts.Role = bootloader.RoleRecovery
   127  		bootloaderRootDir = InitramfsUbuntuSeedDir
   128  		// recovery mode & system command line arguments
   129  		modeArg = "snapd_recovery_mode=recover"
   130  		systemArg = fmt.Sprintf("snapd_recovery_system=%v", system)
   131  	}
   132  	mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts)
   133  	if err != nil {
   134  		if err == errBootConfigNotManaged {
   135  			return "", nil
   136  		}
   137  		return "", err
   138  	}
   139  	// TODO:UC20: fetch extra args from gadget
   140  	extraArgs := ""
   141  	if currentOrCandidate == currentEdition {
   142  		return mbl.CommandLine(modeArg, systemArg, extraArgs)
   143  	} else {
   144  		return mbl.CandidateCommandLine(modeArg, systemArg, extraArgs)
   145  	}
   146  }
   147  
   148  // ComposeRecoveryCommandLine composes the kernel command line used when booting
   149  // a given system in recover mode.
   150  func ComposeRecoveryCommandLine(model *asserts.Model, system string) (string, error) {
   151  	return composeCommandLine(model, currentEdition, ModeRecover, system)
   152  }
   153  
   154  // ComposeCommandLine composes the kernel command line used when booting the
   155  // system in run mode.
   156  func ComposeCommandLine(model *asserts.Model) (string, error) {
   157  	return composeCommandLine(model, currentEdition, ModeRun, "")
   158  }
   159  
   160  // ComposeCandidateCommandLine composes the kernel command line used when
   161  // booting the system in run mode with the current built-in edition of managed
   162  // boot assets.
   163  func ComposeCandidateCommandLine(model *asserts.Model) (string, error) {
   164  	return composeCommandLine(model, candidateEdition, ModeRun, "")
   165  }
   166  
   167  // ComposeCandidateRecoveryCommandLine composes the kernel command line used
   168  // when booting the given system in recover mode with the current built-in
   169  // edition of managed boot assets.
   170  func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system string) (string, error) {
   171  	return composeCommandLine(model, candidateEdition, ModeRecover, system)
   172  }
   173  
   174  // observeSuccessfulCommandLine observes a successful boot with a command line
   175  // and takes an action based on the contents of the modeenv. The current kernel
   176  // command lines in the modeenv can have up to 2 entries when the managed
   177  // bootloader boot config gets updated.
   178  func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   179  	// TODO:UC20 only care about run mode for now
   180  	if m.Mode != "run" {
   181  		return m, nil
   182  	}
   183  
   184  	switch len(m.CurrentKernelCommandLines) {
   185  	case 0:
   186  		// compatibility scenario, no command lines tracked in modeenv
   187  		// yet, this can happen when having booted with a newer snapd
   188  		return observeSuccessfulCommandLineCompatBoot(model, m)
   189  	case 1:
   190  		// no command line update
   191  		return m, nil
   192  	default:
   193  		return observeSuccessfulCommandLineUpdate(m)
   194  	}
   195  }
   196  
   197  // observeSuccessfulCommandLineUpdate observes a successful boot with a command
   198  // line which is expected to be listed among the current kernel command line
   199  // entries carried in the modeenv. One of those entries must match the current
   200  // kernel command line of a running system and will be recorded alone as in use.
   201  func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) {
   202  	newM, err := m.Copy()
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	// get the current command line
   208  	cmdlineBootedWith, err := osutil.KernelCommandLine()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) {
   213  		return nil, fmt.Errorf("current command line content %q not matching any expected entry",
   214  			cmdlineBootedWith)
   215  	}
   216  	newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith}
   217  
   218  	return newM, nil
   219  }
   220  
   221  // observeSuccessfulCommandLineCompatBoot observes a successful boot with a
   222  // kernel command line, where the list of current kernel command lines in the
   223  // modeenv is unpopulated. This handles a compatibility scenario with systems
   224  // that were installed using a previous version of snapd. It verifies that the
   225  // expected kernel command line matches the one the system booted with and
   226  // populates modeenv kernel command line list accordingly.
   227  func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   228  	cmdlineExpected, err := ComposeCommandLine(model)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	cmdlineBootedWith, err := osutil.KernelCommandLine()
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	if cmdlineExpected != cmdlineBootedWith {
   237  		return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith)
   238  	}
   239  	newM, err := m.Copy()
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected}
   244  	return newM, nil
   245  }
   246  
   247  // observeCommandLineUpdate observes a pending kernel command line change caused
   248  // by an update of boot config. When needed, the modeenv is updated with a
   249  // candidate command line and the encryption keys are resealed. This helper
   250  // should be called right before updating the managed boot config.
   251  func observeCommandLineUpdate(model *asserts.Model) error {
   252  	// TODO:UC20: consider updating a recovery system command line
   253  
   254  	m, err := loadModeenv()
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	if len(m.CurrentKernelCommandLines) == 0 {
   260  		return fmt.Errorf("internal error: current kernel command lines is unset")
   261  	}
   262  	// this is the current expected command line which was recorded by
   263  	// bootstate
   264  	cmdline := m.CurrentKernelCommandLines[0]
   265  	// this is the new expected command line
   266  	candidateCmdline, err := ComposeCandidateCommandLine(model)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	if cmdline == candidateCmdline {
   271  		// no change in command line contents, nothing to do
   272  		return nil
   273  	}
   274  	m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline}
   275  
   276  	if err := m.Write(); err != nil {
   277  		return err
   278  	}
   279  
   280  	expectReseal := true
   281  	if err := resealKeyToModeenv(dirs.GlobalRootDir, model, m, expectReseal); err != nil {
   282  		return err
   283  	}
   284  	return nil
   285  }
   286  
   287  // kernelCommandLinesForResealWithFallback provides the list of kernel command
   288  // lines for use during reseal. During normal operation, the command lines will
   289  // be listed in the modeenv.
   290  func kernelCommandLinesForResealWithFallback(model *asserts.Model, modeenv *Modeenv) (cmdlines []string, err error) {
   291  	if len(modeenv.CurrentKernelCommandLines) > 0 {
   292  		return modeenv.CurrentKernelCommandLines, nil
   293  	}
   294  	// fallback for when reseal is called before mark boot successful set a
   295  	// default during snapd update
   296  	cmdline, err := ComposeCommandLine(model)
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	return []string{cmdline}, nil
   301  }