github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/bootloader/grub.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-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 bootloader
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/snapcore/snapd/bootloader/assets"
    29  	"github.com/snapcore/snapd/bootloader/grubenv"
    30  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/snap"
    32  	"github.com/snapcore/snapd/strutil"
    33  )
    34  
    35  // sanity - grub implements the required interfaces
    36  var (
    37  	_ Bootloader                        = (*grub)(nil)
    38  	_ installableBootloader             = (*grub)(nil)
    39  	_ RecoveryAwareBootloader           = (*grub)(nil)
    40  	_ ExtractedRunKernelImageBootloader = (*grub)(nil)
    41  	_ TrustedAssetsBootloader           = (*grub)(nil)
    42  )
    43  
    44  type grub struct {
    45  	rootdir string
    46  
    47  	basedir string
    48  
    49  	uefiRunKernelExtraction bool
    50  	recovery                bool
    51  	nativePartitionLayout   bool
    52  }
    53  
    54  // newGrub create a new Grub bootloader object
    55  func newGrub(rootdir string, opts *Options) Bootloader {
    56  	g := &grub{rootdir: rootdir}
    57  	if opts != nil {
    58  		// Set the flag to extract the run kernel, only
    59  		// for UC20 run mode.
    60  		// Both UC16/18 and the recovery mode of UC20 load
    61  		// the kernel directly from snaps.
    62  		g.uefiRunKernelExtraction = opts.Role == RoleRunMode
    63  		g.recovery = opts.Role == RoleRecovery
    64  		g.nativePartitionLayout = opts.NoSlashBoot || g.recovery
    65  	}
    66  	if g.nativePartitionLayout {
    67  		g.basedir = "EFI/ubuntu"
    68  	} else {
    69  		g.basedir = "boot/grub"
    70  	}
    71  
    72  	return g
    73  }
    74  
    75  func (g *grub) Name() string {
    76  	return "grub"
    77  }
    78  
    79  func (g *grub) setRootDir(rootdir string) {
    80  	g.rootdir = rootdir
    81  }
    82  
    83  func (g *grub) dir() string {
    84  	if g.rootdir == "" {
    85  		panic("internal error: unset rootdir")
    86  	}
    87  	return filepath.Join(g.rootdir, g.basedir)
    88  }
    89  
    90  func (g *grub) installManagedRecoveryBootConfig(gadgetDir string) (bool, error) {
    91  	gadgetGrubCfg := filepath.Join(gadgetDir, g.Name()+".conf")
    92  	if !osutil.FileExists(gadgetGrubCfg) {
    93  		// gadget does not use grub bootloader
    94  		return false, nil
    95  	}
    96  	assetName := g.Name() + "-recovery.cfg"
    97  	systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg")
    98  	return genericSetBootConfigFromAsset(systemFile, assetName)
    99  }
   100  
   101  func (g *grub) installManagedBootConfig(gadgetDir string) (bool, error) {
   102  	gadgetGrubCfg := filepath.Join(gadgetDir, g.Name()+".conf")
   103  	if !osutil.FileExists(gadgetGrubCfg) {
   104  		// gadget does not use grub bootloader
   105  		return false, nil
   106  	}
   107  	assetName := g.Name() + ".cfg"
   108  	systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg")
   109  	return genericSetBootConfigFromAsset(systemFile, assetName)
   110  }
   111  
   112  func (g *grub) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) {
   113  	if opts != nil && opts.Role == RoleRecovery {
   114  		// install managed config for the recovery partition
   115  		return g.installManagedRecoveryBootConfig(gadgetDir)
   116  	}
   117  	if opts != nil && opts.Role == RoleRunMode {
   118  		// install managed boot config that can handle kernel.efi
   119  		return g.installManagedBootConfig(gadgetDir)
   120  	}
   121  
   122  	gadgetFile := filepath.Join(gadgetDir, g.Name()+".conf")
   123  	systemFile := filepath.Join(g.rootdir, "/boot/grub/grub.cfg")
   124  	return genericInstallBootConfig(gadgetFile, systemFile)
   125  }
   126  
   127  func (g *grub) SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error {
   128  	if recoverySystemDir == "" {
   129  		return fmt.Errorf("internal error: recoverySystemDir unset")
   130  	}
   131  	recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv")
   132  	if err := os.MkdirAll(filepath.Dir(recoverySystemGrubEnv), 0755); err != nil {
   133  		return err
   134  	}
   135  	genv := grubenv.NewEnv(recoverySystemGrubEnv)
   136  	for k, v := range values {
   137  		genv.Set(k, v)
   138  	}
   139  	return genv.Save()
   140  }
   141  
   142  func (g *grub) GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) {
   143  	if recoverySystemDir == "" {
   144  		return "", fmt.Errorf("internal error: recoverySystemDir unset")
   145  	}
   146  	recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv")
   147  	genv := grubenv.NewEnv(recoverySystemGrubEnv)
   148  	if err := genv.Load(); err != nil {
   149  		if os.IsNotExist(err) {
   150  			return "", nil
   151  		}
   152  		return "", err
   153  	}
   154  	return genv.Get(key), nil
   155  }
   156  
   157  func (g *grub) ConfigFile() string {
   158  	return filepath.Join(g.dir(), "grub.cfg")
   159  }
   160  
   161  func (g *grub) envFile() string {
   162  	return filepath.Join(g.dir(), "grubenv")
   163  }
   164  
   165  func (g *grub) GetBootVars(names ...string) (map[string]string, error) {
   166  	out := make(map[string]string)
   167  
   168  	env := grubenv.NewEnv(g.envFile())
   169  	if err := env.Load(); err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	for _, name := range names {
   174  		out[name] = env.Get(name)
   175  	}
   176  
   177  	return out, nil
   178  }
   179  
   180  func (g *grub) SetBootVars(values map[string]string) error {
   181  	env := grubenv.NewEnv(g.envFile())
   182  	if err := env.Load(); err != nil && !os.IsNotExist(err) {
   183  		return err
   184  	}
   185  	for k, v := range values {
   186  		env.Set(k, v)
   187  	}
   188  	return env.Save()
   189  }
   190  
   191  func (g *grub) extractedKernelDir(prefix string, s snap.PlaceInfo) string {
   192  	return filepath.Join(
   193  		prefix,
   194  		s.Filename(),
   195  	)
   196  }
   197  
   198  func (g *grub) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
   199  	// default kernel assets are:
   200  	// - kernel.img
   201  	// - initrd.img
   202  	// - dtbs/*
   203  	var assets []string
   204  	if g.uefiRunKernelExtraction {
   205  		assets = []string{"kernel.efi"}
   206  	} else {
   207  		assets = []string{"kernel.img", "initrd.img", "dtbs/*"}
   208  	}
   209  
   210  	// extraction can be forced through either a special file in the kernel snap
   211  	// or through an option in the bootloader
   212  	_, err := snapf.ReadFile("meta/force-kernel-extraction")
   213  	if g.uefiRunKernelExtraction || err == nil {
   214  		return extractKernelAssetsToBootDir(
   215  			g.extractedKernelDir(g.dir(), s),
   216  			snapf,
   217  			assets,
   218  		)
   219  	}
   220  	return nil
   221  }
   222  
   223  func (g *grub) RemoveKernelAssets(s snap.PlaceInfo) error {
   224  	return removeKernelAssetsFromBootDir(g.dir(), s)
   225  }
   226  
   227  // ExtractedRunKernelImageBootloader helper methods
   228  
   229  func (g *grub) makeKernelEfiSymlink(s snap.PlaceInfo, name string) error {
   230  	// use a relative symlink destination so that it resolves properly, if grub
   231  	// is located at /run/mnt/ubuntu-boot or /boot/grub, etc.
   232  	target := filepath.Join(
   233  		s.Filename(),
   234  		"kernel.efi",
   235  	)
   236  
   237  	// the location of the destination symlink as an absolute filepath
   238  	source := filepath.Join(g.dir(), name)
   239  
   240  	// check that the kernel snap has been extracted already so we don't
   241  	// inadvertently create a dangling symlink
   242  	// expand the relative symlink from g.dir()
   243  	if !osutil.FileExists(filepath.Join(g.dir(), target)) {
   244  		return fmt.Errorf(
   245  			"cannot enable %s at %s: %v",
   246  			name,
   247  			target,
   248  			os.ErrNotExist,
   249  		)
   250  	}
   251  
   252  	// the symlink doesn't exist so just create it
   253  	return osutil.AtomicSymlink(target, source)
   254  }
   255  
   256  // unlinkKernelEfiSymlink will remove the specified symlink if it exists. Note
   257  // that if the symlink is "dangling", it will still remove the symlink without
   258  // returning an error. This is useful for example to disable a try-kernel that
   259  // was incorrectly created.
   260  func (g *grub) unlinkKernelEfiSymlink(name string) error {
   261  	symlink := filepath.Join(g.dir(), name)
   262  	err := os.Remove(symlink)
   263  	if err != nil && !os.IsNotExist(err) {
   264  		return err
   265  	}
   266  	return nil
   267  }
   268  
   269  func (g *grub) readKernelSymlink(name string) (snap.PlaceInfo, error) {
   270  	// read the symlink from <grub-dir>/<name> to
   271  	// <grub-dir>/<snap-file-name>/<name> and parse the
   272  	// directory (which is supposed to be the name of the snap) into the snap
   273  	link := filepath.Join(g.dir(), name)
   274  
   275  	// check that the symlink is not dangling before continuing
   276  	if !osutil.FileExists(link) {
   277  		return nil, fmt.Errorf("cannot read dangling symlink %s", name)
   278  	}
   279  
   280  	targetKernelEfi, err := os.Readlink(link)
   281  	if err != nil {
   282  		return nil, fmt.Errorf("cannot read %s symlink: %v", link, err)
   283  	}
   284  
   285  	kernelSnapFileName := filepath.Base(filepath.Dir(targetKernelEfi))
   286  	sn, err := snap.ParsePlaceInfoFromSnapFileName(kernelSnapFileName)
   287  	if err != nil {
   288  		return nil, fmt.Errorf(
   289  			"cannot parse kernel snap file name from symlink target %q: %v",
   290  			kernelSnapFileName,
   291  			err,
   292  		)
   293  	}
   294  	return sn, nil
   295  }
   296  
   297  // actual ExtractedRunKernelImageBootloader methods
   298  
   299  // EnableKernel will install a kernel.efi symlink in the bootloader partition,
   300  // pointing to the referenced kernel snap. EnableKernel() will fail if the
   301  // referenced kernel snap does not exist.
   302  func (g *grub) EnableKernel(s snap.PlaceInfo) error {
   303  	// add symlink from ubuntuBootPartition/kernel.efi to
   304  	// <ubuntu-boot>/EFI/ubuntu/<snap-name>.snap/kernel.efi
   305  	// so that we are consistent between uc16/uc18 and uc20 with where we
   306  	// extract kernels
   307  	return g.makeKernelEfiSymlink(s, "kernel.efi")
   308  }
   309  
   310  // EnableTryKernel will install a try-kernel.efi symlink in the bootloader
   311  // partition, pointing towards the referenced kernel snap. EnableTryKernel()
   312  // will fail if the referenced kernel snap does not exist.
   313  func (g *grub) EnableTryKernel(s snap.PlaceInfo) error {
   314  	// add symlink from ubuntuBootPartition/kernel.efi to
   315  	// <ubuntu-boot>/EFI/ubuntu/<snap-name>.snap/kernel.efi
   316  	// so that we are consistent between uc16/uc18 and uc20 with where we
   317  	// extract kernels
   318  	return g.makeKernelEfiSymlink(s, "try-kernel.efi")
   319  }
   320  
   321  // DisableTryKernel will remove the try-kernel.efi symlink if it exists. Note
   322  // that when performing an update, you should probably first use EnableKernel(),
   323  // then DisableTryKernel() for maximum safety.
   324  func (g *grub) DisableTryKernel() error {
   325  	return g.unlinkKernelEfiSymlink("try-kernel.efi")
   326  }
   327  
   328  // Kernel will return the kernel snap currently installed in the bootloader
   329  // partition, pointed to by the kernel.efi symlink.
   330  func (g *grub) Kernel() (snap.PlaceInfo, error) {
   331  	return g.readKernelSymlink("kernel.efi")
   332  }
   333  
   334  // TryKernel will return the kernel snap currently being tried if it exists and
   335  // false if there is not currently a try-kernel.efi symlink. Note if the symlink
   336  // exists but does not point to an existing file an error will be returned.
   337  func (g *grub) TryKernel() (snap.PlaceInfo, error) {
   338  	// check that the _symlink_ exists, not that it points to something real
   339  	// we check for whether it is a dangling symlink inside readKernelSymlink,
   340  	// which returns an error when the symlink is dangling
   341  	_, err := os.Lstat(filepath.Join(g.dir(), "try-kernel.efi"))
   342  	if err == nil {
   343  		p, err := g.readKernelSymlink("try-kernel.efi")
   344  		// if we failed to read the symlink, then the try kernel isn't usable,
   345  		// so return err because the symlink is there
   346  		if err != nil {
   347  			return nil, err
   348  		}
   349  		return p, nil
   350  	}
   351  	return nil, ErrNoTryKernelRef
   352  }
   353  
   354  // UpdateBootConfig updates the grub boot config only if it is already managed
   355  // and has a lower edition.
   356  //
   357  // Implements ManagedAssetsBootloader for the grub bootloader.
   358  func (g *grub) UpdateBootConfig(opts *Options) error {
   359  	// XXX: do we need to take opts here?
   360  	bootScriptName := "grub.cfg"
   361  	currentBootConfig := filepath.Join(g.dir(), "grub.cfg")
   362  	if opts != nil && opts.Role == RoleRecovery {
   363  		// use the recovery asset when asked to do so
   364  		bootScriptName = "grub-recovery.cfg"
   365  	}
   366  	return genericUpdateBootConfigFromAssets(currentBootConfig, bootScriptName)
   367  }
   368  
   369  // ManagedAssets returns a list relative paths to boot assets inside the root
   370  // directory of the filesystem.
   371  //
   372  // Implements ManagedAssetsBootloader for the grub bootloader.
   373  func (g *grub) ManagedAssets() []string {
   374  	return []string{
   375  		filepath.Join(g.basedir, "grub.cfg"),
   376  	}
   377  }
   378  
   379  func (g *grub) commandLineForEdition(edition uint, modeArg, systemArg, extraArgs string) (string, error) {
   380  	assetName := "grub.cfg"
   381  	if g.recovery {
   382  		assetName = "grub-recovery.cfg"
   383  	}
   384  	staticCmdline := staticCommandLineForGrubAssetEdition(assetName, edition)
   385  	args, err := strutil.KernelCommandLineSplit(staticCmdline + " " + extraArgs)
   386  	if err != nil {
   387  		return "", fmt.Errorf("cannot use badly formatted kernel command line: %v", err)
   388  	}
   389  	// join all argument with a single space, see
   390  	// grub-core/lib/cmdline.c:grub_create_loader_cmdline() for reference,
   391  	// arguments are separated by a single space, the space after last is
   392  	// replaced with terminating NULL
   393  	snapdArgs := make([]string, 0, 2)
   394  	if modeArg != "" {
   395  		snapdArgs = append(snapdArgs, modeArg)
   396  	}
   397  	if systemArg != "" {
   398  		snapdArgs = append(snapdArgs, systemArg)
   399  	}
   400  	return strings.Join(append(snapdArgs, args...), " "), nil
   401  }
   402  
   403  // CommandLine returns the kernel command line composed of mode and
   404  // system arguments, built-in bootloader specific static arguments
   405  // corresponding to the on-disk boot asset edition, followed by any
   406  // extra arguments. The command line may be different when using a
   407  // recovery bootloader.
   408  //
   409  // Implements ManagedAssetsBootloader for the grub bootloader.
   410  func (g *grub) CommandLine(modeArg, systemArg, extraArgs string) (string, error) {
   411  	currentBootConfig := filepath.Join(g.dir(), "grub.cfg")
   412  	edition, err := editionFromDiskConfigAsset(currentBootConfig)
   413  	if err != nil {
   414  		if err != errNoEdition {
   415  			return "", fmt.Errorf("cannot obtain edition number of current boot config: %v", err)
   416  		}
   417  		// we were called using the ManagedAssetsBootloader interface
   418  		// meaning the caller expects to us to use the managed assets,
   419  		// since one on disk is not managed, use the initial edition of
   420  		// the internal boot asset which is compatible with grub.cfg
   421  		// used before we started writing out the files ourselves
   422  		edition = 1
   423  	}
   424  	return g.commandLineForEdition(edition, modeArg, systemArg, extraArgs)
   425  }
   426  
   427  // CandidateCommandLine is similar to CommandLine, but uses the current
   428  // edition of managed built-in boot assets as reference.
   429  //
   430  // Implements ManagedAssetsBootloader for the grub bootloader.
   431  func (g *grub) CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) {
   432  	assetName := "grub.cfg"
   433  	if g.recovery {
   434  		assetName = "grub-recovery.cfg"
   435  	}
   436  	edition, err := editionFromInternalConfigAsset(assetName)
   437  	if err != nil {
   438  		return "", err
   439  	}
   440  	return g.commandLineForEdition(edition, modeArg, systemArg, extraArgs)
   441  }
   442  
   443  // staticCommandLineForGrubAssetEdition fetches a static command line for given
   444  // grub asset edition
   445  func staticCommandLineForGrubAssetEdition(asset string, edition uint) string {
   446  	cmdline := assets.SnippetForEdition(fmt.Sprintf("%s:static-cmdline", asset), edition)
   447  	if cmdline == nil {
   448  		return ""
   449  	}
   450  	return string(cmdline)
   451  }
   452  
   453  var (
   454  	grubRecoveryModeTrustedAssets = []string{
   455  		// recovery mode shim EFI binary
   456  		"EFI/boot/bootx64.efi",
   457  		// recovery mode grub EFI binary
   458  		"EFI/boot/grubx64.efi",
   459  	}
   460  
   461  	grubRunModeTrustedAssets = []string{
   462  		// run mode grub EFI binary
   463  		"EFI/boot/grubx64.efi",
   464  	}
   465  )
   466  
   467  // TrustedAssets returns the list of relative paths to assets inside
   468  // the bootloader's rootdir that are measured in the boot process in the
   469  // order of loading during the boot.
   470  func (g *grub) TrustedAssets() ([]string, error) {
   471  	if !g.nativePartitionLayout {
   472  		return nil, fmt.Errorf("internal error: trusted assets called without native host-partition layout")
   473  	}
   474  	if g.recovery {
   475  		return grubRecoveryModeTrustedAssets, nil
   476  	}
   477  	return grubRunModeTrustedAssets, nil
   478  }
   479  
   480  // RecoveryBootChain returns the load chain for recovery modes.
   481  // It should be called on a RoleRecovery bootloader.
   482  func (g *grub) RecoveryBootChain(kernelPath string) ([]BootFile, error) {
   483  	if !g.recovery {
   484  		return nil, fmt.Errorf("not a recovery bootloader")
   485  	}
   486  
   487  	// add trusted assets to the recovery chain
   488  	chain := make([]BootFile, 0, len(grubRecoveryModeTrustedAssets)+1)
   489  	for _, ta := range grubRecoveryModeTrustedAssets {
   490  		chain = append(chain, NewBootFile("", ta, RoleRecovery))
   491  	}
   492  	// add recovery kernel to the recovery chain
   493  	chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRecovery))
   494  
   495  	return chain, nil
   496  }
   497  
   498  // BootChain returns the load chain for run mode.
   499  // It should be called on a RoleRecovery bootloader passing the
   500  // RoleRunMode bootloader.
   501  func (g *grub) BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) {
   502  	if !g.recovery {
   503  		return nil, fmt.Errorf("not a recovery bootloader")
   504  	}
   505  	if runBl.Name() != "grub" {
   506  		return nil, fmt.Errorf("run mode bootloader must be grub")
   507  	}
   508  
   509  	// add trusted assets to the recovery chain
   510  	chain := make([]BootFile, 0, len(grubRecoveryModeTrustedAssets)+len(grubRunModeTrustedAssets)+1)
   511  	for _, ta := range grubRecoveryModeTrustedAssets {
   512  		chain = append(chain, NewBootFile("", ta, RoleRecovery))
   513  	}
   514  	for _, ta := range grubRunModeTrustedAssets {
   515  		chain = append(chain, NewBootFile("", ta, RoleRunMode))
   516  	}
   517  	// add kernel to the boot chain
   518  	chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRunMode))
   519  
   520  	return chain, nil
   521  }