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