github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 "fmt" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "strings" 28 "syscall" 29 30 "github.com/jessevdk/go-flags" 31 32 "github.com/snapcore/snapd/asserts" 33 "github.com/snapcore/snapd/boot" 34 "github.com/snapcore/snapd/dirs" 35 "github.com/snapcore/snapd/osutil" 36 "github.com/snapcore/snapd/osutil/disks" 37 "github.com/snapcore/snapd/overlord/state" 38 "github.com/snapcore/snapd/secboot" 39 "github.com/snapcore/snapd/snap" 40 "github.com/snapcore/snapd/snap/squashfs" 41 "github.com/snapcore/snapd/sysconfig" 42 43 // to set sysconfig.ApplyFilesystemOnlyDefaultsImpl 44 _ "github.com/snapcore/snapd/overlord/configstate/configcore" 45 ) 46 47 func init() { 48 const ( 49 short = "Generate mounts for the initramfs" 50 long = "Generate and perform all mounts for the initramfs before transitioning to userspace" 51 ) 52 53 addCommandBuilder(func(parser *flags.Parser) { 54 if _, err := parser.AddCommand("initramfs-mounts", short, long, &cmdInitramfsMounts{}); err != nil { 55 panic(err) 56 } 57 }) 58 59 snap.SanitizePlugsSlots = func(*snap.Info) {} 60 } 61 62 type cmdInitramfsMounts struct{} 63 64 func (c *cmdInitramfsMounts) Execute(args []string) error { 65 return generateInitramfsMounts() 66 } 67 68 var ( 69 osutilIsMounted = osutil.IsMounted 70 71 snapTypeToMountDir = map[snap.Type]string{ 72 snap.TypeBase: "base", 73 snap.TypeKernel: "kernel", 74 snap.TypeSnapd: "snapd", 75 } 76 77 secbootMeasureSnapSystemEpochWhenPossible = secboot.MeasureSnapSystemEpochWhenPossible 78 secbootMeasureSnapModelWhenPossible = secboot.MeasureSnapModelWhenPossible 79 secbootUnlockVolumeIfEncrypted = secboot.UnlockVolumeIfEncrypted 80 81 bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk 82 ) 83 84 func stampedAction(stamp string, action func() error) error { 85 stampFile := filepath.Join(dirs.SnapBootstrapRunDir, stamp) 86 if osutil.FileExists(stampFile) { 87 return nil 88 } 89 if err := os.MkdirAll(filepath.Dir(stampFile), 0755); err != nil { 90 return err 91 } 92 if err := action(); err != nil { 93 return err 94 } 95 return ioutil.WriteFile(stampFile, nil, 0644) 96 } 97 98 func generateInitramfsMounts() error { 99 // Ensure there is a very early initial measurement 100 err := stampedAction("secboot-epoch-measured", func() error { 101 return secbootMeasureSnapSystemEpochWhenPossible() 102 }) 103 if err != nil { 104 return err 105 } 106 107 mode, recoverySystem, err := boot.ModeAndRecoverySystemFromKernelCommandLine() 108 if err != nil { 109 return err 110 } 111 112 mst := &initramfsMountsState{ 113 mode: mode, 114 recoverySystem: recoverySystem, 115 } 116 117 switch mode { 118 case "recover": 119 return generateMountsModeRecover(mst) 120 case "install": 121 return generateMountsModeInstall(mst) 122 case "run": 123 return generateMountsModeRun(mst) 124 } 125 // this should never be reached 126 return fmt.Errorf("internal error: mode in generateInitramfsMounts not handled") 127 } 128 129 // generateMountsMode* is called multiple times from initramfs until it 130 // no longer generates more mount points and just returns an empty output. 131 func generateMountsModeInstall(mst *initramfsMountsState) error { 132 // steps 1 and 2 are shared with recover mode 133 if err := generateMountsCommonInstallRecover(mst); err != nil { 134 return err 135 } 136 137 // 3. final step: write modeenv to tmpfs data dir and disable cloud-init in 138 // install mode 139 modeEnv := &boot.Modeenv{ 140 Mode: "install", 141 RecoverySystem: mst.recoverySystem, 142 } 143 if err := modeEnv.WriteTo(boot.InitramfsWritableDir); err != nil { 144 return err 145 } 146 147 // done, no output, no error indicates to initramfs we are done with 148 // mounting stuff 149 return nil 150 } 151 152 // copyNetworkConfig copies the network configuration to the target 153 // directory. This is used to copy the network configuration 154 // data from a real uc20 ubuntu-data partition into a ephemeral one. 155 func copyNetworkConfig(src, dst string) error { 156 for _, globEx := range []string{ 157 // for network configuration setup by console-conf, etc. 158 // TODO:UC20: we want some way to "try" or "verify" the network 159 // configuration or to only use known-to-be-good network 160 // configuration i.e. from ubuntu-save before installing it 161 // onto recover mode, because the network configuration could 162 // have been what was broken so we don't want to break 163 // network configuration for recover mode as well, but for 164 // now this is fine 165 "system-data/etc/netplan/*", 166 // etc/machine-id is part of what systemd-networkd uses to generate a 167 // DHCP clientid (the other part being the interface name), so to have 168 // the same IP addresses across run mode and recover mode, we need to 169 // also copy the machine-id across 170 "system-data/etc/machine-id", 171 } { 172 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 173 return err 174 } 175 } 176 return nil 177 } 178 179 // copyUbuntuDataMisc copies miscellaneous other files from the run mode system 180 // to the recover system such as: 181 // - timesync clock to keep the same time setting in recover as in run mode 182 func copyUbuntuDataMisc(src, dst string) error { 183 for _, globEx := range []string{ 184 // systemd's timesync clock file so that the time in recover mode moves 185 // forward to what it was in run mode 186 // NOTE: we don't sync back the time movement from recover mode to run 187 // mode currently, unclear how/when we could do this, but recover mode 188 // isn't meant to be long lasting and as such it's probably not a big 189 // problem to "lose" the time spent in recover mode 190 "system-data/var/lib/systemd/timesync/clock", 191 } { 192 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 193 return err 194 } 195 } 196 197 return nil 198 } 199 200 // copyUbuntuDataAuth copies the authentication files like 201 // - extrausers passwd,shadow etc 202 // - sshd host configuration 203 // - user .ssh dir 204 // to the target directory. This is used to copy the authentication 205 // data from a real uc20 ubuntu-data partition into a ephemeral one. 206 func copyUbuntuDataAuth(src, dst string) error { 207 for _, globEx := range []string{ 208 "system-data/var/lib/extrausers/*", 209 "system-data/etc/ssh/*", 210 "user-data/*/.ssh/*", 211 // this ensures we get proper authentication to snapd from "snap" 212 // commands in recover mode 213 "user-data/*/.snap/auth.json", 214 // this ensures we also get non-ssh enabled accounts copied 215 "user-data/*/.profile", 216 // so that users have proper perms, i.e. console-conf added users are 217 // sudoers 218 "system-data/etc/sudoers.d/*", 219 } { 220 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 221 return err 222 } 223 } 224 225 // ensure the user state is transferred as well 226 srcState := filepath.Join(src, "system-data/var/lib/snapd/state.json") 227 dstState := filepath.Join(dst, "system-data/var/lib/snapd/state.json") 228 err := state.CopyState(srcState, dstState, []string{"auth.users", "auth.macaroon-key", "auth.last-id"}) 229 if err != nil && err != state.ErrNoState { 230 return fmt.Errorf("cannot copy user state: %v", err) 231 } 232 233 return nil 234 } 235 236 func copyFromGlobHelper(src, dst, globEx string) error { 237 matches, err := filepath.Glob(filepath.Join(src, globEx)) 238 if err != nil { 239 return err 240 } 241 for _, p := range matches { 242 comps := strings.Split(strings.TrimPrefix(p, src), "/") 243 for i := range comps { 244 part := filepath.Join(comps[0 : i+1]...) 245 fi, err := os.Stat(filepath.Join(src, part)) 246 if err != nil { 247 return err 248 } 249 if fi.IsDir() { 250 if err := os.Mkdir(filepath.Join(dst, part), fi.Mode()); err != nil && !os.IsExist(err) { 251 return err 252 } 253 st, ok := fi.Sys().(*syscall.Stat_t) 254 if !ok { 255 return fmt.Errorf("cannot get stat data: %v", err) 256 } 257 if err := os.Chown(filepath.Join(dst, part), int(st.Uid), int(st.Gid)); err != nil { 258 return err 259 } 260 } else { 261 if err := osutil.CopyFile(p, filepath.Join(dst, part), osutil.CopyFlagPreserveAll); err != nil { 262 return err 263 } 264 } 265 } 266 } 267 268 return nil 269 } 270 271 func generateMountsModeRecover(mst *initramfsMountsState) error { 272 // steps 1 and 2 are shared with install mode 273 if err := generateMountsCommonInstallRecover(mst); err != nil { 274 return err 275 } 276 277 // get the disk that we mounted the ubuntu-seed partition from as a 278 // reference point for future mounts 279 disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) 280 if err != nil { 281 return err 282 } 283 284 // 3. mount ubuntu-data for recovery 285 const lockKeysOnFinish = true 286 device, isDecryptDev, err := secbootUnlockVolumeIfEncrypted(disk, "ubuntu-data", boot.InitramfsEncryptionKeyDir, lockKeysOnFinish) 287 if err != nil { 288 return err 289 } 290 291 // don't do fsck on the data partition, it could be corrupted 292 if err := doSystemdMount(device, boot.InitramfsHostUbuntuDataDir, nil); err != nil { 293 return err 294 } 295 296 // 3.1 verify that the host ubuntu-data comes from where we expect it to 297 diskOpts := &disks.Options{} 298 if isDecryptDev { 299 // then we need to specify that the data mountpoint is expected to be a 300 // decrypted device 301 diskOpts.IsDecryptedDevice = true 302 } 303 304 matches, err := disk.MountPointIsFromDisk(boot.InitramfsHostUbuntuDataDir, diskOpts) 305 if err != nil { 306 return err 307 } 308 if !matches { 309 return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) 310 } 311 312 // 4. final step: copy the auth data and network config from 313 // the real ubuntu-data dir to the ephemeral ubuntu-data 314 // dir, write the modeenv to the tmpfs data, and disable 315 // cloud-init in recover mode 316 if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 317 return err 318 } 319 if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 320 return err 321 } 322 if err := copyUbuntuDataMisc(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 323 return err 324 } 325 326 modeEnv := &boot.Modeenv{ 327 Mode: "recover", 328 RecoverySystem: mst.recoverySystem, 329 } 330 if err := modeEnv.WriteTo(boot.InitramfsWritableDir); err != nil { 331 return err 332 } 333 334 // finally we need to modify the bootenv to mark the system as successful, 335 // this ensures that when you reboot from recover mode without doing 336 // anything else, you are auto-transitioned back to run mode 337 // TODO:UC20: as discussed unclear we need to pass the recovery system here 338 if err := boot.EnsureNextBootToRunMode(mst.recoverySystem); err != nil { 339 return err 340 } 341 342 // done, no output, no error indicates to initramfs we are done with 343 // mounting stuff 344 return nil 345 } 346 347 // mountPartitionMatchingKernelDisk will select the partition to mount at dir, 348 // using the boot package function FindPartitionUUIDForBootedKernelDisk to 349 // determine what partition the booted kernel came from. If which disk the 350 // kernel came from cannot be determined, then it will fallback to mounting via 351 // the specified disk label. 352 func mountPartitionMatchingKernelDisk(dir, fallbacklabel string) error { 353 partuuid, err := bootFindPartitionUUIDForBootedKernelDisk() 354 // TODO: the by-partuuid is only available on gpt disks, on mbr we need 355 // to use by-uuid or by-id 356 partSrc := filepath.Join("/dev/disk/by-partuuid", partuuid) 357 if err != nil { 358 // no luck, try mounting by label instead 359 partSrc = filepath.Join("/dev/disk/by-label", fallbacklabel) 360 } 361 362 opts := &systemdMountOptions{ 363 // always fsck the partition when we are mounting it, as this is the 364 // first partition we will be mounting, we can't know if anything is 365 // corrupted yet 366 NeedsFsck: true, 367 } 368 return doSystemdMount(partSrc, dir, opts) 369 } 370 371 func generateMountsCommonInstallRecover(mst *initramfsMountsState) error { 372 // 1. always ensure seed partition is mounted first before the others, 373 // since the seed partition is needed to mount the snap files there 374 if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "ubuntu-seed"); err != nil { 375 return err 376 } 377 378 // load model and verified essential snaps metadata 379 typs := []snap.Type{snap.TypeBase, snap.TypeKernel, snap.TypeSnapd, snap.TypeGadget} 380 model, essSnaps, err := mst.ReadEssential("", typs) 381 if err != nil { 382 return fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err) 383 } 384 385 // 2.1. measure model 386 err = stampedAction(fmt.Sprintf("%s-model-measured", mst.recoverySystem), func() error { 387 return secbootMeasureSnapModelWhenPossible(func() (*asserts.Model, error) { 388 return model, nil 389 }) 390 }) 391 if err != nil { 392 return err 393 } 394 395 // 2.2. (auto) select recovery system and mount seed snaps 396 // TODO:UC20: do we need more cross checks here? 397 for _, essentialSnap := range essSnaps { 398 if essentialSnap.EssentialType == snap.TypeGadget { 399 // don't need to mount the gadget anywhere, but we use the snap 400 // later hence it is loaded 401 continue 402 } 403 dir := snapTypeToMountDir[essentialSnap.EssentialType] 404 // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub 405 if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { 406 return err 407 } 408 } 409 410 // TODO:UC20: after we have the kernel and base snaps mounted, we should do 411 // the bind mounts from the kernel modules on top of the base 412 // mount and delete the corresponding systemd units from the 413 // initramfs layout 414 415 // TODO:UC20: after the kernel and base snaps are mounted, we should setup 416 // writable here as well to take over from "the-modeenv" script 417 // in the initrd too 418 419 // TODO:UC20: after the kernel and base snaps are mounted and writable is 420 // mounted, we should also implement writable-paths here too as 421 // writing it in Go instead of shellscript is desirable 422 423 // 2.3. mount "ubuntu-data" on a tmpfs 424 mntOpts := &systemdMountOptions{ 425 Tmpfs: true, 426 } 427 err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts) 428 if err != nil { 429 return err 430 } 431 432 // finally get the gadget snap from the essential snaps and use it to 433 // configure the ephemeral system 434 // should only be one seed snap 435 gadgetPath := "" 436 for _, essentialSnap := range essSnaps { 437 if essentialSnap.EssentialType == snap.TypeGadget { 438 gadgetPath = essentialSnap.Path 439 } 440 } 441 gadgetSnap := squashfs.New(gadgetPath) 442 443 // we need to configure the ephemeral system with defaults and such using 444 // from the seed gadget 445 configOpts := &sysconfig.Options{ 446 // never allow cloud-init to run inside the ephemeral system, in the 447 // install case we don't want it to ever run, and in the recover case 448 // cloud-init will already have run in run mode, so things like network 449 // config and users should already be setup and we will copy those 450 // further down in the setup for recover mode 451 AllowCloudInit: false, 452 TargetRootDir: boot.InitramfsWritableDir, 453 GadgetSnap: gadgetSnap, 454 } 455 return sysconfig.ConfigureTargetSystem(configOpts) 456 } 457 458 func generateMountsModeRun(mst *initramfsMountsState) error { 459 // 1. mount ubuntu-boot 460 if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot"); err != nil { 461 return err 462 } 463 464 // get the disk that we mounted the ubuntu-boot partition from as a 465 // reference point for future mounts 466 disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuBootDir, nil) 467 if err != nil { 468 return err 469 } 470 471 // 2. mount ubuntu-seed 472 // use the disk we mounted ubuntu-boot from as a reference to find 473 // ubuntu-seed and mount it 474 partUUID, err := disk.FindMatchingPartitionUUID("ubuntu-seed") 475 if err != nil { 476 return err 477 } 478 479 // don't run fsck on ubuntu-seed in run mode so we minimize chance of 480 // corruption 481 482 if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuSeedDir, nil); err != nil { 483 return err 484 } 485 486 // 3.1. measure model 487 err = stampedAction("run-model-measured", func() error { 488 return secbootMeasureSnapModelWhenPossible(mst.UnverifiedBootModel) 489 }) 490 if err != nil { 491 return err 492 } 493 // TODO:UC20: cross check the model we read from ubuntu-boot/model with 494 // one recorded in ubuntu-data modeenv during install 495 496 // 3.2. mount Data 497 const lockKeysOnFinish = true 498 device, isDecryptDev, err := secbootUnlockVolumeIfEncrypted(disk, "ubuntu-data", boot.InitramfsEncryptionKeyDir, lockKeysOnFinish) 499 if err != nil { 500 return err 501 } 502 503 opts := &systemdMountOptions{ 504 // TODO: do we actually need fsck if we are mounting a mapper device? 505 // probably not? 506 NeedsFsck: true, 507 } 508 if err := doSystemdMount(device, boot.InitramfsDataDir, opts); err != nil { 509 return err 510 } 511 512 // 4.1 verify that ubuntu-data comes from where we expect it to 513 diskOpts := &disks.Options{} 514 if isDecryptDev { 515 // then we need to specify that the data mountpoint is expected to be a 516 // decrypted device 517 diskOpts.IsDecryptedDevice = true 518 } 519 520 matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts) 521 if err != nil { 522 return err 523 } 524 if !matches { 525 // failed to verify that ubuntu-data mountpoint comes from the same disk 526 // as ubuntu-boot 527 return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) 528 } 529 530 // 4.2. read modeenv 531 modeEnv, err := boot.ReadModeenv(boot.InitramfsWritableDir) 532 if err != nil { 533 return err 534 } 535 536 typs := []snap.Type{snap.TypeBase, snap.TypeKernel} 537 538 // 4.2 choose base and kernel snaps (this includes updating modeenv if 539 // needed to try the base snap) 540 mounts, err := boot.InitramfsRunModeSelectSnapsToMount(typs, modeEnv) 541 if err != nil { 542 return err 543 } 544 545 // TODO:UC20: with grade > dangerous, verify the kernel snap hash against 546 // what we booted using the tpm log, this may need to be passed 547 // to the function above to make decisions there, or perhaps this 548 // code actually belongs in the bootloader implementation itself 549 550 // 4.3 mount base and kernel snaps 551 // make sure this is a deterministic order 552 for _, typ := range []snap.Type{snap.TypeBase, snap.TypeKernel} { 553 if sn, ok := mounts[typ]; ok { 554 dir := snapTypeToMountDir[typ] 555 snapPath := filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) 556 if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { 557 return err 558 } 559 } 560 } 561 562 // 4.4 mount snapd snap only on first boot 563 if modeEnv.RecoverySystem != "" { 564 // load the recovery system and generate mount for snapd 565 _, essSnaps, err := mst.ReadEssential(modeEnv.RecoverySystem, []snap.Type{snap.TypeSnapd}) 566 if err != nil { 567 return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) 568 } 569 570 return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), nil) 571 } 572 573 return nil 574 }