github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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(currentOrCandidate int, mode, system, gadgetDirOrSnapPath string) (string, error) {
   138  	if mode != ModeRun && mode != ModeRecover {
   139  		return "", fmt.Errorf("internal error: unsupported command line mode %q", mode)
   140  	}
   141  	// get the run mode bootloader under the native run partition layout
   142  	opts := &bootloader.Options{
   143  		Role:        bootloader.RoleRunMode,
   144  		NoSlashBoot: true,
   145  	}
   146  	bootloaderRootDir := InitramfsUbuntuBootDir
   147  	components := bootloader.CommandLineComponents{
   148  		ModeArg: "snapd_recovery_mode=run",
   149  	}
   150  	if mode == ModeRecover {
   151  		if system == "" {
   152  			return "", fmt.Errorf("internal error: system is unset")
   153  		}
   154  		// dealing with recovery system bootloader
   155  		opts.Role = bootloader.RoleRecovery
   156  		bootloaderRootDir = InitramfsUbuntuSeedDir
   157  		// recovery mode & system command line arguments
   158  		components = bootloader.CommandLineComponents{
   159  			ModeArg:   "snapd_recovery_mode=recover",
   160  			SystemArg: fmt.Sprintf("snapd_recovery_system=%v", system),
   161  		}
   162  	}
   163  	mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts)
   164  	if err != nil {
   165  		if err == errBootConfigNotManaged {
   166  			return "", nil
   167  		}
   168  		return "", err
   169  	}
   170  	if gadgetDirOrSnapPath != "" {
   171  		extraOrFull, full, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath)
   172  		if err != nil && err != gadget.ErrNoKernelCommandline {
   173  			return "", fmt.Errorf("cannot use kernel command line from gadget: %v", err)
   174  		}
   175  		if err == nil {
   176  			// gadget provides some part of the kernel command line
   177  			if full {
   178  				components.FullArgs = extraOrFull
   179  			} else {
   180  				components.ExtraArgs = extraOrFull
   181  			}
   182  		}
   183  	}
   184  	if currentOrCandidate == currentEdition {
   185  		return mbl.CommandLine(components)
   186  	} else {
   187  		return mbl.CandidateCommandLine(components)
   188  	}
   189  }
   190  
   191  // ComposeRecoveryCommandLine composes the kernel command line used when booting
   192  // a given system in recover mode.
   193  func ComposeRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) {
   194  	if model.Grade() == asserts.ModelGradeUnset {
   195  		return "", nil
   196  	}
   197  	return composeCommandLine(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  	if model.Grade() == asserts.ModelGradeUnset {
   204  		return "", nil
   205  	}
   206  	return composeCommandLine(currentEdition, ModeRun, "", gadgetDirOrSnapPath)
   207  }
   208  
   209  // ComposeCandidateCommandLine composes the kernel command line used when
   210  // booting the system in run mode with the current built-in edition of managed
   211  // boot assets.
   212  func ComposeCandidateCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) {
   213  	if model.Grade() == asserts.ModelGradeUnset {
   214  		return "", nil
   215  	}
   216  	return composeCommandLine(candidateEdition, ModeRun, "", gadgetDirOrSnapPath)
   217  }
   218  
   219  // ComposeCandidateRecoveryCommandLine composes the kernel command line used
   220  // when booting the given system in recover mode with the current built-in
   221  // edition of managed boot assets.
   222  func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) {
   223  	if model.Grade() == asserts.ModelGradeUnset {
   224  		return "", nil
   225  	}
   226  	return composeCommandLine(candidateEdition, ModeRecover, system, gadgetDirOrSnapPath)
   227  }
   228  
   229  // observeSuccessfulCommandLine observes a successful boot with a command line
   230  // and takes an action based on the contents of the modeenv. The current kernel
   231  // command lines in the modeenv can have up to 2 entries when the managed
   232  // bootloader boot config gets updated.
   233  func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   234  	// TODO:UC20 only care about run mode for now
   235  	if m.Mode != "run" {
   236  		return m, nil
   237  	}
   238  
   239  	switch len(m.CurrentKernelCommandLines) {
   240  	case 0:
   241  		// maybe a compatibility scenario, no command lines tracked in
   242  		// modeenv yet, this can happen when having booted with a newer
   243  		// snapd
   244  		return observeSuccessfulCommandLineCompatBoot(model, m)
   245  	case 1:
   246  		// no command line update
   247  		return m, nil
   248  	default:
   249  		return observeSuccessfulCommandLineUpdate(m)
   250  	}
   251  }
   252  
   253  // observeSuccessfulCommandLineUpdate observes a successful boot with a command
   254  // line which is expected to be listed among the current kernel command line
   255  // entries carried in the modeenv. One of those entries must match the current
   256  // kernel command line of a running system and will be recorded alone as in use.
   257  func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) {
   258  	newM, err := m.Copy()
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	// get the current command line
   264  	cmdlineBootedWith, err := osutil.KernelCommandLine()
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) {
   269  		return nil, fmt.Errorf("current command line content %q not matching any expected entry",
   270  			cmdlineBootedWith)
   271  	}
   272  	newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith}
   273  
   274  	return newM, nil
   275  }
   276  
   277  // observeSuccessfulCommandLineCompatBoot observes a successful boot with a
   278  // kernel command line, where the list of current kernel command lines in the
   279  // modeenv is unpopulated. This handles a compatibility scenario with systems
   280  // that were installed using a previous version of snapd. It verifies that the
   281  // expected kernel command line matches the one the system booted with and
   282  // populates modeenv kernel command line list accordingly.
   283  func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   284  	// since this is a compatibility scenario, the kernel command line
   285  	// arguments would not have come from the gadget before either
   286  	cmdlineExpected, err := ComposeCommandLine(model, "")
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	if cmdlineExpected == "" {
   291  		// there is no particular command line expected for this model
   292  		// and system bootloader, indicating that the command line is
   293  		// not being tracked
   294  		return m, nil
   295  	}
   296  	cmdlineBootedWith, err := osutil.KernelCommandLine()
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	if cmdlineExpected != cmdlineBootedWith {
   301  		return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith)
   302  	}
   303  	newM, err := m.Copy()
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected}
   308  	return newM, nil
   309  }
   310  
   311  type commandLineUpdateReason int
   312  
   313  const (
   314  	commandLineUpdateReasonSnapd commandLineUpdateReason = iota
   315  	commandLineUpdateReasonGadget
   316  )
   317  
   318  // observeCommandLineUpdate observes a pending kernel command line change caused
   319  // by an update of boot config or the gadget snap. When needed, the modeenv is
   320  // updated with a candidate command line and the encryption keys are resealed.
   321  // This helper should be called right before updating the managed boot config.
   322  func observeCommandLineUpdate(model *asserts.Model, reason commandLineUpdateReason, gadgetSnapOrDir string) (updated bool, err error) {
   323  	// TODO:UC20: consider updating a recovery system command line
   324  
   325  	m, err := loadModeenv()
   326  	if err != nil {
   327  		return false, err
   328  	}
   329  
   330  	if len(m.CurrentKernelCommandLines) == 0 {
   331  		return false, fmt.Errorf("internal error: current kernel command lines is unset")
   332  	}
   333  	// this is the current expected command line which was recorded by
   334  	// bootstate
   335  	cmdline := m.CurrentKernelCommandLines[0]
   336  	// this is the new expected command line
   337  	var candidateCmdline string
   338  	switch reason {
   339  	case commandLineUpdateReasonSnapd:
   340  		// pending boot config update
   341  		candidateCmdline, err = ComposeCandidateCommandLine(model, gadgetSnapOrDir)
   342  	case commandLineUpdateReasonGadget:
   343  		// pending gadget update
   344  		candidateCmdline, err = ComposeCommandLine(model, gadgetSnapOrDir)
   345  	}
   346  	if err != nil {
   347  		return false, err
   348  	}
   349  	if cmdline == candidateCmdline {
   350  		// command line is the same or no actual change in modeenv
   351  		return false, nil
   352  	}
   353  	// actual change of the command line content
   354  	m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline}
   355  
   356  	if err := m.Write(); err != nil {
   357  		return false, err
   358  	}
   359  
   360  	expectReseal := true
   361  	if err := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal); err != nil {
   362  		return false, err
   363  	}
   364  	return true, nil
   365  }
   366  
   367  // kernelCommandLinesForResealWithFallback provides the list of kernel command
   368  // lines for use during reseal. During normal operation, the command lines will
   369  // be listed in the modeenv.
   370  func kernelCommandLinesForResealWithFallback(modeenv *Modeenv) (cmdlines []string, err error) {
   371  	if len(modeenv.CurrentKernelCommandLines) > 0 {
   372  		return modeenv.CurrentKernelCommandLines, nil
   373  	}
   374  	// fallback for when reseal is called before mark boot successful set a
   375  	// default during snapd update, since this is a compatibility scenario
   376  	// there would be no kernel command lines arguments coming from the
   377  	// gadget either
   378  	gadgetDir := ""
   379  	cmdline, err := composeCommandLine(currentEdition, ModeRun, "", gadgetDir)
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  	return []string{cmdline}, nil
   384  }