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