github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/bootloader/bootloader.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  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	"github.com/snapcore/snapd/bootloader/assets"
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/snap"
    32  )
    33  
    34  var (
    35  	// ErrBootloader is returned if the bootloader can not be determined.
    36  	ErrBootloader = errors.New("cannot determine bootloader")
    37  
    38  	// ErrNoTryKernelRef is returned if the bootloader finds no enabled
    39  	// try-kernel.
    40  	ErrNoTryKernelRef = errors.New("no try-kernel referenced")
    41  )
    42  
    43  // Role indicates whether the bootloader is used for recovery or run mode.
    44  type Role string
    45  
    46  const (
    47  	// RoleSole applies to the sole bootloader used by UC16/18.
    48  	RoleSole Role = ""
    49  	// RoleRunMode applies to the run mode booloader.
    50  	RoleRunMode Role = "run-mode"
    51  	// RoleRecovery apllies to the recovery bootloader.
    52  	RoleRecovery Role = "recovery"
    53  )
    54  
    55  // Options carries bootloader options.
    56  type Options struct {
    57  	// PrepareImageTime indicates whether the booloader is being
    58  	// used at prepare-image time, that means not on a runtime
    59  	// system.
    60  	PrepareImageTime bool
    61  
    62  	// Role specifies to use the bootloader for the given role.
    63  	Role Role
    64  
    65  	// NoSlashBoot indicates to use the native layout of the
    66  	// bootloader partition and not the /boot mount.
    67  	// It applies only for RoleRunMode.
    68  	// It is implied and ignored for RoleRecovery.
    69  	// It is an error to set it for RoleSole.
    70  	NoSlashBoot bool
    71  }
    72  
    73  func (o *Options) validate() error {
    74  	if o == nil {
    75  		return nil
    76  	}
    77  	if o.NoSlashBoot && o.Role == RoleSole {
    78  		return fmt.Errorf("internal error: bootloader.RoleSole doesn't expect NoSlashBoot set")
    79  	}
    80  	if o.PrepareImageTime && o.Role == RoleRunMode {
    81  		return fmt.Errorf("internal error: cannot use run mode bootloader at prepare-image time")
    82  	}
    83  	return nil
    84  }
    85  
    86  // Bootloader provides an interface to interact with the system
    87  // bootloader.
    88  type Bootloader interface {
    89  	// Return the value of the specified bootloader variable.
    90  	GetBootVars(names ...string) (map[string]string, error)
    91  
    92  	// Set the value of the specified bootloader variable.
    93  	SetBootVars(values map[string]string) error
    94  
    95  	// Name returns the bootloader name.
    96  	Name() string
    97  
    98  	// Present returns whether the bootloader is currently present on the
    99  	// system - in other words whether this bootloader has been installed to the
   100  	// current system. Implementations should only return non-nil error if they
   101  	// can positively identify that the bootloader is installed, but there is
   102  	// actually an error with the installation.
   103  	Present() (bool, error)
   104  
   105  	// InstallBootConfig will try to install the boot config in the
   106  	// given gadgetDir to rootdir.
   107  	InstallBootConfig(gadgetDir string, opts *Options) error
   108  
   109  	// ExtractKernelAssets extracts kernel assets from the given kernel snap.
   110  	ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error
   111  
   112  	// RemoveKernelAssets removes the assets for the given kernel snap.
   113  	RemoveKernelAssets(s snap.PlaceInfo) error
   114  }
   115  
   116  type RecoveryAwareBootloader interface {
   117  	Bootloader
   118  	SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error
   119  	GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error)
   120  }
   121  
   122  type ExtractedRecoveryKernelImageBootloader interface {
   123  	Bootloader
   124  	ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error
   125  }
   126  
   127  // ExtractedRunKernelImageBootloader is a Bootloader that also supports specific
   128  // methods needed to setup booting from an extracted kernel, which is needed to
   129  // implement encryption and/or secure boot. Prototypical implementation is UC20
   130  // grub implementation with FDE.
   131  type ExtractedRunKernelImageBootloader interface {
   132  	Bootloader
   133  
   134  	// EnableKernel enables the specified kernel on ubuntu-boot to be used
   135  	// during normal boots. The specified kernel should already have been
   136  	// extracted. This is usually implemented with a "kernel.efi" symlink
   137  	// pointing to the extracted kernel image.
   138  	EnableKernel(snap.PlaceInfo) error
   139  
   140  	// EnableTryKernel enables the specified kernel on ubuntu-boot to be
   141  	// tried by the bootloader on a reboot, to be used in conjunction with
   142  	// setting "kernel_status" to "try". The specified kernel should already
   143  	// have been extracted. This is usually implemented with a
   144  	// "try-kernel.efi" symlink pointing to the extracted kernel image.
   145  	EnableTryKernel(snap.PlaceInfo) error
   146  
   147  	// Kernel returns the current enabled kernel on the bootloader, not
   148  	// necessarily the kernel that was used to boot the current session, but the
   149  	// kernel that is enabled to boot on "normal" boots.
   150  	// If error is not nil, the first argument shall be non-nil.
   151  	Kernel() (snap.PlaceInfo, error)
   152  
   153  	// TryKernel returns the current enabled try-kernel on the bootloader, if
   154  	// there is no such enabled try-kernel, then ErrNoTryKernelRef is returned.
   155  	// If error is not nil, the first argument shall be non-nil.
   156  	TryKernel() (snap.PlaceInfo, error)
   157  
   158  	// DisableTryKernel disables the current enabled try-kernel on the
   159  	// bootloader, if it exists. It does not need to return an error if the
   160  	// enabled try-kernel does not exist or is in an inconsistent state before
   161  	// disabling it, errors should only be returned when the implementation
   162  	// fails to disable the try-kernel.
   163  	DisableTryKernel() error
   164  }
   165  
   166  // ComamndLineComponents carries the components of the kernel command line. The
   167  // bootloader is expected to combine the provided components, optionally
   168  // including its built-in static set of arguments, and produce a command line
   169  // that will be passed to the kernel during boot.
   170  type CommandLineComponents struct {
   171  	// Argument related to mode selection.
   172  	ModeArg string
   173  	// Argument related to recovery system selection, relevant for given
   174  	// mode argument.
   175  	SystemArg string
   176  	// Extra arguments requested by the system.
   177  	ExtraArgs string
   178  	// A complete set of arguments that overrides both the built-in static
   179  	// set and ExtraArgs. Note that, it is an error if extra and full
   180  	// arguments are non-empty.
   181  	FullArgs string
   182  }
   183  
   184  func (c *CommandLineComponents) Validate() error {
   185  	if c.ExtraArgs != "" && c.FullArgs != "" {
   186  		return fmt.Errorf("cannot use both full and extra components of command line")
   187  	}
   188  	return nil
   189  }
   190  
   191  // TrustedAssetsBootloader has boot assets that take part in the secure boot
   192  // process and need to be tracked, while other boot assets (typically boot
   193  // config) are managed by snapd.
   194  type TrustedAssetsBootloader interface {
   195  	Bootloader
   196  
   197  	// ManagedAssets returns a list of boot assets managed by the bootloader
   198  	// in the boot filesystem. Does not require rootdir to be set.
   199  	ManagedAssets() []string
   200  	// UpdateBootConfig attempts to update the boot config assets used by
   201  	// the bootloader. Returns true when assets were updated.
   202  	UpdateBootConfig() (bool, error)
   203  	// CommandLine returns the kernel command line composed of mode and
   204  	// system arguments, followed by either a built-in bootloader specific
   205  	// static arguments corresponding to the on-disk boot asset edition, and
   206  	// any extra arguments or a separate set of arguments provided in the
   207  	// components. The command line may be different when using a recovery
   208  	// bootloader.
   209  	CommandLine(pieces CommandLineComponents) (string, error)
   210  	// CandidateCommandLine is similar to CommandLine, but uses the current
   211  	// edition of managed built-in boot assets as reference.
   212  	CandidateCommandLine(pieces CommandLineComponents) (string, error)
   213  
   214  	// TrustedAssets returns the list of relative paths to assets inside the
   215  	// bootloader's rootdir that are measured in the boot process in the
   216  	// order of loading during the boot. Does not require rootdir to be set.
   217  	TrustedAssets() ([]string, error)
   218  
   219  	// RecoveryBootChain returns the load chain for recovery modes.
   220  	// It should be called on a RoleRecovery bootloader.
   221  	RecoveryBootChain(kernelPath string) ([]BootFile, error)
   222  
   223  	// BootChain returns the load chain for run mode.
   224  	// It should be called on a RoleRecovery bootloader passing the
   225  	// RoleRunMode bootloader.
   226  	BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error)
   227  }
   228  
   229  func genericInstallBootConfig(gadgetFile, systemFile string) error {
   230  	if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil {
   231  		return err
   232  	}
   233  	return osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite)
   234  }
   235  
   236  func genericSetBootConfigFromAsset(systemFile, assetName string) error {
   237  	bootConfig := assets.Internal(assetName)
   238  	if bootConfig == nil {
   239  		return fmt.Errorf("internal error: no boot asset for %q", assetName)
   240  	}
   241  	if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil {
   242  		return err
   243  	}
   244  	return osutil.AtomicWriteFile(systemFile, bootConfig, 0644, 0)
   245  }
   246  
   247  func genericUpdateBootConfigFromAssets(systemFile string, assetName string) (updated bool, err error) {
   248  	currentBootConfigEdition, err := editionFromDiskConfigAsset(systemFile)
   249  	if err != nil && err != errNoEdition {
   250  		return false, err
   251  	}
   252  	if err == errNoEdition {
   253  		return false, nil
   254  	}
   255  	newBootConfig := assets.Internal(assetName)
   256  	if len(newBootConfig) == 0 {
   257  		return false, fmt.Errorf("no boot config asset with name %q", assetName)
   258  	}
   259  	bc, err := configAssetFrom(newBootConfig)
   260  	if err != nil {
   261  		return false, err
   262  	}
   263  	if bc.Edition() <= currentBootConfigEdition {
   264  		// edition of the candidate boot config is lower than or equal
   265  		// to one currently installed
   266  		return false, nil
   267  	}
   268  	if err := osutil.AtomicWriteFile(systemFile, bc.Raw(), 0644, 0); err != nil {
   269  		return false, err
   270  	}
   271  	return true, nil
   272  }
   273  
   274  // InstallBootConfig installs the bootloader config from the gadget
   275  // snap dir into the right place.
   276  func InstallBootConfig(gadgetDir, rootDir string, opts *Options) error {
   277  	if err := opts.validate(); err != nil {
   278  		return err
   279  	}
   280  	bl, err := ForGadget(gadgetDir, rootDir, opts)
   281  	if err != nil {
   282  		return fmt.Errorf("cannot find boot config in %q", gadgetDir)
   283  	}
   284  	return bl.InstallBootConfig(gadgetDir, opts)
   285  }
   286  
   287  type bootloaderNewFunc func(rootdir string, opts *Options) Bootloader
   288  
   289  var (
   290  	//  bootloaders list all possible bootloaders by their constructor
   291  	//  function.
   292  	bootloaders = []bootloaderNewFunc{
   293  		newUboot,
   294  		newGrub,
   295  		newAndroidBoot,
   296  		newLk,
   297  	}
   298  )
   299  
   300  var (
   301  	forcedBootloader Bootloader
   302  	forcedError      error
   303  )
   304  
   305  // Find returns the bootloader for the system
   306  // or an error if no bootloader is found.
   307  //
   308  // The rootdir option is useful for image creation operations. It
   309  // can also be used to find the recovery bootloader, e.g. on uc20:
   310  //   bootloader.Find("/run/mnt/ubuntu-seed")
   311  func Find(rootdir string, opts *Options) (Bootloader, error) {
   312  	if err := opts.validate(); err != nil {
   313  		return nil, err
   314  	}
   315  	if forcedBootloader != nil || forcedError != nil {
   316  		return forcedBootloader, forcedError
   317  	}
   318  
   319  	if rootdir == "" {
   320  		rootdir = dirs.GlobalRootDir
   321  	}
   322  	if opts == nil {
   323  		opts = &Options{}
   324  	}
   325  
   326  	// note that the order of this is not deterministic
   327  	for _, blNew := range bootloaders {
   328  		bl := blNew(rootdir, opts)
   329  		present, err := bl.Present()
   330  		if err != nil {
   331  			return nil, fmt.Errorf("bootloader %q found but not usable: %v", bl.Name(), err)
   332  		}
   333  		if present {
   334  			return bl, nil
   335  		}
   336  	}
   337  	// no, weeeee
   338  	return nil, ErrBootloader
   339  }
   340  
   341  // Force can be used to force Find to always find the specified bootloader; use
   342  // nil to reset to normal lookup.
   343  func Force(booloader Bootloader) {
   344  	forcedBootloader = booloader
   345  	forcedError = nil
   346  }
   347  
   348  // ForceError can be used to force Find to return an error; use nil to
   349  // reset to normal lookup.
   350  func ForceError(err error) {
   351  	forcedBootloader = nil
   352  	forcedError = err
   353  }
   354  
   355  func extractKernelAssetsToBootDir(dstDir string, snapf snap.Container, assets []string) error {
   356  	// now do the kernel specific bits
   357  	if err := os.MkdirAll(dstDir, 0755); err != nil {
   358  		return err
   359  	}
   360  	dir, err := os.Open(dstDir)
   361  	if err != nil {
   362  		return err
   363  	}
   364  	defer dir.Close()
   365  
   366  	for _, src := range assets {
   367  		if err := snapf.Unpack(src, dstDir); err != nil {
   368  			return err
   369  		}
   370  		if err := dir.Sync(); err != nil {
   371  			return err
   372  		}
   373  	}
   374  	return nil
   375  }
   376  
   377  func removeKernelAssetsFromBootDir(bootDir string, s snap.PlaceInfo) error {
   378  	// remove the kernel blob
   379  	blobName := s.Filename()
   380  	dstDir := filepath.Join(bootDir, blobName)
   381  	if err := os.RemoveAll(dstDir); err != nil {
   382  		return err
   383  	}
   384  
   385  	return nil
   386  }
   387  
   388  // ForGadget returns a bootloader matching a given gadget by inspecting the
   389  // contents of gadget directory or an error if no matching bootloader is found.
   390  func ForGadget(gadgetDir, rootDir string, opts *Options) (Bootloader, error) {
   391  	if err := opts.validate(); err != nil {
   392  		return nil, err
   393  	}
   394  	if forcedBootloader != nil || forcedError != nil {
   395  		return forcedBootloader, forcedError
   396  	}
   397  	for _, blNew := range bootloaders {
   398  		bl := blNew(rootDir, opts)
   399  		markerConf := filepath.Join(gadgetDir, bl.Name()+".conf")
   400  		// do we have a marker file?
   401  		if osutil.FileExists(markerConf) {
   402  			return bl, nil
   403  		}
   404  	}
   405  	return nil, ErrBootloader
   406  }
   407  
   408  // BootFile represents each file in the chains of trusted assets and
   409  // kernels used in the boot process. For example a boot file can be an
   410  // EFI binary or a snap file containing an EFI binary.
   411  type BootFile struct {
   412  	// Path is the path to the file in the filesystem or, if Snap
   413  	// is set, the relative path inside the snap file.
   414  	Path string
   415  	// Snap contains the path to the snap file if a snap file is used.
   416  	Snap string
   417  	// Role is set to the role of the bootloader this boot file
   418  	// originates from.
   419  	Role Role
   420  }
   421  
   422  func NewBootFile(snap, path string, role Role) BootFile {
   423  	return BootFile{
   424  		Snap: snap,
   425  		Path: path,
   426  		Role: role,
   427  	}
   428  }
   429  
   430  // WithPath returns a copy of the BootFile with path updated to the
   431  // specified value.
   432  func (b BootFile) WithPath(path string) BootFile {
   433  	b.Path = path
   434  	return b
   435  }