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  }