github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/boot/makebootable.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2019 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 boot
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  
    27  	"github.com/snapcore/snapd/asserts"
    28  	"github.com/snapcore/snapd/bootloader"
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/snap"
    32  	"github.com/snapcore/snapd/snap/snapfile"
    33  )
    34  
    35  // BootableSet represents the boot snaps of a system to be made bootable.
    36  type BootableSet struct {
    37  	Base       *snap.Info
    38  	BasePath   string
    39  	Kernel     *snap.Info
    40  	KernelPath string
    41  
    42  	RecoverySystemLabel string
    43  	// RecoverySystemDir is a path to a directory with recovery system
    44  	// assets. The path is relative to the recovery bootloader root
    45  	// directory.
    46  	RecoverySystemDir string
    47  
    48  	UnpackedGadgetDir string
    49  
    50  	// Recover is set when making the recovery partition bootable.
    51  	Recovery bool
    52  }
    53  
    54  // MakeBootableImage sets up the given bootable set and target filesystem
    55  // such that the image can be booted.
    56  //
    57  // rootdir points to an image filesystem (UC 16/18) or an image recovery
    58  // filesystem (UC20 at prepare-image time).
    59  // On UC20, bootWith.Recovery must be true, as this function makes the recovery
    60  // system bootable. It does not make a run system bootable, for that
    61  // functionality see MakeRunnableSystem, which is meant to be used at runtime
    62  // from UC20 install mode.
    63  // For a UC20 image a set of boot flags that will be set in the recovery
    64  // boot environment can be specified.
    65  func MakeBootableImage(model *asserts.Model, rootdir string, bootWith *BootableSet, bootFlags []string) error {
    66  	if model.Grade() == asserts.ModelGradeUnset {
    67  		if len(bootFlags) != 0 {
    68  			return fmt.Errorf("no boot flags support for UC16/18")
    69  		}
    70  		return makeBootable16(model, rootdir, bootWith)
    71  	}
    72  
    73  	if !bootWith.Recovery {
    74  		return fmt.Errorf("internal error: MakeBootableImage called at runtime, use MakeRunnableSystem instead")
    75  	}
    76  	return makeBootable20(model, rootdir, bootWith, bootFlags)
    77  }
    78  
    79  // makeBootable16 setups the image filesystem for boot with UC16
    80  // and UC18 models. This entails:
    81  //  - installing the bootloader configuration from the gadget
    82  //  - creating symlinks for boot snaps from seed to the runtime blob dir
    83  //  - setting boot env vars pointing to the revisions of the boot snaps to use
    84  //  - extracting kernel assets as needed by the bootloader
    85  func makeBootable16(model *asserts.Model, rootdir string, bootWith *BootableSet) error {
    86  	opts := &bootloader.Options{
    87  		PrepareImageTime: true,
    88  	}
    89  
    90  	// install the bootloader configuration from the gadget
    91  	if err := bootloader.InstallBootConfig(bootWith.UnpackedGadgetDir, rootdir, opts); err != nil {
    92  		return err
    93  	}
    94  
    95  	// setup symlinks for kernel and boot base from the blob directory
    96  	// to the seed snaps
    97  
    98  	snapBlobDir := dirs.SnapBlobDirUnder(rootdir)
    99  	if err := os.MkdirAll(snapBlobDir, 0755); err != nil {
   100  		return err
   101  	}
   102  
   103  	for _, fn := range []string{bootWith.BasePath, bootWith.KernelPath} {
   104  		dst := filepath.Join(snapBlobDir, filepath.Base(fn))
   105  		// construct a relative symlink from the blob dir
   106  		// to the seed snap file
   107  		relSymlink, err := filepath.Rel(snapBlobDir, fn)
   108  		if err != nil {
   109  			return fmt.Errorf("cannot build symlink for boot snap: %v", err)
   110  		}
   111  		if err := os.Symlink(relSymlink, dst); err != nil {
   112  			return err
   113  		}
   114  	}
   115  
   116  	// Set bootvars for kernel/core snaps so the system boots and
   117  	// does the first-time initialization. There is also no
   118  	// mounted kernel/core/base snap, but just the blobs.
   119  	bl, err := bootloader.Find(rootdir, opts)
   120  	if err != nil {
   121  		return fmt.Errorf("cannot set kernel/core boot variables: %s", err)
   122  	}
   123  
   124  	m := map[string]string{
   125  		"snap_mode":       "",
   126  		"snap_try_core":   "",
   127  		"snap_try_kernel": "",
   128  	}
   129  	if model.DisplayName() != "" {
   130  		m["snap_menuentry"] = model.DisplayName()
   131  	}
   132  
   133  	setBoot := func(name, fn string) {
   134  		m[name] = filepath.Base(fn)
   135  	}
   136  	// base
   137  	setBoot("snap_core", bootWith.BasePath)
   138  
   139  	// kernel
   140  	kernelf, err := snapfile.Open(bootWith.KernelPath)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	if err := bl.ExtractKernelAssets(bootWith.Kernel, kernelf); err != nil {
   145  		return err
   146  	}
   147  	setBoot("snap_kernel", bootWith.KernelPath)
   148  
   149  	if err := bl.SetBootVars(m); err != nil {
   150  		return err
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet, bootFlags []string) error {
   157  	// we can only make a single recovery system bootable right now
   158  	recoverySystems, err := filepath.Glob(filepath.Join(rootdir, "systems/*"))
   159  	if err != nil {
   160  		return fmt.Errorf("cannot validate recovery systems: %v", err)
   161  	}
   162  	if len(recoverySystems) > 1 {
   163  		return fmt.Errorf("cannot make multiple recovery systems bootable yet")
   164  	}
   165  
   166  	if bootWith.RecoverySystemLabel == "" {
   167  		return fmt.Errorf("internal error: recovery system label unset")
   168  	}
   169  
   170  	blVars := make(map[string]string, 3)
   171  	if len(bootFlags) != 0 {
   172  		if err := setImageBootFlags(bootFlags, blVars); err != nil {
   173  			return err
   174  		}
   175  	}
   176  
   177  	opts := &bootloader.Options{
   178  		PrepareImageTime: true,
   179  		// setup the recovery bootloader
   180  		Role: bootloader.RoleRecovery,
   181  	}
   182  
   183  	// install the bootloader configuration from the gadget
   184  	if err := bootloader.InstallBootConfig(bootWith.UnpackedGadgetDir, rootdir, opts); err != nil {
   185  		return err
   186  	}
   187  
   188  	// now install the recovery system specific boot config
   189  	bl, err := bootloader.Find(rootdir, opts)
   190  	if err != nil {
   191  		return fmt.Errorf("internal error: cannot find bootloader: %v", err)
   192  	}
   193  
   194  	// record which recovery system is to be used on the bootloader, note
   195  	// that this goes on the main bootloader environment, and not on the
   196  	// recovery system bootloader environment, for example for grub
   197  	// bootloader, this env var is set on the ubuntu-seed root grubenv, and
   198  	// not on the recovery system grubenv in the systems/20200314/ subdir on
   199  	// ubuntu-seed
   200  	blVars["snapd_recovery_system"] = bootWith.RecoverySystemLabel
   201  	// always set the mode as install
   202  	blVars["snapd_recovery_mode"] = ModeInstall
   203  	if err := bl.SetBootVars(blVars); err != nil {
   204  		return fmt.Errorf("cannot set recovery environment: %v", err)
   205  	}
   206  
   207  	return MakeRecoverySystemBootable(rootdir, bootWith.RecoverySystemDir, &RecoverySystemBootableSet{
   208  		Kernel:           bootWith.Kernel,
   209  		KernelPath:       bootWith.KernelPath,
   210  		GadgetSnapOrDir:  bootWith.UnpackedGadgetDir,
   211  		PrepareImageTime: true,
   212  	})
   213  }
   214  
   215  // RecoverySystemBootableSet is a set of snaps relevant to booting a recovery
   216  // system.
   217  type RecoverySystemBootableSet struct {
   218  	Kernel          *snap.Info
   219  	KernelPath      string
   220  	GadgetSnapOrDir string
   221  	// PrepareImageTime is true when the structure is being used when
   222  	// preparing a bootable system image.
   223  	PrepareImageTime bool
   224  }
   225  
   226  // MakeRecoverySystemBootable prepares a recovery system under a path relative
   227  // to recovery bootloader's rootdir for booting.
   228  func MakeRecoverySystemBootable(rootdir string, relativeRecoverySystemDir string, bootWith *RecoverySystemBootableSet) error {
   229  	opts := &bootloader.Options{
   230  		// XXX: this is only needed by LK, it is unclear whether LK does
   231  		// too much when extracting recovery kernel assets, in the end
   232  		// it is currently not possible to create a recovery system at
   233  		// runtime when using LK.
   234  		PrepareImageTime: bootWith.PrepareImageTime,
   235  		// setup the recovery bootloader
   236  		Role: bootloader.RoleRecovery,
   237  	}
   238  
   239  	bl, err := bootloader.Find(rootdir, opts)
   240  	if err != nil {
   241  		return fmt.Errorf("internal error: cannot find bootloader: %v", err)
   242  	}
   243  
   244  	// on e.g. ARM we need to extract the kernel assets on the recovery
   245  	// system as well, but the bootloader does not load any environment from
   246  	// the recovery system
   247  	erkbl, ok := bl.(bootloader.ExtractedRecoveryKernelImageBootloader)
   248  	if ok {
   249  		kernelf, err := snapfile.Open(bootWith.KernelPath)
   250  		if err != nil {
   251  			return err
   252  		}
   253  
   254  		err = erkbl.ExtractRecoveryKernelAssets(
   255  			relativeRecoverySystemDir,
   256  			bootWith.Kernel,
   257  			kernelf,
   258  		)
   259  		if err != nil {
   260  			return fmt.Errorf("cannot extract recovery system kernel assets: %v", err)
   261  		}
   262  
   263  		return nil
   264  	}
   265  
   266  	rbl, ok := bl.(bootloader.RecoveryAwareBootloader)
   267  	if !ok {
   268  		return fmt.Errorf("cannot use %s bootloader: does not support recovery systems", bl.Name())
   269  	}
   270  	kernelPath, err := filepath.Rel(rootdir, bootWith.KernelPath)
   271  	if err != nil {
   272  		return fmt.Errorf("cannot construct kernel boot path: %v", err)
   273  	}
   274  	recoveryBlVars := map[string]string{
   275  		"snapd_recovery_kernel": filepath.Join("/", kernelPath),
   276  	}
   277  	if _, ok := bl.(bootloader.TrustedAssetsBootloader); ok {
   278  		recoveryCmdlineArgs, err := bootVarsForTrustedCommandLineFromGadget(bootWith.GadgetSnapOrDir)
   279  		if err != nil {
   280  			return fmt.Errorf("cannot obtain recovery system command line: %v", err)
   281  		}
   282  		for k, v := range recoveryCmdlineArgs {
   283  			recoveryBlVars[k] = v
   284  		}
   285  	}
   286  
   287  	if err := rbl.SetRecoverySystemEnv(relativeRecoverySystemDir, recoveryBlVars); err != nil {
   288  		return fmt.Errorf("cannot set recovery system environment: %v", err)
   289  	}
   290  	return nil
   291  }
   292  
   293  // MakeRunnableSystem is like MakeBootableImage in that it sets up a system to
   294  // be able to boot, but is unique in that it is intended to be called from UC20
   295  // install mode and makes the run system bootable (hence it is called
   296  // "runnable").
   297  // Note that this function does not update the recovery bootloader env to
   298  // actually transition to run mode here, that is left to the caller via
   299  // something like boot.EnsureNextBootToRunMode(). This is to enable separately
   300  // setting up a run system and actually transitioning to it, with hooks, etc.
   301  // running in between.
   302  func MakeRunnableSystem(model *asserts.Model, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error {
   303  	if model.Grade() == asserts.ModelGradeUnset {
   304  		return fmt.Errorf("internal error: cannot make non-uc20 system runnable")
   305  	}
   306  	// TODO:UC20:
   307  	// - figure out what to do for uboot gadgets, currently we require them to
   308  	//   install the boot.sel onto ubuntu-boot directly, but the file should be
   309  	//   managed by snapd instead
   310  
   311  	// copy kernel/base into the ubuntu-data partition
   312  	snapBlobDir := dirs.SnapBlobDirUnder(InstallHostWritableDir)
   313  	if err := os.MkdirAll(snapBlobDir, 0755); err != nil {
   314  		return err
   315  	}
   316  	for _, fn := range []string{bootWith.BasePath, bootWith.KernelPath} {
   317  		dst := filepath.Join(snapBlobDir, filepath.Base(fn))
   318  		// if the source filename is a symlink, don't copy the symlink, copy the
   319  		// target file instead of copying the symlink, as the initramfs won't
   320  		// follow the symlink when it goes to mount the base and kernel snaps by
   321  		// design as the initramfs should only be using trusted things from
   322  		// ubuntu-data to boot in run mode
   323  		if osutil.IsSymlink(fn) {
   324  			link, err := os.Readlink(fn)
   325  			if err != nil {
   326  				return err
   327  			}
   328  			fn = link
   329  		}
   330  		if err := osutil.CopyFile(fn, dst, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync); err != nil {
   331  			return err
   332  		}
   333  	}
   334  
   335  	// replicate the boot assets cache in host's writable
   336  	if err := CopyBootAssetsCacheToRoot(InstallHostWritableDir); err != nil {
   337  		return fmt.Errorf("cannot replicate boot assets cache: %v", err)
   338  	}
   339  
   340  	var currentTrustedBootAssets bootAssetsMap
   341  	var currentTrustedRecoveryBootAssets bootAssetsMap
   342  	if sealer != nil {
   343  		currentTrustedBootAssets = sealer.currentTrustedBootAssetsMap()
   344  		currentTrustedRecoveryBootAssets = sealer.currentTrustedRecoveryBootAssetsMap()
   345  	}
   346  	recoverySystemLabel := filepath.Base(bootWith.RecoverySystemDir)
   347  	// write modeenv on the ubuntu-data partition
   348  	modeenv := &Modeenv{
   349  		Mode:           "run",
   350  		RecoverySystem: recoverySystemLabel,
   351  		// default to the system we were installed from
   352  		CurrentRecoverySystems: []string{recoverySystemLabel},
   353  		// which is also considered to be good
   354  		GoodRecoverySystems:              []string{recoverySystemLabel},
   355  		CurrentTrustedBootAssets:         currentTrustedBootAssets,
   356  		CurrentTrustedRecoveryBootAssets: currentTrustedRecoveryBootAssets,
   357  		// kernel command lines are set later once a boot config is
   358  		// installed
   359  		CurrentKernelCommandLines: nil,
   360  		// keep this comment to make gofmt 1.9 happy
   361  		Base:           filepath.Base(bootWith.BasePath),
   362  		CurrentKernels: []string{bootWith.Kernel.Filename()},
   363  		BrandID:        model.BrandID(),
   364  		Model:          model.Model(),
   365  		Grade:          string(model.Grade()),
   366  		ModelSignKeyID: model.SignKeyID(),
   367  	}
   368  
   369  	// get the ubuntu-boot bootloader and extract the kernel there
   370  	opts := &bootloader.Options{
   371  		// Bootloader for run mode
   372  		Role: bootloader.RoleRunMode,
   373  		// At this point the run mode bootloader is under the native
   374  		// run partition layout, no /boot mount.
   375  		NoSlashBoot: true,
   376  	}
   377  	// the bootloader config may have been installed when the ubuntu-boot
   378  	// partition was created, but for a trusted assets the bootloader config
   379  	// will be installed further down; for now identify the run mode
   380  	// bootloader by looking at the gadget
   381  	bl, err := bootloader.ForGadget(bootWith.UnpackedGadgetDir, InitramfsUbuntuBootDir, opts)
   382  	if err != nil {
   383  		return fmt.Errorf("internal error: cannot identify run system bootloader: %v", err)
   384  	}
   385  
   386  	// extract the kernel first and mark kernel_status ready
   387  	kernelf, err := snapfile.Open(bootWith.KernelPath)
   388  	if err != nil {
   389  		return err
   390  	}
   391  
   392  	err = bl.ExtractKernelAssets(bootWith.Kernel, kernelf)
   393  	if err != nil {
   394  		return err
   395  	}
   396  
   397  	blVars := map[string]string{
   398  		"kernel_status": "",
   399  	}
   400  
   401  	ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader)
   402  	if ok {
   403  		// the bootloader supports additional extracted kernel handling
   404  
   405  		// enable the kernel on the bootloader and finally transition to
   406  		// run-mode last in case we get rebooted in between anywhere here
   407  
   408  		// it's okay to enable the kernel before writing the boot vars, because
   409  		// we haven't written snapd_recovery_mode=run, which is the critical
   410  		// thing that will inform the bootloader to try booting from ubuntu-boot
   411  		if err := ebl.EnableKernel(bootWith.Kernel); err != nil {
   412  			return err
   413  		}
   414  	} else {
   415  		// the bootloader does not support additional handling of
   416  		// extracted kernel images, we must name the kernel to be used
   417  		// explicitly in bootloader variables
   418  		blVars["snap_kernel"] = bootWith.Kernel.Filename()
   419  	}
   420  
   421  	// set the ubuntu-boot bootloader variables before triggering transition to
   422  	// try and boot from ubuntu-boot (that transition happens when we write
   423  	// snapd_recovery_mode below)
   424  	if err := bl.SetBootVars(blVars); err != nil {
   425  		return fmt.Errorf("cannot set run system environment: %v", err)
   426  	}
   427  
   428  	_, ok = bl.(bootloader.TrustedAssetsBootloader)
   429  	if ok {
   430  		// the bootloader can manage its boot config
   431  
   432  		// installing boot config must be performed after the boot
   433  		// partition has been populated with gadget data
   434  		if err := bl.InstallBootConfig(bootWith.UnpackedGadgetDir, opts); err != nil {
   435  			return fmt.Errorf("cannot install managed bootloader assets: %v", err)
   436  		}
   437  		// determine the expected command line
   438  		cmdline, err := ComposeCandidateCommandLine(model, bootWith.UnpackedGadgetDir)
   439  		if err != nil {
   440  			return fmt.Errorf("cannot compose the candidate command line: %v", err)
   441  		}
   442  		modeenv.CurrentKernelCommandLines = bootCommandLines{cmdline}
   443  
   444  		cmdlineVars, err := bootVarsForTrustedCommandLineFromGadget(bootWith.UnpackedGadgetDir)
   445  		if err != nil {
   446  			return fmt.Errorf("cannot prepare bootloader variables for kernel command line: %v", err)
   447  		}
   448  		if err := bl.SetBootVars(cmdlineVars); err != nil {
   449  			return fmt.Errorf("cannot set run system kernel command line arguments: %v", err)
   450  		}
   451  	}
   452  
   453  	// all fields that needed to be set in the modeenv must have been set by
   454  	// now, write modeenv to disk
   455  	if err := modeenv.WriteTo(InstallHostWritableDir); err != nil {
   456  		return fmt.Errorf("cannot write modeenv: %v", err)
   457  	}
   458  
   459  	if sealer != nil {
   460  		// seal the encryption key to the parameters specified in modeenv
   461  		if err := sealKeyToModeenv(sealer.dataEncryptionKey, sealer.saveEncryptionKey, model, modeenv); err != nil {
   462  			return err
   463  		}
   464  	}
   465  
   466  	return nil
   467  }