github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/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  func dropFromRecoverySystemsList(systemsList []string, systemLabel string) (newList []string, found bool) {
    32  	for idx, sys := range systemsList {
    33  		if sys == systemLabel {
    34  			return append(systemsList[:idx], systemsList[idx+1:]...), true
    35  		}
    36  	}
    37  	return systemsList, false
    38  }
    39  
    40  // ClearTryRecoverySystem removes a given candidate recovery system and clears
    41  // the try model in the modeenv state file, then reseals and clears related
    42  // bootloader variables. An empty system label can be passed when the boot
    43  // variables state is inconsistent.
    44  func ClearTryRecoverySystem(dev Device, systemLabel string) error {
    45  	if !dev.HasModeenv() {
    46  		return fmt.Errorf("internal error: recovery systems can only be used on UC20")
    47  	}
    48  
    49  	m, err := loadModeenv()
    50  	if err != nil {
    51  		return err
    52  	}
    53  	opts := &bootloader.Options{
    54  		// setup the recovery bootloader
    55  		Role: bootloader.RoleRecovery,
    56  	}
    57  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	modified := false
    63  	// we may be repeating the cleanup, in which case the system was already
    64  	// removed from the modeenv and we don't need to rewrite the modeenv
    65  	if updated, found := dropFromRecoverySystemsList(m.CurrentRecoverySystems, systemLabel); found {
    66  		m.CurrentRecoverySystems = updated
    67  		modified = true
    68  	}
    69  	if m.TryModel != "" {
    70  		// recovery system is tried with a matching models
    71  		m.clearTryModel()
    72  		modified = true
    73  	}
    74  	if modified {
    75  		if err := m.Write(); err != nil {
    76  			return err
    77  		}
    78  	}
    79  
    80  	// clear both variables, no matter the values they hold
    81  	vars := map[string]string{
    82  		"try_recovery_system":    "",
    83  		"recovery_system_status": "",
    84  	}
    85  	// try to clear regardless of reseal failing
    86  	blErr := bl.SetBootVars(vars)
    87  
    88  	// but we still want to reseal, in case the cleanup did not reach this
    89  	// point before
    90  	const expectReseal = true
    91  	resealErr := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal)
    92  
    93  	if resealErr != nil {
    94  		return resealErr
    95  	}
    96  	return blErr
    97  }
    98  
    99  // SetTryRecoverySystem sets up the boot environment for trying out a recovery
   100  // system with given label in the context of the provided device. The call adds
   101  // the new system to the list of current recovery systems in the modeenv, and
   102  // optionally sets a try model, if the device model is different from the
   103  // current one, which typically can happen during a remodel. Once done, the
   104  // caller should request switching to the given recovery system.
   105  func SetTryRecoverySystem(dev Device, systemLabel string) (err error) {
   106  	if !dev.HasModeenv() {
   107  		return fmt.Errorf("internal error: recovery systems can only be used on UC20")
   108  	}
   109  
   110  	m, err := loadModeenv()
   111  	if err != nil {
   112  		return err
   113  	}
   114  	opts := &bootloader.Options{
   115  		// setup the recovery bootloader
   116  		Role: bootloader.RoleRecovery,
   117  	}
   118  	// TODO:UC20: seed may need to be switched to RW
   119  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	modified := false
   125  	// we could have rebooted before resealing the keys
   126  	if !strutil.ListContains(m.CurrentRecoverySystems, systemLabel) {
   127  		m.CurrentRecoverySystems = append(m.CurrentRecoverySystems, systemLabel)
   128  		modified = true
   129  
   130  	}
   131  	// we either have the current device context, in which case the model
   132  	// will match the current model in the modeenv, or a remodel device
   133  	// context carrying a new model, for which we may need to set the try
   134  	// model in the modeenv
   135  	model := dev.Model()
   136  	if modelUniqueID(model) != modelUniqueID(m.ModelForSealing()) {
   137  		// recovery system is tried with a matching model
   138  		m.setTryModel(model)
   139  		modified = true
   140  	}
   141  	if modified {
   142  		if err := m.Write(); err != nil {
   143  			return err
   144  		}
   145  	}
   146  
   147  	defer func() {
   148  		if err == nil {
   149  			return
   150  		}
   151  		if cleanupErr := ClearTryRecoverySystem(dev, systemLabel); cleanupErr != nil {
   152  			err = fmt.Errorf("%v (cleanup failed: %v)", err, cleanupErr)
   153  		}
   154  	}()
   155  
   156  	// even when we unexpectedly reboot after updating the bootenv here, we
   157  	// should not boot into the tried system, as the caller must explicitly
   158  	// request that by other means
   159  	vars := map[string]string{
   160  		"try_recovery_system":    systemLabel,
   161  		"recovery_system_status": "try",
   162  	}
   163  	if err := bl.SetBootVars(vars); err != nil {
   164  		return err
   165  	}
   166  
   167  	// until the keys are resealed, even if we unexpectedly boot into the
   168  	// tried system, data will still be inaccessible and the system will be
   169  	// considered as nonoperational
   170  	const expectReseal = true
   171  	return resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal)
   172  }
   173  
   174  type errInconsistentRecoverySystemState struct {
   175  	why string
   176  }
   177  
   178  func (e *errInconsistentRecoverySystemState) Error() string { return e.why }
   179  func IsInconsistentRecoverySystemState(err error) bool {
   180  	_, ok := err.(*errInconsistentRecoverySystemState)
   181  	return ok
   182  }
   183  
   184  // InitramfsIsTryingRecoverySystem, typically called while in initramfs of
   185  // recovery mode system, checks whether the boot variables indicate that the
   186  // given recovery system is only being tried. When the state of boot variables
   187  // is inconsistent, eg. status indicates that a recovery system is to be tried,
   188  // but the label is unset, a specific error which can be tested with
   189  // IsInconsystemRecoverySystemState() is returned.
   190  func InitramfsIsTryingRecoverySystem(currentSystemLabel string) (bool, error) {
   191  	opts := &bootloader.Options{
   192  		// setup the recovery bootloader
   193  		Role: bootloader.RoleRecovery,
   194  	}
   195  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   196  	if err != nil {
   197  		return false, err
   198  	}
   199  
   200  	vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status")
   201  	if err != nil {
   202  		return false, err
   203  	}
   204  
   205  	status := vars["recovery_system_status"]
   206  	switch status {
   207  	case "":
   208  		// not trying any recovery systems right now
   209  		return false, nil
   210  	case "try", "tried":
   211  		// both are valid options, where tried may indicate there was an
   212  		// unexpected reboot somewhere along the path of getting back to
   213  		// the run system
   214  	default:
   215  		return false, &errInconsistentRecoverySystemState{
   216  			why: fmt.Sprintf("unexpected recovery system status %q", status),
   217  		}
   218  	}
   219  
   220  	trySystem := vars["try_recovery_system"]
   221  	if trySystem == "" {
   222  		// XXX: could we end up with one variable set and the other not?
   223  		return false, &errInconsistentRecoverySystemState{
   224  			why: fmt.Sprintf("try recovery system is unset but status is %q", status),
   225  		}
   226  	}
   227  
   228  	if trySystem == currentSystemLabel {
   229  		// we are running a recovery system indicated in the boot
   230  		// variables, which may or may not be considered good at this
   231  		// point, nonetheless we are in recover mode and thus consider
   232  		// the system as being tried
   233  
   234  		// note, with status set to 'tried', we may be back to the
   235  		// tried system again, most likely due to an unexpected reboot
   236  		// when coming back to run mode
   237  		return true, nil
   238  	}
   239  	// we may still be running an actual recovery system if such mode was
   240  	// requested
   241  	return false, nil
   242  }
   243  
   244  type TryRecoverySystemOutcome int
   245  
   246  const (
   247  	TryRecoverySystemOutcomeFailure TryRecoverySystemOutcome = iota
   248  	TryRecoverySystemOutcomeSuccess
   249  	// TryRecoverySystemOutcomeInconsistent indicates that the booted try
   250  	// recovery system state was incorrect and corresponding boot variables
   251  	// need to be cleared
   252  	TryRecoverySystemOutcomeInconsistent
   253  	// TryRecoverySystemOutcomeNoneTried indicates a state in which no
   254  	// recovery system has been tried
   255  	TryRecoverySystemOutcomeNoneTried
   256  )
   257  
   258  // EnsureNextBootToRunModeWithTryRecoverySystemOutcome, typically called while
   259  // in initramfs, updates the boot environment to indicate an outcome of trying
   260  // out a recovery system and sets the system up to boot into run mode. It is up
   261  // to the caller to ensure the status is updated for the right recovery system,
   262  // typically by calling InitramfsIsTryingRecoverySystem beforehand.
   263  func EnsureNextBootToRunModeWithTryRecoverySystemOutcome(outcome TryRecoverySystemOutcome) error {
   264  	opts := &bootloader.Options{
   265  		// setup the recovery bootloader
   266  		Role: bootloader.RoleRecovery,
   267  	}
   268  	// TODO:UC20: seed may need to be switched to RW
   269  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   270  	if err != nil {
   271  		return err
   272  	}
   273  	vars := map[string]string{
   274  		// always going to back to run mode
   275  		"snapd_recovery_mode":    "run",
   276  		"snapd_recovery_system":  "",
   277  		"recovery_system_status": "try",
   278  	}
   279  	switch outcome {
   280  	case TryRecoverySystemOutcomeFailure:
   281  		// already set up for this scenario
   282  	case TryRecoverySystemOutcomeSuccess:
   283  		vars["recovery_system_status"] = "tried"
   284  	case TryRecoverySystemOutcomeInconsistent:
   285  		// there may be an unexpected status, or the tried system label
   286  		// is unset, in either case, clear the status
   287  		vars["recovery_system_status"] = ""
   288  	}
   289  	return bl.SetBootVars(vars)
   290  }
   291  
   292  func observeSuccessfulSystems(model *asserts.Model, m *Modeenv) (*Modeenv, error) {
   293  	// updates happen in run mode only
   294  	if m.Mode != "run" {
   295  		return m, nil
   296  	}
   297  
   298  	// compatibility scenario, no good systems are tracked in modeenv yet,
   299  	// and there is a single entry in the current systems list
   300  	if len(m.GoodRecoverySystems) == 0 && len(m.CurrentRecoverySystems) == 1 {
   301  		newM, err := m.Copy()
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  		newM.GoodRecoverySystems = []string{m.CurrentRecoverySystems[0]}
   306  		return newM, nil
   307  	}
   308  	return m, nil
   309  }
   310  
   311  // InspectTryRecoverySystemOutcome obtains a tried recovery system status. When
   312  // no recovery system has been tried, the outcome will be
   313  // TryRecoverySystemOutcomeNoneTried. The caller is responsible for clearing the
   314  // bootenv once the status bas been properly acted on.
   315  func InspectTryRecoverySystemOutcome(dev Device) (outcome TryRecoverySystemOutcome, label string, err error) {
   316  	opts := &bootloader.Options{
   317  		// setup the recovery bootloader
   318  		Role: bootloader.RoleRecovery,
   319  	}
   320  	// TODO:UC20: seed may need to be switched to RW
   321  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   322  	if err != nil {
   323  		return TryRecoverySystemOutcomeFailure, "", err
   324  	}
   325  
   326  	vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status")
   327  	if err != nil {
   328  		return TryRecoverySystemOutcomeFailure, "", err
   329  	}
   330  	status := vars["recovery_system_status"]
   331  	trySystem := vars["try_recovery_system"]
   332  
   333  	outcome = TryRecoverySystemOutcomeFailure
   334  	switch {
   335  	case status == "" && trySystem == "":
   336  		// simplest case, not trying a system
   337  		return TryRecoverySystemOutcomeNoneTried, "", nil
   338  	case status != "try" && status != "tried":
   339  		// system label is set, but the status is unexpected status
   340  		return TryRecoverySystemOutcomeInconsistent, "", &errInconsistentRecoverySystemState{
   341  			why: fmt.Sprintf("unexpected recovery system status %q", status),
   342  		}
   343  	case trySystem == "":
   344  		// no system set, but we have status
   345  		return TryRecoverySystemOutcomeInconsistent, "", &errInconsistentRecoverySystemState{
   346  			why: fmt.Sprintf("try recovery system is unset but status is %q", status),
   347  		}
   348  	case status == "tried":
   349  		// check that try_recovery_system ended up in the modeenv's
   350  		// CurrentRecoverySystems
   351  		m, err := ReadModeenv("")
   352  		if err != nil {
   353  			return TryRecoverySystemOutcomeFailure, trySystem, err
   354  		}
   355  
   356  		found := false
   357  		for _, sys := range m.CurrentRecoverySystems {
   358  			if sys == trySystem {
   359  				found = true
   360  			}
   361  		}
   362  		if !found {
   363  			return TryRecoverySystemOutcomeFailure, trySystem, &errInconsistentRecoverySystemState{
   364  				why: fmt.Sprintf("recovery system %q was tried, but is not present in the modeenv CurrentRecoverySystems", trySystem),
   365  			}
   366  		}
   367  
   368  		outcome = TryRecoverySystemOutcomeSuccess
   369  	}
   370  
   371  	return outcome, trySystem, nil
   372  }
   373  
   374  // PromoteTriedRecoverySystem promotes the provided recovery system to be
   375  // recognized as a good one, and ensures that the system is present in the list
   376  // of good recovery systems and current recovery systems in modeenv. The
   377  // provided list of tried systems should contain the system in question. If the
   378  // system uses encryption, the keys will updated state. If resealing fails, an
   379  // attempt to restore the previous state is made
   380  func PromoteTriedRecoverySystem(dev Device, systemLabel string, triedSystems []string) (err error) {
   381  	if !dev.HasModeenv() {
   382  		return fmt.Errorf("internal error: recovery systems can only be used on UC20")
   383  	}
   384  
   385  	if !strutil.ListContains(triedSystems, systemLabel) {
   386  		// system is not among the tried systems
   387  		return fmt.Errorf("system has not been successfully tried")
   388  	}
   389  
   390  	m, err := loadModeenv()
   391  	if err != nil {
   392  		return err
   393  	}
   394  	rewriteModeenv := false
   395  	if !strutil.ListContains(m.CurrentRecoverySystems, systemLabel) {
   396  		m.CurrentRecoverySystems = append(m.CurrentRecoverySystems, systemLabel)
   397  		rewriteModeenv = true
   398  	}
   399  	if !strutil.ListContains(m.GoodRecoverySystems, systemLabel) {
   400  		m.GoodRecoverySystems = append(m.GoodRecoverySystems, systemLabel)
   401  		rewriteModeenv = true
   402  	}
   403  	if rewriteModeenv {
   404  		if err := m.Write(); err != nil {
   405  			return err
   406  		}
   407  	}
   408  
   409  	const expectReseal = true
   410  	if err := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal); err != nil {
   411  		if cleanupErr := DropRecoverySystem(dev, systemLabel); cleanupErr != nil {
   412  			err = fmt.Errorf("%v (cleanup failed: %v)", err, cleanupErr)
   413  		}
   414  		return err
   415  	}
   416  	return nil
   417  }
   418  
   419  // DropRecoverySystem drops a provided system from the list of good and current
   420  // recovery systems, updates the modeenv and reseals the keys a needed. Note,
   421  // this call *DOES NOT* clear the boot environment variables.
   422  func DropRecoverySystem(dev Device, systemLabel string) error {
   423  	if !dev.HasModeenv() {
   424  		return fmt.Errorf("internal error: recovery systems can only be used on UC20")
   425  	}
   426  
   427  	m, err := loadModeenv()
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	rewriteModeenv := false
   433  	if updatedGood, found := dropFromRecoverySystemsList(m.GoodRecoverySystems, systemLabel); found {
   434  		m.GoodRecoverySystems = updatedGood
   435  		rewriteModeenv = true
   436  	}
   437  	if updatedCurrent, found := dropFromRecoverySystemsList(m.CurrentRecoverySystems, systemLabel); found {
   438  		m.CurrentRecoverySystems = updatedCurrent
   439  		rewriteModeenv = true
   440  	}
   441  	if rewriteModeenv {
   442  		if err := m.Write(); err != nil {
   443  			return err
   444  		}
   445  	}
   446  
   447  	const expectReseal = true
   448  	return resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal)
   449  }