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