github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/cmd/snap-bootstrap/cmd_initramfs_mounts.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019-2020 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 main 21 22 import ( 23 "crypto/subtle" 24 "encoding/json" 25 "fmt" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "strings" 30 "syscall" 31 32 "github.com/jessevdk/go-flags" 33 34 "github.com/snapcore/snapd/asserts" 35 "github.com/snapcore/snapd/boot" 36 "github.com/snapcore/snapd/dirs" 37 "github.com/snapcore/snapd/logger" 38 "github.com/snapcore/snapd/osutil" 39 "github.com/snapcore/snapd/osutil/disks" 40 "github.com/snapcore/snapd/overlord/state" 41 "github.com/snapcore/snapd/secboot" 42 "github.com/snapcore/snapd/snap" 43 "github.com/snapcore/snapd/snap/squashfs" 44 "github.com/snapcore/snapd/sysconfig" 45 46 // to set sysconfig.ApplyFilesystemOnlyDefaultsImpl 47 _ "github.com/snapcore/snapd/overlord/configstate/configcore" 48 ) 49 50 func init() { 51 const ( 52 short = "Generate mounts for the initramfs" 53 long = "Generate and perform all mounts for the initramfs before transitioning to userspace" 54 ) 55 56 addCommandBuilder(func(parser *flags.Parser) { 57 if _, err := parser.AddCommand("initramfs-mounts", short, long, &cmdInitramfsMounts{}); err != nil { 58 panic(err) 59 } 60 }) 61 62 snap.SanitizePlugsSlots = func(*snap.Info) {} 63 } 64 65 type cmdInitramfsMounts struct{} 66 67 func (c *cmdInitramfsMounts) Execute(args []string) error { 68 return generateInitramfsMounts() 69 } 70 71 var ( 72 osutilIsMounted = osutil.IsMounted 73 74 snapTypeToMountDir = map[snap.Type]string{ 75 snap.TypeBase: "base", 76 snap.TypeKernel: "kernel", 77 snap.TypeSnapd: "snapd", 78 } 79 80 secbootMeasureSnapSystemEpochWhenPossible func() error 81 secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error 82 secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) 83 secbootUnlockEncryptedVolumeUsingKey func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) 84 85 secbootLockSealedKeys func() error 86 87 bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk 88 ) 89 90 func stampedAction(stamp string, action func() error) error { 91 stampFile := filepath.Join(dirs.SnapBootstrapRunDir, stamp) 92 if osutil.FileExists(stampFile) { 93 return nil 94 } 95 if err := os.MkdirAll(filepath.Dir(stampFile), 0755); err != nil { 96 return err 97 } 98 if err := action(); err != nil { 99 return err 100 } 101 return ioutil.WriteFile(stampFile, nil, 0644) 102 } 103 104 func generateInitramfsMounts() (err error) { 105 // ensure that the last thing we do is to lock access to sealed keys, 106 // regardless of mode or early failures. 107 defer func() { 108 if e := secbootLockSealedKeys(); e != nil { 109 e = fmt.Errorf("error locking access to sealed keys: %v", e) 110 if err == nil { 111 err = e 112 } else { 113 // preserve err but log 114 logger.Noticef("%v", e) 115 } 116 } 117 }() 118 119 // Ensure there is a very early initial measurement 120 err = stampedAction("secboot-epoch-measured", func() error { 121 return secbootMeasureSnapSystemEpochWhenPossible() 122 }) 123 if err != nil { 124 return err 125 } 126 127 mode, recoverySystem, err := boot.ModeAndRecoverySystemFromKernelCommandLine() 128 if err != nil { 129 return err 130 } 131 132 mst := &initramfsMountsState{ 133 mode: mode, 134 recoverySystem: recoverySystem, 135 } 136 137 switch mode { 138 case "recover": 139 return generateMountsModeRecover(mst) 140 case "install": 141 return generateMountsModeInstall(mst) 142 case "run": 143 return generateMountsModeRun(mst) 144 } 145 // this should never be reached 146 return fmt.Errorf("internal error: mode in generateInitramfsMounts not handled") 147 } 148 149 // generateMountsMode* is called multiple times from initramfs until it 150 // no longer generates more mount points and just returns an empty output. 151 func generateMountsModeInstall(mst *initramfsMountsState) error { 152 // steps 1 and 2 are shared with recover mode 153 model, snaps, err := generateMountsCommonInstallRecover(mst) 154 if err != nil { 155 return err 156 } 157 158 // 3. final step: write modeenv to tmpfs data dir and disable cloud-init in 159 // install mode 160 modeEnv, err := mst.EphemeralModeenvForModel(model, snaps) 161 if err != nil { 162 return err 163 } 164 if err := modeEnv.WriteTo(boot.InitramfsWritableDir); err != nil { 165 return err 166 } 167 168 // done, no output, no error indicates to initramfs we are done with 169 // mounting stuff 170 return nil 171 } 172 173 // copyNetworkConfig copies the network configuration to the target 174 // directory. This is used to copy the network configuration 175 // data from a real uc20 ubuntu-data partition into a ephemeral one. 176 func copyNetworkConfig(src, dst string) error { 177 for _, globEx := range []string{ 178 // for network configuration setup by console-conf, etc. 179 // TODO:UC20: we want some way to "try" or "verify" the network 180 // configuration or to only use known-to-be-good network 181 // configuration i.e. from ubuntu-save before installing it 182 // onto recover mode, because the network configuration could 183 // have been what was broken so we don't want to break 184 // network configuration for recover mode as well, but for 185 // now this is fine 186 "system-data/etc/netplan/*", 187 // etc/machine-id is part of what systemd-networkd uses to generate a 188 // DHCP clientid (the other part being the interface name), so to have 189 // the same IP addresses across run mode and recover mode, we need to 190 // also copy the machine-id across 191 "system-data/etc/machine-id", 192 } { 193 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 194 return err 195 } 196 } 197 return nil 198 } 199 200 // copyUbuntuDataMisc copies miscellaneous other files from the run mode system 201 // to the recover system such as: 202 // - timesync clock to keep the same time setting in recover as in run mode 203 func copyUbuntuDataMisc(src, dst string) error { 204 for _, globEx := range []string{ 205 // systemd's timesync clock file so that the time in recover mode moves 206 // forward to what it was in run mode 207 // NOTE: we don't sync back the time movement from recover mode to run 208 // mode currently, unclear how/when we could do this, but recover mode 209 // isn't meant to be long lasting and as such it's probably not a big 210 // problem to "lose" the time spent in recover mode 211 "system-data/var/lib/systemd/timesync/clock", 212 } { 213 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 214 return err 215 } 216 } 217 218 return nil 219 } 220 221 // copyUbuntuDataAuth copies the authentication files like 222 // - extrausers passwd,shadow etc 223 // - sshd host configuration 224 // - user .ssh dir 225 // to the target directory. This is used to copy the authentication 226 // data from a real uc20 ubuntu-data partition into a ephemeral one. 227 func copyUbuntuDataAuth(src, dst string) error { 228 for _, globEx := range []string{ 229 "system-data/var/lib/extrausers/*", 230 "system-data/etc/ssh/*", 231 "user-data/*/.ssh/*", 232 // this ensures we get proper authentication to snapd from "snap" 233 // commands in recover mode 234 "user-data/*/.snap/auth.json", 235 // this ensures we also get non-ssh enabled accounts copied 236 "user-data/*/.profile", 237 // so that users have proper perms, i.e. console-conf added users are 238 // sudoers 239 "system-data/etc/sudoers.d/*", 240 } { 241 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 242 return err 243 } 244 } 245 246 // ensure the user state is transferred as well 247 srcState := filepath.Join(src, "system-data/var/lib/snapd/state.json") 248 dstState := filepath.Join(dst, "system-data/var/lib/snapd/state.json") 249 err := state.CopyState(srcState, dstState, []string{"auth.users", "auth.macaroon-key", "auth.last-id"}) 250 if err != nil && err != state.ErrNoState { 251 return fmt.Errorf("cannot copy user state: %v", err) 252 } 253 254 return nil 255 } 256 257 // copySafeDefaultData will copy to the destination a "safe" set of data for 258 // a blank recover mode, i.e. one where we cannot copy authentication, etc. from 259 // the actual host ubuntu-data. Currently this is just a file to disable 260 // console-conf from running. 261 func copySafeDefaultData(dst string) error { 262 consoleConfCompleteFile := filepath.Join(dst, "system-data/var/lib/console-conf/complete") 263 if err := os.MkdirAll(filepath.Dir(consoleConfCompleteFile), 0755); err != nil { 264 return err 265 } 266 return ioutil.WriteFile(consoleConfCompleteFile, nil, 0644) 267 } 268 269 func copyFromGlobHelper(src, dst, globEx string) error { 270 matches, err := filepath.Glob(filepath.Join(src, globEx)) 271 if err != nil { 272 return err 273 } 274 for _, p := range matches { 275 comps := strings.Split(strings.TrimPrefix(p, src), "/") 276 for i := range comps { 277 part := filepath.Join(comps[0 : i+1]...) 278 fi, err := os.Stat(filepath.Join(src, part)) 279 if err != nil { 280 return err 281 } 282 if fi.IsDir() { 283 if err := os.Mkdir(filepath.Join(dst, part), fi.Mode()); err != nil && !os.IsExist(err) { 284 return err 285 } 286 st, ok := fi.Sys().(*syscall.Stat_t) 287 if !ok { 288 return fmt.Errorf("cannot get stat data: %v", err) 289 } 290 if err := os.Chown(filepath.Join(dst, part), int(st.Uid), int(st.Gid)); err != nil { 291 return err 292 } 293 } else { 294 if err := osutil.CopyFile(p, filepath.Join(dst, part), osutil.CopyFlagPreserveAll); err != nil { 295 return err 296 } 297 } 298 } 299 } 300 301 return nil 302 } 303 304 // states for partition state 305 const ( 306 // states for LocateState 307 partitionFound = "found" 308 partitionNotFound = "not-found" 309 partitionErrFinding = "error-finding" 310 // states for MountState 311 partitionMounted = "mounted" 312 partitionErrMounting = "error-mounting" 313 partitionAbsentOptional = "absent-but-optional" 314 partitionMountedUntrusted = "mounted-untrusted" 315 // states for UnlockState 316 partitionUnlocked = "unlocked" 317 partitionErrUnlocking = "error-unlocking" 318 // keys used to unlock for UnlockKey 319 keyRun = "run" 320 keyFallback = "fallback" 321 keyRecovery = "recovery" 322 ) 323 324 // partitionState is the state of a partition after recover mode has completed 325 // for degraded mode. 326 type partitionState struct { 327 // MountState is whether the partition was mounted successfully or not. 328 MountState string `json:"mount-state,omitempty"` 329 // MountLocation is where the partition was mounted. 330 MountLocation string `json:"mount-location,omitempty"` 331 // Device is what device the partition corresponds to. It can be the 332 // physical block device if the partition is unencrypted or if it was not 333 // successfully unlocked, or it can be a decrypted mapper device if the 334 // partition was encrypted and successfully decrypted, or it can be the 335 // empty string (or missing) if the partition was not found at all. 336 Device string `json:"device,omitempty"` 337 // FindState indicates whether the partition was found on the disk or not. 338 FindState string `json:"find-state,omitempty"` 339 // UnlockState was whether the partition was unlocked successfully or not. 340 UnlockState string `json:"unlock-state,omitempty"` 341 // UnlockKey was what key the partition was unlocked with, either "run", 342 // "fallback" or "recovery". 343 UnlockKey string `json:"unlock-key,omitempty"` 344 345 // unexported internal fields for tracking the device, these are used during 346 // state machine execution, and then combined into Device during finalize() 347 // for simple representation to the consumer of degraded.json 348 349 // fsDevice is what decrypted mapper device corresponds to the 350 // partition, it can have the following states 351 // - successfully decrypted => the decrypted mapper device 352 // - unencrypted => the block device of the partition 353 // - identified as decrypted, but failed to decrypt => empty string 354 fsDevice string 355 // partDevice is always the physical block device of the partition, in the 356 // encrypted case this is the physical encrypted partition. 357 partDevice string 358 } 359 360 type recoverDegradedState struct { 361 // UbuntuData is the state of the ubuntu-data (or ubuntu-data-enc) 362 // partition. 363 UbuntuData partitionState `json:"ubuntu-data,omitempty"` 364 // UbuntuBoot is the state of the ubuntu-boot partition. 365 UbuntuBoot partitionState `json:"ubuntu-boot,omitempty"` 366 // UbuntuSave is the state of the ubuntu-save (or ubuntu-save-enc) 367 // partition. 368 UbuntuSave partitionState `json:"ubuntu-save,omitempty"` 369 // ErrorLog is the log of error messages encountered during recover mode 370 // setting up degraded mode. 371 ErrorLog []string `json:"error-log"` 372 } 373 374 func (r *recoverDegradedState) partition(part string) *partitionState { 375 switch part { 376 case "ubuntu-data": 377 return &r.UbuntuData 378 case "ubuntu-boot": 379 return &r.UbuntuBoot 380 case "ubuntu-save": 381 return &r.UbuntuSave 382 } 383 panic(fmt.Sprintf("unknown partition %s", part)) 384 } 385 386 func (r *recoverDegradedState) LogErrorf(format string, v ...interface{}) { 387 msg := fmt.Sprintf(format, v...) 388 r.ErrorLog = append(r.ErrorLog, msg) 389 logger.Noticef(msg) 390 } 391 392 // stateFunc is a function which executes a state action, returns the next 393 // function (for the next) state or nil if it is the final state. 394 type stateFunc func() (stateFunc, error) 395 396 // recoverModeStateMachine is a state machine implementing the logic for 397 // degraded recover mode. 398 // A full state diagram for the state machine can be found in 399 // /cmd/snap-bootstrap/degraded-recover-mode.svg in this repo. 400 type recoverModeStateMachine struct { 401 // the current state is the one that is about to be executed 402 current stateFunc 403 404 // device model 405 model *asserts.Model 406 407 // the disk we have all our partitions on 408 disk disks.Disk 409 410 // when true, the fallback unlock paths will not be tried 411 noFallback bool 412 413 // TODO:UC20: for clarity turn this into into tristate: 414 // unknown|encrypted|unencrypted 415 isEncryptedDev bool 416 417 // state for tracking what happens as we progress through degraded mode of 418 // recovery 419 degradedState *recoverDegradedState 420 } 421 422 // degraded returns whether a degraded recover mode state has fallen back from 423 // the typical operation to some sort of degraded mode. 424 func (m *recoverModeStateMachine) degraded() bool { 425 r := m.degradedState 426 427 if m.isEncryptedDev { 428 // for encrypted devices, we need to have ubuntu-save mounted 429 if r.UbuntuSave.MountState != partitionMounted { 430 return true 431 } 432 433 // we also should have all the unlock keys as run keys 434 if r.UbuntuData.UnlockKey != keyRun { 435 return true 436 } 437 438 if r.UbuntuSave.UnlockKey != keyRun { 439 return true 440 } 441 } else { 442 // for unencrypted devices, ubuntu-save must either be mounted or 443 // absent-but-optional 444 if r.UbuntuSave.MountState != partitionMounted { 445 if r.UbuntuSave.MountState != partitionAbsentOptional { 446 return true 447 } 448 } 449 } 450 451 // ubuntu-boot and ubuntu-data should both be mounted 452 if r.UbuntuBoot.MountState != partitionMounted { 453 return true 454 } 455 if r.UbuntuData.MountState != partitionMounted { 456 return true 457 } 458 459 // TODO: should we also check MountLocation too? 460 461 // we should have nothing in the error log 462 if len(r.ErrorLog) != 0 { 463 return true 464 } 465 466 return false 467 } 468 469 func (m *recoverModeStateMachine) diskOpts() *disks.Options { 470 if m.isEncryptedDev { 471 return &disks.Options{ 472 IsDecryptedDevice: true, 473 } 474 } 475 return nil 476 } 477 478 func (m *recoverModeStateMachine) verifyMountPoint(dir, name string) error { 479 matches, err := m.disk.MountPointIsFromDisk(dir, m.diskOpts()) 480 if err != nil { 481 return err 482 } 483 if !matches { 484 return fmt.Errorf("cannot validate mount: %s mountpoint target %s is expected to be from disk %s but is not", name, dir, m.disk.Dev()) 485 } 486 return nil 487 } 488 489 func (m *recoverModeStateMachine) setFindState(partName, partUUID string, err error, optionalPartition bool) error { 490 part := m.degradedState.partition(partName) 491 if err != nil { 492 if _, ok := err.(disks.PartitionNotFoundError); ok { 493 // explicit error that the device was not found 494 part.FindState = partitionNotFound 495 if !optionalPartition { 496 // partition is not optional, thus the error is relevant 497 m.degradedState.LogErrorf("cannot find %v partition on disk %s", partName, m.disk.Dev()) 498 } 499 return nil 500 } 501 // the error is not "not-found", so we have a real error 502 part.FindState = partitionErrFinding 503 m.degradedState.LogErrorf("error finding %v partition on disk %s: %v", partName, m.disk.Dev(), err) 504 return nil 505 } 506 507 // device was found 508 part.FindState = partitionFound 509 dev := fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID) 510 part.partDevice = dev 511 part.fsDevice = dev 512 return nil 513 } 514 515 func (m *recoverModeStateMachine) setMountState(part, where string, err error) error { 516 if err != nil { 517 m.degradedState.LogErrorf("cannot mount %v: %v", part, err) 518 m.degradedState.partition(part).MountState = partitionErrMounting 519 return nil 520 } 521 522 m.degradedState.partition(part).MountState = partitionMounted 523 m.degradedState.partition(part).MountLocation = where 524 525 if err := m.verifyMountPoint(where, part); err != nil { 526 m.degradedState.LogErrorf("cannot verify %s mount point at %v: %v", part, where, err) 527 return err 528 } 529 return nil 530 } 531 532 func (m *recoverModeStateMachine) setUnlockStateWithRunKey(partName string, unlockRes secboot.UnlockResult, err error) error { 533 part := m.degradedState.partition(partName) 534 // save the device if we found it from secboot 535 if unlockRes.PartDevice != "" { 536 part.FindState = partitionFound 537 part.partDevice = unlockRes.PartDevice 538 part.fsDevice = unlockRes.FsDevice 539 } else { 540 part.FindState = partitionNotFound 541 } 542 if unlockRes.IsEncrypted { 543 m.isEncryptedDev = true 544 } 545 546 if err != nil { 547 // create different error message for encrypted vs unencrypted 548 if unlockRes.IsEncrypted { 549 // if we know the device is decrypted we must also always know at 550 // least the partDevice (which is the encrypted block device) 551 m.degradedState.LogErrorf("cannot unlock encrypted %s (device %s) with sealed run key: %v", partName, part.partDevice, err) 552 part.UnlockState = partitionErrUnlocking 553 } else { 554 // TODO: we don't know if this is a plain not found or a different error 555 m.degradedState.LogErrorf("cannot locate %s partition for mounting host data: %v", partName, err) 556 } 557 558 return nil 559 } 560 561 if unlockRes.IsEncrypted { 562 // unlocked successfully 563 part.UnlockState = partitionUnlocked 564 part.UnlockKey = keyRun 565 } 566 567 return nil 568 } 569 570 func (m *recoverModeStateMachine) setUnlockStateWithFallbackKey(partName string, unlockRes secboot.UnlockResult, err error) error { 571 // first check the result and error for consistency; since we are using udev 572 // there could be inconsistent results at different points in time 573 574 // TODO: consider refactoring UnlockVolumeUsingSealedKeyIfEncrypted to not 575 // also find the partition on the disk, that should eliminate this 576 // consistency checking as we can code it such that we don't get these 577 // possible inconsistencies 578 579 // do basic consistency checking on unlockRes to make sure the 580 // result makes sense. 581 if unlockRes.FsDevice != "" && err != nil { 582 // This case should be impossible to enter, we can't 583 // have a filesystem device but an error set 584 return fmt.Errorf("internal error: inconsistent return values from UnlockVolumeUsingSealedKeyIfEncrypted for partition %s: %v", partName, err) 585 } 586 587 part := m.degradedState.partition(partName) 588 // Also make sure that if we previously saw a partition device that we see 589 // the same device again. 590 if unlockRes.PartDevice != "" && part.partDevice != "" && unlockRes.PartDevice != part.partDevice { 591 return fmt.Errorf("inconsistent partitions found for %s: previously found %s but now found %s", partName, part.partDevice, unlockRes.PartDevice) 592 } 593 594 // ensure consistency between encrypted state of the device/disk and what we 595 // may have seen previously 596 if m.isEncryptedDev && !unlockRes.IsEncrypted { 597 // then we previously were able to positively identify an 598 // ubuntu-data-enc but can't anymore, so we have inconsistent results 599 // from inspecting the disk which is suspicious and we should fail 600 return fmt.Errorf("inconsistent disk encryption status: previous access resulted in encrypted, but now is unencrypted from partition %s", partName) 601 } 602 603 // now actually process the result into the state 604 if unlockRes.PartDevice != "" { 605 part.FindState = partitionFound 606 // Note that in some case this may be redundantly assigning the same 607 // value to partDevice again. 608 part.partDevice = unlockRes.PartDevice 609 part.fsDevice = unlockRes.FsDevice 610 } 611 612 // There are a few cases where this could be the first time that we found a 613 // decrypted device in the UnlockResult, but m.isEncryptedDev is still 614 // false. 615 // - The first case is if we couldn't find ubuntu-boot at all, in which case 616 // we can't use the run object keys from there and instead need to directly 617 // fallback to trying the fallback object keys from ubuntu-seed 618 // - The second case is if we couldn't identify an ubuntu-data-enc or an 619 // ubuntu-data partition at all, we still could have an ubuntu-save-enc 620 // partition in which case we maybe could still have an encrypted disk that 621 // needs unlocking with the fallback object keys from ubuntu-seed 622 // 623 // As such, if m.isEncryptedDev is false, but unlockRes.IsEncrypted is 624 // true, then it is safe to assign m.isEncryptedDev to true. 625 if !m.isEncryptedDev && unlockRes.IsEncrypted { 626 m.isEncryptedDev = true 627 } 628 629 if err != nil { 630 // create different error message for encrypted vs unencrypted 631 if m.isEncryptedDev { 632 m.degradedState.LogErrorf("cannot unlock encrypted %s partition with sealed fallback key: %v", partName, err) 633 part.UnlockState = partitionErrUnlocking 634 } else { 635 // if we don't have an encrypted device and err != nil, then the 636 // device must be not-found, see above checks 637 638 // log an error the partition is mandatory 639 m.degradedState.LogErrorf("cannot locate %s partition: %v", partName, err) 640 } 641 642 return nil 643 } 644 645 if m.isEncryptedDev { 646 // unlocked successfully 647 part.UnlockState = partitionUnlocked 648 649 // figure out which key/method we used to unlock the partition 650 switch unlockRes.UnlockMethod { 651 case secboot.UnlockedWithSealedKey: 652 part.UnlockKey = keyFallback 653 case secboot.UnlockedWithRecoveryKey: 654 part.UnlockKey = keyRecovery 655 656 // TODO: should we fail with internal error for default case here? 657 } 658 } 659 660 return nil 661 } 662 663 func newRecoverModeStateMachine(model *asserts.Model, disk disks.Disk, allowFallback bool) *recoverModeStateMachine { 664 m := &recoverModeStateMachine{ 665 model: model, 666 disk: disk, 667 degradedState: &recoverDegradedState{ 668 ErrorLog: []string{}, 669 }, 670 noFallback: !allowFallback, 671 } 672 // first step is to mount ubuntu-boot to check for run mode keys to unlock 673 // ubuntu-data 674 m.current = m.mountBoot 675 return m 676 } 677 678 func (m *recoverModeStateMachine) execute() (finished bool, err error) { 679 next, err := m.current() 680 m.current = next 681 finished = next == nil 682 if finished && err == nil { 683 if err := m.finalize(); err != nil { 684 return true, err 685 } 686 } 687 return finished, err 688 } 689 690 func (m *recoverModeStateMachine) finalize() error { 691 // check soundness 692 // the grade check makes sure that if data was mounted unencrypted 693 // but the model is secured it will end up marked as untrusted 694 isEncrypted := m.isEncryptedDev || m.model.StorageSafety() == asserts.StorageSafetyEncrypted 695 part := m.degradedState.partition("ubuntu-data") 696 if part.MountState == partitionMounted && isEncrypted { 697 // check that save and data match 698 // We want to avoid a chosen ubuntu-data 699 // (e.g. activated with a recovery key) to get access 700 // via its logins to the secrets in ubuntu-save (in 701 // particular the policy update auth key) 702 // TODO:UC20: we should try to be a bit more specific here in checking that 703 // data and save match, and not mark data as untrusted if we 704 // know that the real save is locked/protected (or doesn't exist 705 // in the case of bad corruption) because currently this code will 706 // mark data as untrusted, even if it was unlocked with the run 707 // object key and we failed to unlock ubuntu-save at all, which is 708 // undesirable. This effectively means that you need to have both 709 // ubuntu-data and ubuntu-save unlockable and have matching marker 710 // files in order to use the files from ubuntu-data to log-in, 711 // etc. 712 trustData, _ := checkDataAndSavePairing(boot.InitramfsHostWritableDir) 713 if !trustData { 714 part.MountState = partitionMountedUntrusted 715 m.degradedState.LogErrorf("cannot trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install") 716 } 717 } 718 719 // finally, combine the states of partDevice and fsDevice into the 720 // exported Device field for marshalling 721 // ubuntu-boot is easy - it will always be unencrypted so we just set 722 // Device to partDevice 723 m.degradedState.partition("ubuntu-boot").Device = m.degradedState.partition("ubuntu-boot").partDevice 724 725 // for ubuntu-data and save, we need to actually look at the states 726 for _, partName := range []string{"ubuntu-data", "ubuntu-save"} { 727 part := m.degradedState.partition(partName) 728 if part.fsDevice == "" { 729 // then the device is encrypted, but we failed to decrypt it, so 730 // set Device to the encrypted block device 731 part.Device = part.partDevice 732 } else { 733 // all other cases, fsDevice is set to what we want to 734 // export, either it is set to the decrypted mapper device in the 735 // case it was successfully decrypted, or it is set to the encrypted 736 // block device if we failed to decrypt it, or it was set to the 737 // unencrypted block device if it was unencrypted 738 part.Device = part.fsDevice 739 } 740 } 741 742 return nil 743 } 744 745 func (m *recoverModeStateMachine) trustData() bool { 746 return m.degradedState.partition("ubuntu-data").MountState == partitionMounted 747 } 748 749 // mountBoot is the first state to execute in the state machine, it can 750 // transition to the following states: 751 // - if ubuntu-boot is mounted successfully, execute unlockDataRunKey 752 // - if ubuntu-boot can't be mounted, execute unlockDataFallbackKey 753 // - if we mounted the wrong ubuntu-boot (or otherwise can't verify which one we 754 // mounted), return fatal error 755 func (m *recoverModeStateMachine) mountBoot() (stateFunc, error) { 756 part := m.degradedState.partition("ubuntu-boot") 757 // use the disk we mounted ubuntu-seed from as a reference to find 758 // ubuntu-seed and mount it 759 partUUID, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-boot") 760 const partitionMandatory = false 761 if err := m.setFindState("ubuntu-boot", partUUID, findErr, partitionMandatory); err != nil { 762 return nil, err 763 } 764 if part.FindState != partitionFound { 765 // if we didn't find ubuntu-boot, we can't try to unlock data with the 766 // run key, and should instead just jump straight to attempting to 767 // unlock with the fallback key 768 return m.unlockDataFallbackKey, nil 769 } 770 771 // should we fsck ubuntu-boot? probably yes because on some platforms 772 // (u-boot for example) ubuntu-boot is vfat and it could have been unmounted 773 // dirtily, and we need to fsck it to ensure it is mounted safely before 774 // reading keys from it 775 fsckSystemdOpts := &systemdMountOptions{ 776 NeedsFsck: true, 777 } 778 mountErr := doSystemdMount(part.fsDevice, boot.InitramfsUbuntuBootDir, fsckSystemdOpts) 779 if err := m.setMountState("ubuntu-boot", boot.InitramfsUbuntuBootDir, mountErr); err != nil { 780 return nil, err 781 } 782 if part.MountState == partitionErrMounting { 783 // if we didn't mount data, then try to unlock data with the 784 // fallback key 785 return m.unlockDataFallbackKey, nil 786 } 787 788 // next step try to unlock data with run object 789 return m.unlockDataRunKey, nil 790 } 791 792 // stateUnlockDataRunKey will try to unlock ubuntu-data with the normal run-mode 793 // key, and if it fails, progresses to the next state, which is either: 794 // - failed to unlock data, but we know it's an encrypted device -> try to unlock with fallback key 795 // - failed to find data at all -> try to unlock save 796 // - unlocked data with run key -> mount data 797 func (m *recoverModeStateMachine) unlockDataRunKey() (stateFunc, error) { 798 runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") 799 unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 800 // don't allow using the recovery key to unlock, we only try using the 801 // recovery key after we first try the fallback object 802 AllowRecoveryKey: false, 803 } 804 unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", runModeKey, unlockOpts) 805 if err := m.setUnlockStateWithRunKey("ubuntu-data", unlockRes, unlockErr); err != nil { 806 return nil, err 807 } 808 if unlockErr != nil { 809 // we couldn't unlock ubuntu-data with the primary key, or we didn't 810 // find it in the unencrypted case 811 if unlockRes.IsEncrypted { 812 // we know the device is encrypted, so the next state is to try 813 // unlocking with the fallback key 814 return m.unlockDataFallbackKey, nil 815 } 816 817 // if we didn't even find the device to the point where it would have 818 // been identified as decrypted or unencrypted device, we could have 819 // just entirely lost ubuntu-data-enc, and we could still have an 820 // encrypted device, so instead try to unlock ubuntu-save with the 821 // fallback key, the logic there can also handle an unencrypted ubuntu-save 822 return m.unlockMaybeEncryptedAloneSaveFallbackKey, nil 823 } 824 825 // otherwise successfully unlocked it (or just found it if it was unencrypted) 826 // so just mount it 827 return m.mountData, nil 828 } 829 830 func (m *recoverModeStateMachine) unlockDataFallbackKey() (stateFunc, error) { 831 if m.noFallback { 832 return nil, fmt.Errorf("cannot unlock ubuntu-data (fallback disabled)") 833 } 834 835 // try to unlock data with the fallback key on ubuntu-seed, which must have 836 // been mounted at this point 837 unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 838 // we want to allow using the recovery key if the fallback key fails as 839 // using the fallback object is the last chance before we give up trying 840 // to unlock data 841 AllowRecoveryKey: true, 842 } 843 // TODO: this prompts for a recovery key 844 // TODO: we should somehow customize the prompt to mention what key we need 845 // the user to enter, and what we are unlocking (as currently the prompt 846 // says "recovery key" and the partition UUID for what is being unlocked) 847 dataFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key") 848 unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", dataFallbackKey, unlockOpts) 849 if err := m.setUnlockStateWithFallbackKey("ubuntu-data", unlockRes, unlockErr); err != nil { 850 return nil, err 851 } 852 if unlockErr != nil { 853 // skip trying to mount data, since we did not unlock data we cannot 854 // open save with with the run key, so try the fallback one 855 return m.unlockEncryptedSaveFallbackKey, nil 856 } 857 858 // unlocked it, now go mount it 859 return m.mountData, nil 860 } 861 862 func (m *recoverModeStateMachine) mountData() (stateFunc, error) { 863 data := m.degradedState.partition("ubuntu-data") 864 // don't do fsck on the data partition, it could be corrupted 865 mountErr := doSystemdMount(data.fsDevice, boot.InitramfsHostUbuntuDataDir, nil) 866 if err := m.setMountState("ubuntu-data", boot.InitramfsHostUbuntuDataDir, mountErr); err != nil { 867 return nil, err 868 } 869 if m.isEncryptedDev { 870 if mountErr == nil { 871 // if we succeeded in mounting data and we are encrypted, the next step 872 // is to unlock save with the run key from ubuntu-data 873 return m.unlockEncryptedSaveRunKey, nil 874 } else { 875 // we are encrypted and we failed to mount data successfully, meaning we 876 // don't have the bare key from ubuntu-data to use, and need to fall back 877 // to the sealed key from ubuntu-seed 878 return m.unlockEncryptedSaveFallbackKey, nil 879 } 880 } 881 882 // the data is not encrypted, in which case the ubuntu-save, if it 883 // exists, will be plain too 884 return m.openUnencryptedSave, nil 885 } 886 887 func (m *recoverModeStateMachine) unlockEncryptedSaveRunKey() (stateFunc, error) { 888 // to get to this state, we needed to have mounted ubuntu-data on host, so 889 // if encrypted, we can try to read the run key from host ubuntu-data 890 saveKey := filepath.Join(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir), "ubuntu-save.key") 891 key, err := ioutil.ReadFile(saveKey) 892 if err != nil { 893 // log the error and skip to trying the fallback key 894 m.degradedState.LogErrorf("cannot access run ubuntu-save key: %v", err) 895 return m.unlockEncryptedSaveFallbackKey, nil 896 } 897 898 unlockRes, unlockErr := secbootUnlockEncryptedVolumeUsingKey(m.disk, "ubuntu-save", key) 899 if err := m.setUnlockStateWithRunKey("ubuntu-save", unlockRes, unlockErr); err != nil { 900 return nil, err 901 } 902 if unlockErr != nil { 903 // failed to unlock with run key, try fallback key 904 return m.unlockEncryptedSaveFallbackKey, nil 905 } 906 907 // unlocked it properly, go mount it 908 return m.mountSave, nil 909 } 910 911 func (m *recoverModeStateMachine) unlockMaybeEncryptedAloneSaveFallbackKey() (stateFunc, error) { 912 // we can only get here by not finding ubuntu-data at all, meaning the 913 // system can still be encrypted and have an encrypted ubuntu-save, 914 // which we will determine now 915 916 // first check whether there is an encrypted save 917 _, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel(secboot.EncryptedPartitionName("ubuntu-save")) 918 if findErr == nil { 919 // well there is one, go try and unlock it 920 return m.unlockEncryptedSaveFallbackKey, nil 921 } 922 // encrypted ubuntu-save does not exist, there may still be an 923 // unencrypted one 924 return m.openUnencryptedSave, nil 925 } 926 927 func (m *recoverModeStateMachine) openUnencryptedSave() (stateFunc, error) { 928 // do we have ubuntu-save at all? 929 partSave := m.degradedState.partition("ubuntu-save") 930 const partitionOptional = true 931 partUUID, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-save") 932 if err := m.setFindState("ubuntu-save", partUUID, findErr, partitionOptional); err != nil { 933 return nil, err 934 } 935 if partSave.FindState == partitionFound { 936 // we have ubuntu-save, go mount it 937 return m.mountSave, nil 938 } 939 940 // unencrypted ubuntu-save was not found, try to log something in case 941 // the early boot output can be collected for debugging purposes 942 if uuid, err := m.disk.FindMatchingPartitionUUIDWithFsLabel(secboot.EncryptedPartitionName("ubuntu-save")); err == nil { 943 // highly unlikely that encrypted save exists 944 logger.Noticef("ignoring unexpected encrypted ubuntu-save with UUID %q", uuid) 945 } else { 946 logger.Noticef("ubuntu-save was not found") 947 } 948 949 // save is optional in an unencrypted system 950 partSave.MountState = partitionAbsentOptional 951 952 // we're done, nothing more to try 953 return nil, nil 954 } 955 956 func (m *recoverModeStateMachine) unlockEncryptedSaveFallbackKey() (stateFunc, error) { 957 // try to unlock save with the fallback key on ubuntu-seed, which must have 958 // been mounted at this point 959 960 if m.noFallback { 961 return nil, fmt.Errorf("cannot unlock ubuntu-save (fallback disabled)") 962 } 963 964 unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 965 // we want to allow using the recovery key if the fallback key fails as 966 // using the fallback object is the last chance before we give up trying 967 // to unlock save 968 AllowRecoveryKey: true, 969 } 970 saveFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key") 971 // TODO: this prompts again for a recover key, but really this is the 972 // reinstall key we will prompt for 973 // TODO: we should somehow customize the prompt to mention what key we need 974 // the user to enter, and what we are unlocking (as currently the prompt 975 // says "recovery key" and the partition UUID for what is being unlocked) 976 unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-save", saveFallbackKey, unlockOpts) 977 if err := m.setUnlockStateWithFallbackKey("ubuntu-save", unlockRes, unlockErr); err != nil { 978 return nil, err 979 } 980 if unlockErr != nil { 981 // all done, nothing left to try and mount, mounting ubuntu-save is the 982 // last step but we couldn't find or unlock it 983 return nil, nil 984 } 985 // otherwise we unlocked it, so go mount it 986 return m.mountSave, nil 987 } 988 989 func (m *recoverModeStateMachine) mountSave() (stateFunc, error) { 990 save := m.degradedState.partition("ubuntu-save") 991 // TODO: should we fsck ubuntu-save ? 992 mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, nil) 993 if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil { 994 return nil, err 995 } 996 // all done, nothing left to try and mount 997 return nil, nil 998 } 999 1000 func generateMountsModeRecover(mst *initramfsMountsState) error { 1001 // steps 1 and 2 are shared with install mode 1002 model, snaps, err := generateMountsCommonInstallRecover(mst) 1003 if err != nil { 1004 return err 1005 } 1006 1007 // get the disk that we mounted the ubuntu-seed partition from as a 1008 // reference point for future mounts 1009 disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) 1010 if err != nil { 1011 return err 1012 } 1013 1014 // for most cases we allow the use of fallback to unlock/mount things 1015 allowFallback := true 1016 1017 tryingCurrentSystem, err := boot.InitramfsIsTryingRecoverySystem(mst.recoverySystem) 1018 if err != nil { 1019 if boot.IsInconsistentRecoverySystemState(err) { 1020 // there is some try recovery system state in bootenv 1021 // but it is inconsistent, make sure we clear it and 1022 // return back to run mode 1023 1024 // finalize reboots or panics 1025 logger.Noticef("try recovery system state is inconsistent: %v", err) 1026 finalizeTryRecoverySystemAndReboot(boot.TryRecoverySystemOutcomeInconsistent) 1027 } 1028 return err 1029 } 1030 if tryingCurrentSystem { 1031 // but in this case, use only the run keys 1032 allowFallback = false 1033 1034 // make sure that if rebooted, the next boot goes into run mode 1035 if err := boot.EnsureNextBootToRunMode(""); err != nil { 1036 return err 1037 } 1038 } 1039 1040 // 3. run the state machine logic for mounting partitions, this involves 1041 // trying to unlock then mount ubuntu-data, and then unlocking and 1042 // mounting ubuntu-save 1043 // see the state* functions for details of what each step does and 1044 // possible transition points 1045 1046 machine, err := func() (machine *recoverModeStateMachine, err error) { 1047 // first state to execute is to unlock ubuntu-data with the run key 1048 machine = newRecoverModeStateMachine(model, disk, allowFallback) 1049 for { 1050 finished, err := machine.execute() 1051 // TODO: consider whether certain errors are fatal or not 1052 if err != nil { 1053 return nil, err 1054 } 1055 if finished { 1056 break 1057 } 1058 } 1059 1060 return machine, nil 1061 }() 1062 if tryingCurrentSystem { 1063 // end of the line for a recovery system we are only trying out, 1064 // this branch always ends with a reboot (or a panic) 1065 var outcome boot.TryRecoverySystemOutcome 1066 if err == nil && !machine.degraded() { 1067 outcome = boot.TryRecoverySystemOutcomeSuccess 1068 } else { 1069 outcome = boot.TryRecoverySystemOutcomeFailure 1070 if err == nil { 1071 err = fmt.Errorf("in degraded state") 1072 } 1073 logger.Noticef("try recovery system %q failed: %v", mst.recoverySystem, err) 1074 } 1075 // finalize reboots or panics 1076 finalizeTryRecoverySystemAndReboot(outcome) 1077 } 1078 1079 if err != nil { 1080 return err 1081 } 1082 1083 // 3.1 write out degraded.json if we ended up falling back somewhere 1084 if machine.degraded() { 1085 b, err := json.Marshal(machine.degradedState) 1086 if err != nil { 1087 return err 1088 } 1089 1090 if err := os.MkdirAll(dirs.SnapBootstrapRunDir, 0755); err != nil { 1091 return err 1092 } 1093 1094 // leave the information about degraded state at an ephemeral location 1095 if err := ioutil.WriteFile(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), b, 0644); err != nil { 1096 return err 1097 } 1098 } 1099 1100 // 4. final step: copy the auth data and network config from 1101 // the real ubuntu-data dir to the ephemeral ubuntu-data 1102 // dir, write the modeenv to the tmpfs data, and disable 1103 // cloud-init in recover mode 1104 1105 // if we have the host location, then we were able to successfully mount 1106 // ubuntu-data, and as such we can proceed with copying files from there 1107 // onto the tmpfs 1108 // Proceed only if we trust ubuntu-data to be paired with ubuntu-save 1109 if machine.trustData() { 1110 // TODO: erroring here should fallback to copySafeDefaultData and 1111 // proceed on with degraded mode anyways 1112 if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 1113 return err 1114 } 1115 if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 1116 return err 1117 } 1118 if err := copyUbuntuDataMisc(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 1119 return err 1120 } 1121 } else { 1122 // we don't have ubuntu-data host mountpoint, so we should setup safe 1123 // defaults for i.e. console-conf in the running image to block 1124 // attackers from accessing the system - just because we can't access 1125 // ubuntu-data doesn't mean that attackers wouldn't be able to if they 1126 // could login 1127 1128 if err := copySafeDefaultData(boot.InitramfsDataDir); err != nil { 1129 return err 1130 } 1131 } 1132 1133 modeEnv, err := mst.EphemeralModeenvForModel(model, snaps) 1134 if err != nil { 1135 return err 1136 } 1137 if err := modeEnv.WriteTo(boot.InitramfsWritableDir); err != nil { 1138 return err 1139 } 1140 1141 // finally we need to modify the bootenv to mark the system as successful, 1142 // this ensures that when you reboot from recover mode without doing 1143 // anything else, you are auto-transitioned back to run mode 1144 // TODO:UC20: as discussed unclear we need to pass the recovery system here 1145 if err := boot.EnsureNextBootToRunMode(mst.recoverySystem); err != nil { 1146 return err 1147 } 1148 1149 // done, no output, no error indicates to initramfs we are done with 1150 // mounting stuff 1151 return nil 1152 } 1153 1154 // checkDataAndSavePairing make sure that ubuntu-data and ubuntu-save 1155 // come from the same install by comparing secret markers in them 1156 func checkDataAndSavePairing(rootdir string) (bool, error) { 1157 // read the secret marker file from ubuntu-data 1158 markerFile1 := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "marker") 1159 marker1, err := ioutil.ReadFile(markerFile1) 1160 if err != nil { 1161 return false, err 1162 } 1163 // read the secret marker file from ubuntu-save 1164 markerFile2 := filepath.Join(dirs.SnapFDEDirUnderSave(boot.InitramfsUbuntuSaveDir), "marker") 1165 marker2, err := ioutil.ReadFile(markerFile2) 1166 if err != nil { 1167 return false, err 1168 } 1169 return subtle.ConstantTimeCompare(marker1, marker2) == 1, nil 1170 } 1171 1172 // mountPartitionMatchingKernelDisk will select the partition to mount at dir, 1173 // using the boot package function FindPartitionUUIDForBootedKernelDisk to 1174 // determine what partition the booted kernel came from. If which disk the 1175 // kernel came from cannot be determined, then it will fallback to mounting via 1176 // the specified disk label. 1177 func mountPartitionMatchingKernelDisk(dir, fallbacklabel string) error { 1178 partuuid, err := bootFindPartitionUUIDForBootedKernelDisk() 1179 // TODO: the by-partuuid is only available on gpt disks, on mbr we need 1180 // to use by-uuid or by-id 1181 partSrc := filepath.Join("/dev/disk/by-partuuid", partuuid) 1182 if err != nil { 1183 // no luck, try mounting by label instead 1184 partSrc = filepath.Join("/dev/disk/by-label", fallbacklabel) 1185 } 1186 1187 opts := &systemdMountOptions{ 1188 // always fsck the partition when we are mounting it, as this is the 1189 // first partition we will be mounting, we can't know if anything is 1190 // corrupted yet 1191 NeedsFsck: true, 1192 } 1193 return doSystemdMount(partSrc, dir, opts) 1194 } 1195 1196 func generateMountsCommonInstallRecover(mst *initramfsMountsState) (model *asserts.Model, sysSnaps map[snap.Type]snap.PlaceInfo, err error) { 1197 // 1. always ensure seed partition is mounted first before the others, 1198 // since the seed partition is needed to mount the snap files there 1199 if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "ubuntu-seed"); err != nil { 1200 return nil, nil, err 1201 } 1202 1203 // load model and verified essential snaps metadata 1204 typs := []snap.Type{snap.TypeBase, snap.TypeKernel, snap.TypeSnapd, snap.TypeGadget} 1205 model, essSnaps, err := mst.ReadEssential("", typs) 1206 if err != nil { 1207 return nil, nil, fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err) 1208 } 1209 1210 // 2.1. measure model 1211 err = stampedAction(fmt.Sprintf("%s-model-measured", mst.recoverySystem), func() error { 1212 return secbootMeasureSnapModelWhenPossible(func() (*asserts.Model, error) { 1213 return model, nil 1214 }) 1215 }) 1216 if err != nil { 1217 return nil, nil, err 1218 } 1219 // at this point on a system with TPM-based encryption 1220 // data can be open only if the measured model matches the actual 1221 // expected recovery model we sealed against. 1222 // TODO:UC20: on ARM systems and no TPM with encryption 1223 // we need other ways to make sure that the disk is opened 1224 // and we continue booting only for expected recovery models 1225 1226 // 2.2. (auto) select recovery system and mount seed snaps 1227 // TODO:UC20: do we need more cross checks here? 1228 1229 systemSnaps := make(map[snap.Type]snap.PlaceInfo) 1230 1231 for _, essentialSnap := range essSnaps { 1232 if essentialSnap.EssentialType == snap.TypeGadget { 1233 // don't need to mount the gadget anywhere, but we use the snap 1234 // later hence it is loaded 1235 continue 1236 } 1237 systemSnaps[essentialSnap.EssentialType] = essentialSnap.PlaceInfo() 1238 1239 dir := snapTypeToMountDir[essentialSnap.EssentialType] 1240 // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub 1241 if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { 1242 return nil, nil, err 1243 } 1244 } 1245 1246 // TODO:UC20: after we have the kernel and base snaps mounted, we should do 1247 // the bind mounts from the kernel modules on top of the base 1248 // mount and delete the corresponding systemd units from the 1249 // initramfs layout 1250 1251 // TODO:UC20: after the kernel and base snaps are mounted, we should setup 1252 // writable here as well to take over from "the-modeenv" script 1253 // in the initrd too 1254 1255 // TODO:UC20: after the kernel and base snaps are mounted and writable is 1256 // mounted, we should also implement writable-paths here too as 1257 // writing it in Go instead of shellscript is desirable 1258 1259 // 2.3. mount "ubuntu-data" on a tmpfs 1260 mntOpts := &systemdMountOptions{ 1261 Tmpfs: true, 1262 } 1263 err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts) 1264 if err != nil { 1265 return nil, nil, err 1266 } 1267 1268 // finally get the gadget snap from the essential snaps and use it to 1269 // configure the ephemeral system 1270 // should only be one seed snap 1271 gadgetPath := "" 1272 for _, essentialSnap := range essSnaps { 1273 if essentialSnap.EssentialType == snap.TypeGadget { 1274 gadgetPath = essentialSnap.Path 1275 } 1276 } 1277 gadgetSnap := squashfs.New(gadgetPath) 1278 1279 // we need to configure the ephemeral system with defaults and such using 1280 // from the seed gadget 1281 configOpts := &sysconfig.Options{ 1282 // never allow cloud-init to run inside the ephemeral system, in the 1283 // install case we don't want it to ever run, and in the recover case 1284 // cloud-init will already have run in run mode, so things like network 1285 // config and users should already be setup and we will copy those 1286 // further down in the setup for recover mode 1287 AllowCloudInit: false, 1288 TargetRootDir: boot.InitramfsWritableDir, 1289 GadgetSnap: gadgetSnap, 1290 } 1291 if err := sysconfig.ConfigureTargetSystem(configOpts); err != nil { 1292 return nil, nil, err 1293 } 1294 1295 return model, systemSnaps, err 1296 } 1297 1298 func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *systemdMountOptions) (haveSave bool, err error) { 1299 var saveDevice string 1300 if encrypted { 1301 saveKey := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "ubuntu-save.key") 1302 // if ubuntu-save exists and is encrypted, the key has been created during install 1303 if !osutil.FileExists(saveKey) { 1304 // ubuntu-data is encrypted, but we appear to be missing 1305 // a key to open ubuntu-save 1306 return false, fmt.Errorf("cannot find ubuntu-save encryption key at %v", saveKey) 1307 } 1308 // we have save.key, volume exists and is encrypted 1309 key, err := ioutil.ReadFile(saveKey) 1310 if err != nil { 1311 return true, err 1312 } 1313 unlockRes, err := secbootUnlockEncryptedVolumeUsingKey(disk, "ubuntu-save", key) 1314 if err != nil { 1315 return true, fmt.Errorf("cannot unlock ubuntu-save volume: %v", err) 1316 } 1317 saveDevice = unlockRes.FsDevice 1318 } else { 1319 partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-save") 1320 if err != nil { 1321 if _, ok := err.(disks.PartitionNotFoundError); ok { 1322 // this is ok, ubuntu-save may not exist for 1323 // non-encrypted device 1324 return false, nil 1325 } 1326 return false, err 1327 } 1328 saveDevice = filepath.Join("/dev/disk/by-partuuid", partUUID) 1329 } 1330 if err := doSystemdMount(saveDevice, boot.InitramfsUbuntuSaveDir, mountOpts); err != nil { 1331 return true, err 1332 } 1333 return true, nil 1334 } 1335 1336 func generateMountsModeRun(mst *initramfsMountsState) error { 1337 // 1. mount ubuntu-boot 1338 if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot"); err != nil { 1339 return err 1340 } 1341 1342 // get the disk that we mounted the ubuntu-boot partition from as a 1343 // reference point for future mounts 1344 disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuBootDir, nil) 1345 if err != nil { 1346 return err 1347 } 1348 1349 // 2. mount ubuntu-seed 1350 // use the disk we mounted ubuntu-boot from as a reference to find 1351 // ubuntu-seed and mount it 1352 partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-seed") 1353 if err != nil { 1354 return err 1355 } 1356 1357 // fsck is safe to run on ubuntu-seed as per the manpage, it should not 1358 // meaningfully contribute to corruption if we fsck it every time we boot, 1359 // and it is important to fsck it because it is vfat and mounted writable 1360 // TODO:UC20: mount it as read-only here and remount as writable when we 1361 // need it to be writable for i.e. transitioning to recover mode 1362 fsckSystemdOpts := &systemdMountOptions{ 1363 NeedsFsck: true, 1364 } 1365 if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuSeedDir, fsckSystemdOpts); err != nil { 1366 return err 1367 } 1368 1369 // 3.1. measure model 1370 err = stampedAction("run-model-measured", func() error { 1371 return secbootMeasureSnapModelWhenPossible(mst.UnverifiedBootModel) 1372 }) 1373 if err != nil { 1374 return err 1375 } 1376 // at this point on a system with TPM-based encryption 1377 // data can be open only if the measured model matches the actual 1378 // run model. 1379 // TODO:UC20: on ARM systems and no TPM with encryption 1380 // we need other ways to make sure that the disk is opened 1381 // and we continue booting only for expected models 1382 1383 // 3.2. mount Data 1384 runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") 1385 opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 1386 AllowRecoveryKey: true, 1387 } 1388 unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "ubuntu-data", runModeKey, opts) 1389 if err != nil { 1390 return err 1391 } 1392 1393 // TODO: do we actually need fsck if we are mounting a mapper device? 1394 // probably not? 1395 if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil { 1396 return err 1397 } 1398 isEncryptedDev := unlockRes.IsEncrypted 1399 1400 // 3.3. mount ubuntu-save (if present) 1401 haveSave, err := maybeMountSave(disk, boot.InitramfsWritableDir, isEncryptedDev, fsckSystemdOpts) 1402 if err != nil { 1403 return err 1404 } 1405 1406 // 4.1 verify that ubuntu-data comes from where we expect it to 1407 diskOpts := &disks.Options{} 1408 if unlockRes.IsEncrypted { 1409 // then we need to specify that the data mountpoint is expected to be a 1410 // decrypted device, applies to both ubuntu-data and ubuntu-save 1411 diskOpts.IsDecryptedDevice = true 1412 } 1413 1414 matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts) 1415 if err != nil { 1416 return err 1417 } 1418 if !matches { 1419 // failed to verify that ubuntu-data mountpoint comes from the same disk 1420 // as ubuntu-boot 1421 return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) 1422 } 1423 if haveSave { 1424 // 4.1a we have ubuntu-save, verify it as well 1425 matches, err = disk.MountPointIsFromDisk(boot.InitramfsUbuntuSaveDir, diskOpts) 1426 if err != nil { 1427 return err 1428 } 1429 if !matches { 1430 return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev()) 1431 } 1432 1433 if isEncryptedDev { 1434 // in run mode the path to open an encrypted save is for 1435 // data to be encrypted and the save key in it 1436 // to be successfully used. This already should stop 1437 // allowing to chose ubuntu-data to try to access 1438 // save. as safety boot also stops if the keys cannot 1439 // be locked. 1440 // for symmetry with recover code and extra paranoia 1441 // though also check that the markers match. 1442 paired, err := checkDataAndSavePairing(boot.InitramfsWritableDir) 1443 if err != nil { 1444 return err 1445 } 1446 if !paired { 1447 return fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install") 1448 } 1449 } 1450 } 1451 1452 // 4.2. read modeenv 1453 modeEnv, err := boot.ReadModeenv(boot.InitramfsWritableDir) 1454 if err != nil { 1455 return err 1456 } 1457 1458 typs := []snap.Type{snap.TypeBase, snap.TypeKernel} 1459 1460 // 4.2 choose base and kernel snaps (this includes updating modeenv if 1461 // needed to try the base snap) 1462 mounts, err := boot.InitramfsRunModeSelectSnapsToMount(typs, modeEnv) 1463 if err != nil { 1464 return err 1465 } 1466 1467 // TODO:UC20: with grade > dangerous, verify the kernel snap hash against 1468 // what we booted using the tpm log, this may need to be passed 1469 // to the function above to make decisions there, or perhaps this 1470 // code actually belongs in the bootloader implementation itself 1471 1472 // 4.3 mount base and kernel snaps 1473 // make sure this is a deterministic order 1474 for _, typ := range []snap.Type{snap.TypeBase, snap.TypeKernel} { 1475 if sn, ok := mounts[typ]; ok { 1476 dir := snapTypeToMountDir[typ] 1477 snapPath := filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) 1478 if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { 1479 return err 1480 } 1481 } 1482 } 1483 1484 // 4.4 mount snapd snap only on first boot 1485 if modeEnv.RecoverySystem != "" { 1486 // load the recovery system and generate mount for snapd 1487 _, essSnaps, err := mst.ReadEssential(modeEnv.RecoverySystem, []snap.Type{snap.TypeSnapd}) 1488 if err != nil { 1489 return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) 1490 } 1491 1492 return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), nil) 1493 } 1494 1495 return nil 1496 } 1497 1498 var tryRecoverySystemHealthCheck = func() error { 1499 // check that writable is accessible by checking whether the 1500 // state file exists 1501 if !osutil.FileExists(dirs.SnapStateFileUnder(boot.InitramfsHostWritableDir)) { 1502 return fmt.Errorf("host state file is not accessible") 1503 } 1504 return nil 1505 } 1506 1507 func finalizeTryRecoverySystemAndReboot(outcome boot.TryRecoverySystemOutcome) (err error) { 1508 // from this point on, we must finish with a system reboot 1509 defer func() { 1510 if rebootErr := boot.InitramfsReboot(); rebootErr != nil { 1511 if err != nil { 1512 err = fmt.Errorf("%v (cannot reboot to run system: %v)", err, rebootErr) 1513 } else { 1514 err = fmt.Errorf("cannot reboot to run system: %v", rebootErr) 1515 } 1516 } 1517 // not reached, unless in tests 1518 panic(fmt.Errorf("finalize try recovery system did not reboot, last error: %v", err)) 1519 }() 1520 1521 if outcome == boot.TryRecoverySystemOutcomeSuccess { 1522 if err := tryRecoverySystemHealthCheck(); err != nil { 1523 // health checks failed, the recovery system is considered 1524 // unsuccessful 1525 outcome = boot.TryRecoverySystemOutcomeFailure 1526 logger.Noticef("try recovery system health check failed: %v", err) 1527 } 1528 } 1529 1530 // that's it, we've tried booting a new recovery system to this point, 1531 // whether things are looking good or bad we will reboot back to run 1532 // mode and update the boot variables accordingly 1533 if err := boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(outcome); err != nil { 1534 logger.Noticef("cannot update the try recovery system state: %v", err) 1535 return fmt.Errorf("cannot mark recovery system successful: %v", err) 1536 } 1537 return nil 1538 }