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  }