github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/boot/cmdline.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 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/gadget"
    30  	"github.com/snapcore/snapd/logger"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/strutil"
    33  )
    34  
    35  const (
    36  	// ModeRun indicates the regular operating system mode of the device.
    37  	ModeRun = "run"
    38  	// ModeInstall is a mode in which a new system is installed on the
    39  	// device.
    40  	ModeInstall = "install"
    41  	// ModeRecover is a mode in which the device boots into the recovery
    42  	// system.
    43  	ModeRecover = "recover"
    44  )
    45  
    46  var (
    47  	validModes = []string{ModeInstall, ModeRecover, ModeRun}
    48  )
    49  
    50  // ModeAndRecoverySystemFromKernelCommandLine returns the current system mode
    51  // and the recovery system label as passed in the kernel command line by the
    52  // bootloader.
    53  func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) {
    54  	m, err := osutil.KernelCommandLineKeyValues("snapd_recovery_mode", "snapd_recovery_system")
    55  	if err != nil {
    56  		return "", "", err
    57  	}
    58  	var modeOk bool
    59  	mode, modeOk = m["snapd_recovery_mode"]
    60  
    61  	// no mode specified gets interpreted as install
    62  	if modeOk {
    63  		if mode == "" {
    64  			mode = ModeInstall
    65  		} else if !strutil.ListContains(validModes, mode) {
    66  			return "", "", fmt.Errorf("cannot use unknown mode %q", mode)
    67  		}
    68  	}
    69  
    70  	sysLabel = m["snapd_recovery_system"]
    71  
    72  	switch {
    73  	case mode == "" && sysLabel == "":
    74  		return "", "", fmt.Errorf("cannot detect mode nor recovery system to use")
    75  	case mode == "" && sysLabel != "":
    76  		return "", "", fmt.Errorf("cannot specify system label without a mode")
    77  	case mode == ModeInstall && sysLabel == "":
    78  		return "", "", fmt.Errorf("cannot specify install mode without system label")
    79  	case mode == ModeRun && sysLabel != "":
    80  		// XXX: should we silently ignore the label? at least log for now
    81  		logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel)
    82  		sysLabel = ""
    83  	}
    84  	return mode, sysLabel, nil
    85  }
    86  
    87  var errBootConfigNotManaged = errors.New("boot config is not managed")
    88  
    89  func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) {
    90  	bl, err := bootloader.Find(where, opts)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err)
    93  	}
    94  	mbl, ok := bl.(bootloader.TrustedAssetsBootloader)
    95  	if !ok {
    96  		// the bootloader cannot manage its scripts
    97  		return nil, errBootConfigNotManaged
    98  	}
    99  	return mbl, nil
   100  }
   101  
   102  // bootVarsForTrustedCommandLineFromGadget returns a set of boot variables that
   103  // carry the command line arguments requested by the gadget. This is only useful
   104  // if snapd is managing the boot config.
   105  func bootVarsForTrustedCommandLineFromGadget(gadgetDirOrSnapPath string) (map[string]string, error) {
   106  	extraOrFull, full, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath)
   107  	if err != nil {
   108  		if err == gadget.ErrNoKernelCommandline {
   109  			// nothing set by the gadget, but we could have had
   110  			// arguments before, so make sure those are cleared now
   111  			clear := map[string]string{
   112  				"snapd_extra_cmdline_args": "",
   113  				"snapd_full_cmdline_args":  "",
   114  			}
   115  			return clear, nil
   116  		}
   117  		return nil, fmt.Errorf("cannot use kernel command line from gadget: %v", err)
   118  	}
   119  	// gadget has the kernel command line
   120  	args := map[string]string{
   121  		"snapd_extra_cmdline_args": "",
   122  		"snapd_full_cmdline_args":  "",
   123  	}
   124  	if full {
   125  		args["snapd_full_cmdline_args"] = extraOrFull
   126  	} else {
   127  		args["snapd_extra_cmdline_args"] = extraOrFull
   128  	}
   129  	return args, nil
   130  }
   131  
   132  const (
   133  	currentEdition = iota
   134  	candidateEdition
   135  )
   136  
   137  func composeCommandLine(model *asserts.Model, currentOrCandidate int, mode, system, gadgetDirOrSnapPath string) (string, error) {
   138  	if model.Grade() == asserts.ModelGradeUnset {
   139  		return "", nil
   140  	}
   141  	if mode != ModeRun && mode != ModeRecover {
   142  		return "", fmt.Errorf("internal error: unsupported command line mode %q", mode)
   143  	}
   144  	// get the run mode bootloader under the native run partition layout
   145  	opts := &bootloader.Options{
   146  		Role:        bootloader.RoleRunMode,
   147  		NoSlashBoot: true,
   148  	}
   149  	bootloaderRootDir := InitramfsUbuntuBootDir
   150  	components := bootloader.CommandLineComponents{
   151  		ModeArg: "snapd_recovery_mode=run",
   152  	}
   153  	if mode == ModeRecover {
   154  		if system == "" {
   155  			return "", fmt.Errorf("internal error: system is unset")
   156  		}
   157  		// dealing with recovery system bootloader
   158  		opts.Role = bootloader.RoleRecovery
   159  		bootloaderRootDir = InitramfsUbuntuSeedDir
   160  		// recovery mode & system command line arguments
   161  		components = bootloader.CommandLineComponents{
   162  			ModeArg:   "snapd_recovery_mode=recover",
   163  			SystemArg: fmt.Sprintf("snapd_recovery_system=%v", system),
   164  		}
   165  	}
   166  	mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts)
   167  	if err != nil {
   168  		if err == errBootConfigNotManaged {
   169  			return "", nil
   170  		}
   171  		return "", err
   172  	}
   173  	if gadgetDirOrSnapPath != "" {
   174  		extraOrFull, full, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath)
   175  		if err != nil && err != gadget.ErrNoKernelCommandline {
   176  			return "", fmt.Errorf("cannot use kernel command line from gadget: %v", err)
   177  		}
   178  		if err == nil {
   179  			// gadget provides some part of the kernel command line
   180  			if full {
   181  				components.FullArgs = extraOrFull
   182  			} else {
   183  				components.ExtraArgs = extraOrFull
   184  			}
   185  		}
   186  	}
   187  	if currentOrCandidate == currentEdition {
   188  		return mbl.CommandLine(components)
   189  	} else {
   190  		return mbl.CandidateCommandLine(components)
   191  	}
   192  }
   193  
   194  // ComposeRecoveryCommandLine composes the kernel command line used when booting
   195  // a given system in recover mode.
   196  func ComposeRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) {
   197  	return composeCommandLine(model, currentEdition, ModeRecover, system, gadgetDirOrSnapPath)
   198  }
   199  
   200  // ComposeCommandLine composes the kernel command line used when booting the
   201  // system in run mode.
   202  func ComposeCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) {
   203  	return composeCommandLine(model, currentEdition, ModeRun, "", gadgetDirOrSnapPath)
   204  }
   205  
   206  // ComposeCandidateCommandLine composes the kernel command line used when
   207  // booting the system in run mode with the current built-in edition of managed
   208  // boot assets.
   209  func ComposeCandidateCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) {
   210  	return composeCommandLine(model, candidateEdition, ModeRun, "", gadgetDirOrSnapPath)
   211  }
   212  
   213  // ComposeCandidateRecoveryCommandLine composes the kernel command line used
   214  // when booting the given system in recover mode with the current built-in
   215  // edition of managed boot assets.
   216  func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) {
   217  	return composeCommandLine(model, candidateEdition, ModeRecover, system, gadgetDirOrSnapPath)
   218  }
   219  
   220  // observeSuccessfulCommandLine observes a successful boot with a command line
   221  // and takes an action based on the contents of the modeenv. The current kernel
   222  // command lines in the modeenv can have up to 2 entries when the managed
   223  // bootloader boot config gets updated.
   224  func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   225  	// TODO:UC20 only care about run mode for now
   226  	if m.Mode != "run" {
   227  		return m, nil
   228  	}
   229  
   230  	switch len(m.CurrentKernelCommandLines) {
   231  	case 0:
   232  		// maybe a compatibility scenario, no command lines tracked in
   233  		// modeenv yet, this can happen when having booted with a newer
   234  		// snapd
   235  		return observeSuccessfulCommandLineCompatBoot(model, m)
   236  	case 1:
   237  		// no command line update
   238  		return m, nil
   239  	default:
   240  		return observeSuccessfulCommandLineUpdate(m)
   241  	}
   242  }
   243  
   244  // observeSuccessfulCommandLineUpdate observes a successful boot with a command
   245  // line which is expected to be listed among the current kernel command line
   246  // entries carried in the modeenv. One of those entries must match the current
   247  // kernel command line of a running system and will be recorded alone as in use.
   248  func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) {
   249  	newM, err := m.Copy()
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	// get the current command line
   255  	cmdlineBootedWith, err := osutil.KernelCommandLine()
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) {
   260  		return nil, fmt.Errorf("current command line content %q not matching any expected entry",
   261  			cmdlineBootedWith)
   262  	}
   263  	newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith}
   264  
   265  	return newM, nil
   266  }
   267  
   268  // observeSuccessfulCommandLineCompatBoot observes a successful boot with a
   269  // kernel command line, where the list of current kernel command lines in the
   270  // modeenv is unpopulated. This handles a compatibility scenario with systems
   271  // that were installed using a previous version of snapd. It verifies that the
   272  // expected kernel command line matches the one the system booted with and
   273  // populates modeenv kernel command line list accordingly.
   274  func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   275  	// since this is a compatibility scenario, the kernel command line
   276  	// arguments would not have come from the gadget before either
   277  	cmdlineExpected, err := ComposeCommandLine(model, "")
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	if cmdlineExpected == "" {
   282  		// there is no particular command line expected for this model
   283  		// and system bootloader, indicating that the command line is
   284  		// not being tracked
   285  		return m, nil
   286  	}
   287  	cmdlineBootedWith, err := osutil.KernelCommandLine()
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	if cmdlineExpected != cmdlineBootedWith {
   292  		return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith)
   293  	}
   294  	newM, err := m.Copy()
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected}
   299  	return newM, nil
   300  }
   301  
   302  type commandLineUpdateReason int
   303  
   304  const (
   305  	commandLineUpdateReasonSnapd commandLineUpdateReason = iota
   306  	commandLineUpdateReasonGadget
   307  )
   308  
   309  // observeCommandLineUpdate observes a pending kernel command line change caused
   310  // by an update of boot config or the gadget snap. When needed, the modeenv is
   311  // updated with a candidate command line and the encryption keys are resealed.
   312  // This helper should be called right before updating the managed boot config.
   313  func observeCommandLineUpdate(model *asserts.Model, reason commandLineUpdateReason, gadgetSnapOrDir string) (updated bool, err error) {
   314  	// TODO:UC20: consider updating a recovery system command line
   315  
   316  	m, err := loadModeenv()
   317  	if err != nil {
   318  		return false, err
   319  	}
   320  
   321  	if len(m.CurrentKernelCommandLines) == 0 {
   322  		return false, fmt.Errorf("internal error: current kernel command lines is unset")
   323  	}
   324  	// this is the current expected command line which was recorded by
   325  	// bootstate
   326  	cmdline := m.CurrentKernelCommandLines[0]
   327  	// this is the new expected command line
   328  	var candidateCmdline string
   329  	switch reason {
   330  	case commandLineUpdateReasonSnapd:
   331  		// pending boot config update
   332  		candidateCmdline, err = ComposeCandidateCommandLine(model, gadgetSnapOrDir)
   333  	case commandLineUpdateReasonGadget:
   334  		// pending gadget update
   335  		candidateCmdline, err = ComposeCommandLine(model, gadgetSnapOrDir)
   336  	}
   337  	if err != nil {
   338  		return false, err
   339  	}
   340  	if cmdline == candidateCmdline {
   341  		// command line is the same or no actual change in modeenv
   342  		return false, nil
   343  	}
   344  	// actual change of the command line content
   345  	m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline}
   346  
   347  	if err := m.Write(); err != nil {
   348  		return false, err
   349  	}
   350  
   351  	expectReseal := true
   352  	if err := resealKeyToModeenv(dirs.GlobalRootDir, model, m, expectReseal); err != nil {
   353  		return false, err
   354  	}
   355  	return true, nil
   356  }
   357  
   358  // kernelCommandLinesForResealWithFallback provides the list of kernel command
   359  // lines for use during reseal. During normal operation, the command lines will
   360  // be listed in the modeenv.
   361  func kernelCommandLinesForResealWithFallback(model *asserts.Model, modeenv *Modeenv) (cmdlines []string, err error) {
   362  	if len(modeenv.CurrentKernelCommandLines) > 0 {
   363  		return modeenv.CurrentKernelCommandLines, nil
   364  	}
   365  	// fallback for when reseal is called before mark boot successful set a
   366  	// default during snapd update, since this is a compatibility scenario
   367  	// there would be no kernel command lines arguments coming from the
   368  	// gadget either
   369  	cmdline, err := ComposeCommandLine(model, "")
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  	return []string{cmdline}, nil
   374  }