github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/bootloader/grub.go (about)

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