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