github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/boot/systems.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  	"fmt"
    24  
    25  	"github.com/snapcore/snapd/asserts"
    26  	"github.com/snapcore/snapd/bootloader"
    27  	"github.com/snapcore/snapd/dirs"
    28  	"github.com/snapcore/snapd/strutil"
    29  )
    30  
    31  // ClearTryRecoverySystem removes a given candidate recovery system from the
    32  // modeenv state file, reseals and clears related bootloader variables. An empty
    33  // system label can be passed when the boot variables state is inconsistent.
    34  func ClearTryRecoverySystem(dev Device, systemLabel string) error {
    35  	if !dev.HasModeenv() {
    36  		return fmt.Errorf("internal error: recovery systems can only be used on UC20")
    37  	}
    38  
    39  	m, err := loadModeenv()
    40  	if err != nil {
    41  		return err
    42  	}
    43  	opts := &bootloader.Options{
    44  		// setup the recovery bootloader
    45  		Role: bootloader.RoleRecovery,
    46  	}
    47  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	found := false
    53  	for idx, sys := range m.CurrentRecoverySystems {
    54  		if sys == systemLabel {
    55  			found = true
    56  			m.CurrentRecoverySystems = append(m.CurrentRecoverySystems[:idx],
    57  				m.CurrentRecoverySystems[idx+1:]...)
    58  			break
    59  		}
    60  	}
    61  	// we may be repeating the cleanup, in which case the system was already
    62  	// removed from the modeenv and we don't need to rewrite the modeenv
    63  	if found {
    64  		if err := m.Write(); err != nil {
    65  			return err
    66  		}
    67  	}
    68  	// clear both variables, no matter the values they hold
    69  	vars := map[string]string{
    70  		"try_recovery_system":    "",
    71  		"recovery_system_status": "",
    72  	}
    73  	// try to clear regardless of reseal failing
    74  	blErr := bl.SetBootVars(vars)
    75  
    76  	// but we still want to reseal, in case the cleanup did not reach this
    77  	// point before
    78  	const expectReseal = true
    79  	resealErr := resealKeyToModeenv(dirs.GlobalRootDir, dev.Model(), m, expectReseal)
    80  
    81  	if resealErr != nil {
    82  		return resealErr
    83  	}
    84  	return blErr
    85  }
    86  
    87  // SetTryRecoverySystem sets up the boot environment for trying out a recovery
    88  // system with given label and adds the new system to the list of current
    89  // recovery systems in the modeenv. Once done, the caller should request
    90  // switching to the given recovery system.
    91  func SetTryRecoverySystem(dev Device, systemLabel string) (err error) {
    92  	if !dev.HasModeenv() {
    93  		return fmt.Errorf("internal error: recovery systems can only be used on UC20")
    94  	}
    95  
    96  	m, err := loadModeenv()
    97  	if err != nil {
    98  		return err
    99  	}
   100  	opts := &bootloader.Options{
   101  		// setup the recovery bootloader
   102  		Role: bootloader.RoleRecovery,
   103  	}
   104  	// TODO:UC20: seed may need to be switched to RW
   105  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	// we could have rebooted before resealing the keys
   111  	if !strutil.ListContains(m.CurrentRecoverySystems, systemLabel) {
   112  		m.CurrentRecoverySystems = append(m.CurrentRecoverySystems, systemLabel)
   113  
   114  		if err := m.Write(); err != nil {
   115  			return err
   116  		}
   117  	}
   118  
   119  	defer func() {
   120  		if err == nil {
   121  			return
   122  		}
   123  		if cleanupErr := ClearTryRecoverySystem(dev, systemLabel); cleanupErr != nil {
   124  			err = fmt.Errorf("%v (cleanup failed: %v)", err, cleanupErr)
   125  		}
   126  	}()
   127  
   128  	// even when we unexpectedly reboot after updating the bootenv here, we
   129  	// should not boot into the tried system, as the caller must explicitly
   130  	// request that by other means
   131  	vars := map[string]string{
   132  		"try_recovery_system":    systemLabel,
   133  		"recovery_system_status": "try",
   134  	}
   135  	if err := bl.SetBootVars(vars); err != nil {
   136  		return err
   137  	}
   138  
   139  	// until the keys are resealed, even if we unexpectedly boot into the
   140  	// tried system, data will still be inaccessible and the system will be
   141  	// considered as nonoperational
   142  	const expectReseal = true
   143  	return resealKeyToModeenv(dirs.GlobalRootDir, dev.Model(), m, expectReseal)
   144  }
   145  
   146  type errInconsistentRecoverySystemState struct {
   147  	why string
   148  }
   149  
   150  func (e *errInconsistentRecoverySystemState) Error() string { return e.why }
   151  func IsInconsistentRecoverySystemState(err error) bool {
   152  	_, ok := err.(*errInconsistentRecoverySystemState)
   153  	return ok
   154  }
   155  
   156  // InitramfsIsTryingRecoverySystem, typically called while in initramfs of
   157  // recovery mode system, checks whether the boot variables indicate that the
   158  // given recovery system is only being tried. When the state of boot variables
   159  // is inconsistent, eg. status indicates that a recovery system is to be tried,
   160  // but the label is unset, a specific error which can be tested with
   161  // IsInconsystemRecoverySystemState() is returned.
   162  func InitramfsIsTryingRecoverySystem(currentSystemLabel string) (bool, error) {
   163  	opts := &bootloader.Options{
   164  		// setup the recovery bootloader
   165  		Role: bootloader.RoleRecovery,
   166  	}
   167  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   168  	if err != nil {
   169  		return false, err
   170  	}
   171  
   172  	vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status")
   173  	if err != nil {
   174  		return false, err
   175  	}
   176  
   177  	status := vars["recovery_system_status"]
   178  	switch status {
   179  	case "":
   180  		// not trying any recovery systems right now
   181  		return false, nil
   182  	case "try", "tried":
   183  		// both are valid options, where tried may indicate there was an
   184  		// unexpected reboot somewhere along the path of getting back to
   185  		// the run system
   186  	default:
   187  		return false, &errInconsistentRecoverySystemState{
   188  			why: fmt.Sprintf("unexpected recovery system status %q", status),
   189  		}
   190  	}
   191  
   192  	trySystem := vars["try_recovery_system"]
   193  	if trySystem == "" {
   194  		// XXX: could we end up with one variable set and the other not?
   195  		return false, &errInconsistentRecoverySystemState{
   196  			why: fmt.Sprintf("try recovery system is unset but status is %q", status),
   197  		}
   198  	}
   199  
   200  	if trySystem == currentSystemLabel {
   201  		// we are running a recovery system indicated in the boot
   202  		// variables, which may or may not be considered good at this
   203  		// point, nonetheless we are in recover mode and thus consider
   204  		// the system as being tried
   205  
   206  		// note, with status set to 'tried', we may be back to the
   207  		// tried system again, most likely due to an unexpected reboot
   208  		// when coming back to run mode
   209  		return true, nil
   210  	}
   211  	// we may still be running an actual recovery system if such mode was
   212  	// requested
   213  	return false, nil
   214  }
   215  
   216  type TryRecoverySystemOutcome int
   217  
   218  const (
   219  	TryRecoverySystemOutcomeFailure TryRecoverySystemOutcome = iota
   220  	TryRecoverySystemOutcomeSuccess
   221  	// TryRecoverySystemOutcomeInconsistent indicates that the booted try
   222  	// recovery system state was incorrect and corresponding boot variables
   223  	// need to be cleared
   224  	TryRecoverySystemOutcomeInconsistent
   225  	// TryRecoverySystemOutcomeNoneTried indicates a state in which no
   226  	// recovery system has been tried
   227  	TryRecoverySystemOutcomeNoneTried
   228  )
   229  
   230  // EnsureNextBootToRunModeWithTryRecoverySystemOutcome, typically called while
   231  // in initramfs, updates the boot environment to indicate an outcome of trying
   232  // out a recovery system and sets the system up to boot into run mode. It is up
   233  // to the caller to ensure the status is updated for the right recovery system,
   234  // typically by calling InitramfsIsTryingRecoverySystem beforehand.
   235  func EnsureNextBootToRunModeWithTryRecoverySystemOutcome(outcome TryRecoverySystemOutcome) error {
   236  	opts := &bootloader.Options{
   237  		// setup the recovery bootloader
   238  		Role: bootloader.RoleRecovery,
   239  	}
   240  	// TODO:UC20: seed may need to be switched to RW
   241  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	vars := map[string]string{
   246  		// always going to back to run mode
   247  		"snapd_recovery_mode":    "run",
   248  		"snapd_recovery_system":  "",
   249  		"recovery_system_status": "try",
   250  	}
   251  	switch outcome {
   252  	case TryRecoverySystemOutcomeFailure:
   253  		// already set up for this scenario
   254  	case TryRecoverySystemOutcomeSuccess:
   255  		vars["recovery_system_status"] = "tried"
   256  	case TryRecoverySystemOutcomeInconsistent:
   257  		// there may be an unexpected status, or the tried system label
   258  		// is unset, in either case, clear the status
   259  		vars["recovery_system_status"] = ""
   260  	}
   261  	return bl.SetBootVars(vars)
   262  }
   263  
   264  func observeSuccessfulSystems(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   265  	// updates happen in run mode only
   266  	if m.Mode != "run" {
   267  		return m, nil
   268  	}
   269  
   270  	// compatibility scenario, no good systems are tracked in modeenv yet,
   271  	// and there is a single entry in the current systems list
   272  	if len(m.GoodRecoverySystems) == 0 && len(m.CurrentRecoverySystems) == 1 {
   273  		newM, err := m.Copy()
   274  		if err != nil {
   275  			return nil, err
   276  		}
   277  		newM.GoodRecoverySystems = []string{m.CurrentRecoverySystems[0]}
   278  		return newM, nil
   279  	}
   280  	return m, nil
   281  }
   282  
   283  // InspectTryRecoverySystemOutcome obtains a tried recovery system status. When
   284  // no recovery system has been tried, the outcome will be
   285  // TryRecoverySystemOutcomeNoneTried. The caller is responsible for clearing the
   286  // bootenv once the status bas been properly acted on.
   287  func InspectTryRecoverySystemOutcome(dev Device) (outcome TryRecoverySystemOutcome, label string, err error) {
   288  	opts := &bootloader.Options{
   289  		// setup the recovery bootloader
   290  		Role: bootloader.RoleRecovery,
   291  	}
   292  	// TODO:UC20: seed may need to be switched to RW
   293  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   294  	if err != nil {
   295  		return TryRecoverySystemOutcomeFailure, "", err
   296  	}
   297  
   298  	vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status")
   299  	if err != nil {
   300  		return TryRecoverySystemOutcomeFailure, "", err
   301  	}
   302  	status := vars["recovery_system_status"]
   303  	trySystem := vars["try_recovery_system"]
   304  
   305  	outcome = TryRecoverySystemOutcomeFailure
   306  	switch {
   307  	case status == "" && trySystem == "":
   308  		// simplest case, not trying a system
   309  		return TryRecoverySystemOutcomeNoneTried, "", nil
   310  	case status != "try" && status != "tried":
   311  		// system label is set, but the status is unexpected status
   312  		return TryRecoverySystemOutcomeInconsistent, "", &errInconsistentRecoverySystemState{
   313  			why: fmt.Sprintf("unexpected recovery system status %q", status),
   314  		}
   315  	case trySystem == "":
   316  		// no system set, but we have status
   317  		return TryRecoverySystemOutcomeInconsistent, "", &errInconsistentRecoverySystemState{
   318  			why: fmt.Sprintf("try recovery system is unset but status is %q", status),
   319  		}
   320  	case status == "tried":
   321  		outcome = TryRecoverySystemOutcomeSuccess
   322  	}
   323  
   324  	return outcome, trySystem, nil
   325  }