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 }