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 }