Files
snapd/bootloader/grub.go
Valentin David 2034c7edb2 boot,bootloader: add support for shim fallback and setting EFI boot variables on install (#13511)
* boot: added function to set EFI variables

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: renamed trustedShimFallbackBinary to seedShimPath

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: refactored setting EFI boot variables at install

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: adjusted variable names and fixed variable initialization

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: improve setting Boot#### EFI variable

Notably, splits off the process of reading a Boot#### variable and
extracting its DevicePath into its own function `readBootVariable` which
can be mocked and otherwise simplifies the `setBootNumberVariable`
function.

Also, fixes behavior around the final BootFFFF variable.  Previously, it
was not possible to select the BootFFFF variable if it was unused, due
to overflow concerns on uint16.  Now, the behavior around BootFFFF is
identical to that of any other boot variable, by using an int internally
instead of uint16, which also allows a more robust check for whether
there were no matching variables.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: added unit tests for setting EFI Boot#### variable

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: refactored setting EFI boot variables

Rewrote EFI boot variable functions to more closely match the behavior
of shim fallback: https://github.com/rhboot/shim/blob/main/fallback.c

In particular, the following have changed:

1. Existing Boot#### variables must fully match the new load option to
   be considered a match.  In particular, the load option attributes,
   label, and device path must all be byte-for-byte identical.
   Previously, only the device paths were compared.
2. Matching Boot#### variables are no longer overwritten.  Since the
   variable data must now byte-for-byte match the new load option, there
   is no need to overwrite the existing variable.
3. Since existing Boot#### variables are no longer overwritten, the
   variable attributes are no longer checked for those variables.
   Instead, it is assumed that the Boot#### variable attributes are
   viable for it to be used as a boot option.  This matches the behavior
   of `rhboot/shim/fallback.c`, for better or for worse.
4. When modifying the BootOrder variable, boot option numbers are no
   longer pruned if there is no matching Boot#### variable.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot,bootloader: introduce UefiBootloader to build EFI load options

Previously, the path of the shim binary relative to the EFI partition
was passed into `SetEfiBootVariables`. However, different bootloaders
may wish to set up `OptionalData` in the load option.

Additionally, not all `TrustedAssetBootloaders` will attempt to set
EFI boot variables, and not all bootloaders which should set EFI boot
variables necessarily support secure boot. Thus, these should be
decoupled.

This commit adds a new `UefiBootloader` interface with the
`ConstructShimEfiLoadOption` method, which builds an EFI load option
from the shim path for the given bootloader.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot,bootloader: fixed linting errors and improved EFI boot variable test clarity

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

bootloader: improved unit test for grub EFI load option creation

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: set EFI boot variables in `MakeRunnableSystem`

Previously, attempted to set boot variables in
`MakeRecoverySystemBootable`, which is called by `MakeBootableImage`,
which is called when building the image file, rather than during install
mode.

`MakeRunnableSystem` is called on first boot during install mode, and
thus should be responsible for setting EFI boot variables.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: use seed bootloader when setting EFI variables

In install mode, the bootloader located in ubuntu-seed should be used
when setting the EFI boot variables. Previously, the bootloader in
ubuntu-boot was accidentally re-used.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: added simple test to execute setefibootvar.go code

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: fixed standalone set EFI vars code test to work with different layouts

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: moved simple setefibootvar.go check to nested test

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: added check for idempotence when setting EFI boot variables

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

bootloader: adjust comments, organization, and add TODO

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot,bootloader: fix setting EFI boot variables

Make function to search for EFI asset device path and construct load
option common so each UefiBootloader does not have to re-implement it.
Instead, the bootloader returns the description, asset file path, and
optional data, which can then be used to create the EFI load option.

Also, in `makeRunnableSystem`, the bootloader in ubuntu-seed must have
`NoSlashBoot` in order to correctly find the grub.cfg file and thus the
grub bootloader. This commit fixes this bug, and refactors a bit to
account for the changes in responsibilities between the bootloader and
the setefibootvars.go code.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

bootloader: fixed grub EFI load option test with tmp rootdir

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

go.mod: move golang.org/x/text import next to other golang.org/x/ imports

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: adjust opts to look for recovery bootloader when setting EFI variables

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: do not overwrite BootOrder if unchanged, and unexport EFI variable helper functions

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: unexport `setEfiBootOrderVariable`

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: move code to detect bootloader and set EFI variables accordingly into dedicated function

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: unexport `setUbuntuSeedEfiBootVariables` and accompanying error

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot,bootloader: ensure nil optionalData for EFI variable is equivalent to 0-length slice

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: handle empty boot order and other boot var improvements

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

boot: make setefibootvars functions linux-only

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: add nested spread test for setting EFI vars

The test checks that EFI boot variables exist for the following:
1. A Boot#### variable pointing to the shim file path.
2. A BootOrder variable with the #### from the above Boot#### as first.

Since the layout of EFI assets is dependent on the gadget snap, the test
downloads and unpacks the gadget, then modifies the contents so that one
variant has the shim and grub binaries in `EFI/boot/` and another
variant has the shim and grub binaries in `EFI/ubuntu/` and the fallback
binary in `EFI/boot/`.

After building a core image around that modified gadget, the VM is
booted and the test checks that the EFI variables are set correctly.
Then, the test modifies the gadget to match the other variant's initial
layout, and then installs the newly modified gadget. This should trigger
re-setting EFI boot variables as well.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: fix problems in spread test for setting EFI boot variables

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: disabled TPM on EFI boot vars test and separated gadget script

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: fixed EFI vars test to use correct toolbox and include all EFI assets

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: modify-gadget.sh re-use existing gadget so edition is incremented

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: fix mangled EFI var search string and other improvements

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: polish tests for setting EFI boot variables

Notably, allow tests/nested/core/core20-set-efi-boot-variables to run on
arm64 as well as amd64, simplify setefivars.go to search for multiple
assets on multiple architectures, and allow
tests/nested/manual/core20-set-efi-boot-vars to run on any ubuntu-2*.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* bootloader/grub.go: only consider new shim asset in boot entry for now

* tests/nested/core/core20-set-efi-boot-variables: fix details

* boot: update uefi variables on gadget update

* tests/nested/manual/core20-set-efi-boot-vars: work-around file not deleted

* tests/nested/manual/core20-set-efi-boot-vars: use fb.efi like other tests

* tests/nested/manual/core20-set-efi-boot-vars: drop use of toolbox snap

* tests/nested/manual/core20-set-efi-boot-vars: drop work-around for not deleted files

* tests/nested/manual/core20-set-efi-boot-vars: verify install does add a boot entry

* tests/nested/manual/core20-set-efi-boot-vars: run only on version that have UC

* tests/nested/manual/core20-set-efi-boot-vars: obey GADGET_CHANNEL

* tests/nested/manual/core20-set-efi-boot-vars: move get_boot_entry.py to libs

* tests/nested/manual/core20-set-efi-boot-vars: factorize copy of variables

... so we can reuse the script in other tests

* tests/nested/core/core20-set-efi-boot-variables: stop using toolbox snap

* tests/nested/core/core20-set-efi-boot-variables: only run on versions with UC available

* overlord/devicestate: test using EfiLoadOptionParameters

* boot: test that variables are set

* boot: test observers' UpdateBootEntry

* tests/nested/manual/core20-set-efi-boot-vars: also test without secure boot

* many: use trusted install observer when UEFI variables are supported

* boot/makebootable.go: rename sealer to observer

* boot/grub.go: fix function name in doc

* cmd/snap-bootstrap: verify that ObserveExistingTrustedRecoveryAssets is called

* boot: add tests for SetEfiBootVariables

* many: comment on calls to ObserveExistingTrustedRecoveryAssets

---------

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
Co-authored-by: Oliver Calder <oliver.calder@canonical.com>
2024-06-03 10:03:50 +02:00

737 lines
22 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package bootloader
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/bootloader/assets"
"github.com/snapcore/snapd/bootloader/grubenv"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/strutil"
)
// grub implements the required interfaces
var (
_ Bootloader = (*grub)(nil)
_ RecoveryAwareBootloader = (*grub)(nil)
_ ExtractedRunKernelImageBootloader = (*grub)(nil)
_ TrustedAssetsBootloader = (*grub)(nil)
)
type grub struct {
rootdir string
basedir string
uefiRunKernelExtraction bool
recovery bool
nativePartitionLayout bool
prepareImageTime bool
}
// newGrub create a new Grub bootloader object
func newGrub(rootdir string, opts *Options) Bootloader {
g := &grub{rootdir: rootdir}
if opts != nil {
// Set the flag to extract the run kernel, only
// for UC20 run mode.
// Both UC16/18 and the recovery mode of UC20 load
// the kernel directly from snaps.
g.uefiRunKernelExtraction = opts.Role == RoleRunMode
g.recovery = opts.Role == RoleRecovery
g.nativePartitionLayout = opts.NoSlashBoot || g.recovery
g.prepareImageTime = opts.PrepareImageTime
}
if g.nativePartitionLayout {
g.basedir = "EFI/ubuntu"
} else {
g.basedir = "boot/grub"
}
return g
}
func (g *grub) Name() string {
return "grub"
}
func (g *grub) dir() string {
if g.rootdir == "" {
panic("internal error: unset rootdir")
}
return filepath.Join(g.rootdir, g.basedir)
}
func (g *grub) installManagedRecoveryBootConfig() error {
assetName := g.Name() + "-recovery.cfg"
systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg")
return genericSetBootConfigFromAsset(systemFile, assetName)
}
func (g *grub) installManagedBootConfig() error {
assetName := g.Name() + ".cfg"
systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg")
return genericSetBootConfigFromAsset(systemFile, assetName)
}
func (g *grub) InstallBootConfig(gadgetDir string, opts *Options) error {
if opts != nil && opts.Role == RoleRecovery {
// install managed config for the recovery partition
return g.installManagedRecoveryBootConfig()
}
if opts != nil && opts.Role == RoleRunMode {
// install managed boot config that can handle kernel.efi
return g.installManagedBootConfig()
}
gadgetFile := filepath.Join(gadgetDir, g.Name()+".conf")
systemFile := filepath.Join(g.rootdir, "/boot/grub/grub.cfg")
return genericInstallBootConfig(gadgetFile, systemFile)
}
func (g *grub) SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error {
if recoverySystemDir == "" {
return fmt.Errorf("internal error: recoverySystemDir unset")
}
recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv")
if err := os.MkdirAll(filepath.Dir(recoverySystemGrubEnv), 0755); err != nil {
return err
}
genv := grubenv.NewEnv(recoverySystemGrubEnv)
for k, v := range values {
genv.Set(k, v)
}
return genv.Save()
}
func (g *grub) GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) {
if recoverySystemDir == "" {
return "", fmt.Errorf("internal error: recoverySystemDir unset")
}
recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv")
genv := grubenv.NewEnv(recoverySystemGrubEnv)
if err := genv.Load(); err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return genv.Get(key), nil
}
func (g *grub) Present() (bool, error) {
return osutil.FileExists(filepath.Join(g.dir(), "grub.cfg")), nil
}
func (g *grub) envFile() string {
return filepath.Join(g.dir(), "grubenv")
}
func (g *grub) GetBootVars(names ...string) (map[string]string, error) {
out := make(map[string]string)
env := grubenv.NewEnv(g.envFile())
if err := env.Load(); err != nil {
return nil, err
}
for _, name := range names {
out[name] = env.Get(name)
}
return out, nil
}
func (g *grub) SetBootVars(values map[string]string) error {
env := grubenv.NewEnv(g.envFile())
if err := env.Load(); err != nil && !os.IsNotExist(err) {
return err
}
for k, v := range values {
env.Set(k, v)
}
return env.Save()
}
func (g *grub) extractedKernelDir(prefix string, s snap.PlaceInfo) string {
return filepath.Join(
prefix,
s.Filename(),
)
}
func (g *grub) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
// default kernel assets are:
// - kernel.img
// - initrd.img
// - dtbs/*
var assets []string
if g.uefiRunKernelExtraction {
assets = []string{"kernel.efi"}
} else {
assets = []string{"kernel.img", "initrd.img", "dtbs/*"}
}
// extraction can be forced through either a special file in the kernel snap
// or through an option in the bootloader
_, err := snapf.ReadFile("meta/force-kernel-extraction")
if g.uefiRunKernelExtraction || err == nil {
return extractKernelAssetsToBootDir(
g.extractedKernelDir(g.dir(), s),
snapf,
assets,
)
}
return nil
}
func (g *grub) RemoveKernelAssets(s snap.PlaceInfo) error {
return removeKernelAssetsFromBootDir(g.dir(), s)
}
// ExtractedRunKernelImageBootloader helper methods
func (g *grub) makeKernelEfiSymlink(s snap.PlaceInfo, name string) error {
// use a relative symlink destination so that it resolves properly, if grub
// is located at /run/mnt/ubuntu-boot or /boot/grub, etc.
target := filepath.Join(
s.Filename(),
"kernel.efi",
)
// the location of the destination symlink as an absolute filepath
source := filepath.Join(g.dir(), name)
// check that the kernel snap has been extracted already so we don't
// inadvertently create a dangling symlink
// expand the relative symlink from g.dir()
if !osutil.FileExists(filepath.Join(g.dir(), target)) {
return fmt.Errorf(
"cannot enable %s at %s: %v",
name,
target,
os.ErrNotExist,
)
}
// the symlink doesn't exist so just create it
return osutil.AtomicSymlink(target, source)
}
// unlinkKernelEfiSymlink will remove the specified symlink if it exists. Note
// that if the symlink is "dangling", it will still remove the symlink without
// returning an error. This is useful for example to disable a try-kernel that
// was incorrectly created.
func (g *grub) unlinkKernelEfiSymlink(name string) error {
symlink := filepath.Join(g.dir(), name)
err := os.Remove(symlink)
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func (g *grub) readKernelSymlink(name string) (snap.PlaceInfo, error) {
// read the symlink from <grub-dir>/<name> to
// <grub-dir>/<snap-file-name>/<name> and parse the
// directory (which is supposed to be the name of the snap) into the snap
link := filepath.Join(g.dir(), name)
// check that the symlink is not dangling before continuing
if !osutil.FileExists(link) {
return nil, fmt.Errorf("cannot read dangling symlink %s", name)
}
targetKernelEfi, err := os.Readlink(link)
if err != nil {
return nil, fmt.Errorf("cannot read %s symlink: %v", link, err)
}
kernelSnapFileName := filepath.Base(filepath.Dir(targetKernelEfi))
sn, err := snap.ParsePlaceInfoFromSnapFileName(kernelSnapFileName)
if err != nil {
return nil, fmt.Errorf(
"cannot parse kernel snap file name from symlink target %q: %v",
kernelSnapFileName,
err,
)
}
return sn, nil
}
// actual ExtractedRunKernelImageBootloader methods
// EnableKernel will install a kernel.efi symlink in the bootloader partition,
// pointing to the referenced kernel snap. EnableKernel() will fail if the
// referenced kernel snap does not exist.
func (g *grub) EnableKernel(s snap.PlaceInfo) error {
// add symlink from ubuntuBootPartition/kernel.efi to
// <ubuntu-boot>/EFI/ubuntu/<snap-name>.snap/kernel.efi
// so that we are consistent between uc16/uc18 and uc20 with where we
// extract kernels
return g.makeKernelEfiSymlink(s, "kernel.efi")
}
// EnableTryKernel will install a try-kernel.efi symlink in the bootloader
// partition, pointing towards the referenced kernel snap. EnableTryKernel()
// will fail if the referenced kernel snap does not exist.
func (g *grub) EnableTryKernel(s snap.PlaceInfo) error {
// add symlink from ubuntuBootPartition/kernel.efi to
// <ubuntu-boot>/EFI/ubuntu/<snap-name>.snap/kernel.efi
// so that we are consistent between uc16/uc18 and uc20 with where we
// extract kernels
return g.makeKernelEfiSymlink(s, "try-kernel.efi")
}
// DisableTryKernel will remove the try-kernel.efi symlink if it exists. Note
// that when performing an update, you should probably first use EnableKernel(),
// then DisableTryKernel() for maximum safety.
func (g *grub) DisableTryKernel() error {
return g.unlinkKernelEfiSymlink("try-kernel.efi")
}
// Kernel will return the kernel snap currently installed in the bootloader
// partition, pointed to by the kernel.efi symlink.
func (g *grub) Kernel() (snap.PlaceInfo, error) {
return g.readKernelSymlink("kernel.efi")
}
// TryKernel will return the kernel snap currently being tried if it exists and
// false if there is not currently a try-kernel.efi symlink. Note if the symlink
// exists but does not point to an existing file an error will be returned.
func (g *grub) TryKernel() (snap.PlaceInfo, error) {
// check that the _symlink_ exists, not that it points to something real
// we check for whether it is a dangling symlink inside readKernelSymlink,
// which returns an error when the symlink is dangling
_, err := os.Lstat(filepath.Join(g.dir(), "try-kernel.efi"))
if err == nil {
p, err := g.readKernelSymlink("try-kernel.efi")
// if we failed to read the symlink, then the try kernel isn't usable,
// so return err because the symlink is there
if err != nil {
return nil, err
}
return p, nil
}
return nil, ErrNoTryKernelRef
}
// UpdateBootConfig updates the grub boot config only if it is already managed
// and has a lower edition.
//
// Implements TrustedAssetsBootloader for the grub bootloader.
func (g *grub) UpdateBootConfig() (bool, error) {
// XXX: do we need to take opts here?
bootScriptName := "grub.cfg"
currentBootConfig := filepath.Join(g.dir(), "grub.cfg")
if g.recovery {
// use the recovery asset when asked to do so
bootScriptName = "grub-recovery.cfg"
}
return genericUpdateBootConfigFromAssets(currentBootConfig, bootScriptName)
}
// ManagedAssets returns a list relative paths to boot assets inside the root
// directory of the filesystem.
//
// Implements TrustedAssetsBootloader for the grub bootloader.
func (g *grub) ManagedAssets() []string {
return []string{
filepath.Join(g.basedir, "grub.cfg"),
}
}
func (g *grub) commandLineForEdition(edition uint, pieces CommandLineComponents) (string, error) {
if err := pieces.Validate(); err != nil {
return "", err
}
var nonSnapdCmdline string
if pieces.FullArgs == "" {
staticCmdline := g.defaultCommandLineForEdition(edition)
keepDefaultArgs := kcmdline.RemoveMatchingFilter(staticCmdline, pieces.RemoveArgs)
nonSnapdCmdline = strutil.JoinNonEmpty(append(keepDefaultArgs, pieces.ExtraArgs), " ")
} else {
nonSnapdCmdline = pieces.FullArgs
}
args, err := kcmdline.Split(nonSnapdCmdline)
if err != nil {
return "", fmt.Errorf("cannot use badly formatted kernel command line: %v", err)
}
// join all argument with a single space, see
// grub-core/lib/cmdline.c:grub_create_loader_cmdline() for reference,
// arguments are separated by a single space, the space after last is
// replaced with terminating NULL
snapdArgs := make([]string, 0, 2)
if pieces.ModeArg != "" {
snapdArgs = append(snapdArgs, pieces.ModeArg)
}
if pieces.SystemArg != "" {
snapdArgs = append(snapdArgs, pieces.SystemArg)
}
return strings.Join(append(snapdArgs, args...), " "), nil
}
func (g *grub) assetName() string {
if g.recovery {
return "grub-recovery.cfg"
}
return "grub.cfg"
}
func (g *grub) defaultCommandLineForEdition(edition uint) string {
return staticCommandLineForGrubAssetEdition(g.assetName(), edition)
}
func editionFromDiskConfigAssetFallback(bootConfig string) (uint, error) {
edition, err := editionFromDiskConfigAsset(bootConfig)
if err != nil {
if err != errNoEdition {
return 0, err
}
// we were called using the TrustedAssetsBootloader interface
// meaning the caller expects to us to use the managed assets,
// since one on disk is not managed, use the initial edition of
// the internal boot asset which is compatible with grub.cfg
// used before we started writing out the files ourselves
edition = 1
}
return edition, nil
}
// CommandLine returns the kernel command line composed of mode and
// system arguments, followed by either a built-in bootloader specific
// static arguments corresponding to the on-disk boot asset edition, and
// any extra arguments or a separate set of arguments provided in the
// components. The command line may be different when using a recovery
// bootloader.
//
// Implements TrustedAssetsBootloader for the grub bootloader.
func (g *grub) CommandLine(pieces CommandLineComponents) (string, error) {
currentBootConfig := filepath.Join(g.dir(), "grub.cfg")
edition, err := editionFromDiskConfigAssetFallback(currentBootConfig)
if err != nil {
return "", fmt.Errorf("cannot obtain edition number of current boot config: %v", err)
}
return g.commandLineForEdition(edition, pieces)
}
// CandidateCommandLine is similar to CommandLine, but uses the current
// edition of managed built-in boot assets as reference.
//
// Implements TrustedAssetsBootloader for the grub bootloader.
func (g *grub) CandidateCommandLine(pieces CommandLineComponents) (string, error) {
edition, err := editionFromInternalConfigAsset(g.assetName())
if err != nil {
return "", err
}
return g.commandLineForEdition(edition, pieces)
}
// DefaultCommandLine returns the default kernel command-line used by
// the bootloader excluding the recovery mode and system parameters.
func (g *grub) DefaultCommandLine(candidate bool) (string, error) {
var edition uint
// if "candidate", we look for the managed boot assets
// (current snapd) rather than the ones currently installed on
// the boot/seed disk. This is needed to know the default
// command line before candidate boot assets are installed
if candidate {
var err error
edition, err = editionFromInternalConfigAsset(g.assetName())
if err != nil {
return "", err
}
} else {
currentBootConfig := filepath.Join(g.dir(), "grub.cfg")
var err error
edition, err = editionFromDiskConfigAssetFallback(currentBootConfig)
if err != nil {
return "", fmt.Errorf("cannot obtain edition number of current boot config: %v", err)
}
}
return g.defaultCommandLineForEdition(edition), nil
}
// staticCommandLineForGrubAssetEdition fetches a static command line for given
// grub asset edition
func staticCommandLineForGrubAssetEdition(asset string, edition uint) string {
cmdline := assets.SnippetForEdition(fmt.Sprintf("%s:static-cmdline", asset), edition)
if cmdline == nil {
return ""
}
return string(cmdline)
}
type taggedPath struct {
tag string
path string
}
func (t taggedPath) Id() string {
basename := filepath.Base(t.path)
if t.tag == "" {
return basename
}
return fmt.Sprintf("%s:%s", t.tag, basename)
}
// grubBootAssetPath contains the paths for assets in the boot chain.
type grubBootAssetPath struct {
defaultShimBinary taggedPath
defaultGrubBinary taggedPath
fallbackBinary taggedPath
shimBinary taggedPath
grubBinary taggedPath
}
// grubBootAssetsForArch contains the paths for assets for different
// architectures in a map.
// For backward compliance, we do not have tags
// for asset paths that used to exist before usage of tags.
var grubBootAssetsForArch = map[string]grubBootAssetPath{
"amd64": {
defaultShimBinary: taggedPath{
path: filepath.Join("EFI/boot/", "bootx64.efi"),
},
defaultGrubBinary: taggedPath{
path: filepath.Join("EFI/boot/", "grubx64.efi"),
},
fallbackBinary: taggedPath{
tag: "boot",
path: filepath.Join("EFI/boot/", "fbx64.efi"),
},
shimBinary: taggedPath{
tag: "ubuntu",
path: filepath.Join("EFI/ubuntu/", "shimx64.efi"),
},
grubBinary: taggedPath{
tag: "ubuntu",
path: filepath.Join("EFI/ubuntu/", "grubx64.efi"),
},
},
"arm64": {
defaultShimBinary: taggedPath{
path: filepath.Join("EFI/boot/", "bootaa64.efi"),
},
defaultGrubBinary: taggedPath{
path: filepath.Join("EFI/boot/", "grubaa64.efi"),
},
fallbackBinary: taggedPath{
tag: "boot",
path: filepath.Join("EFI/boot/", "fbaa64.efi"),
},
shimBinary: taggedPath{
tag: "ubuntu",
path: filepath.Join("EFI/ubuntu/", "shimaa64.efi"),
},
grubBinary: taggedPath{
tag: "ubuntu",
path: filepath.Join("EFI/ubuntu/", "grubaa64.efi"),
},
},
}
func (g *grub) getGrubBootAssetsForArch() (*grubBootAssetPath, error) {
if g.prepareImageTime {
return nil, fmt.Errorf("internal error: retrieving boot assets at prepare image time")
}
archi := arch.DpkgArchitecture()
assets, ok := grubBootAssetsForArch[archi]
if !ok {
return nil, fmt.Errorf("cannot find grub assets for %q", archi)
}
return &assets, nil
}
// getGrubRecoveryModeTrustedAssets returns the list of ordered asset
// chain for recovery mode, which are shim and grub from the seed
// partition.
func (g *grub) getGrubRecoveryModeTrustedAssets() ([][]taggedPath, error) {
assets, err := g.getGrubBootAssetsForArch()
if err != nil {
return nil, err
}
return [][]taggedPath{{assets.shimBinary, assets.grubBinary}, {assets.defaultShimBinary, assets.defaultGrubBinary}}, nil
}
// getGrubRunModeTrustedAssets returns the list of ordered asset
// chains for run mode, which is grub from the boot partition.
func (g *grub) getGrubRunModeTrustedAssets() ([][]taggedPath, error) {
assets, err := g.getGrubBootAssetsForArch()
if err != nil {
return nil, err
}
return [][]taggedPath{{assets.defaultGrubBinary}}, nil
}
// TrustedAssets returns the map of relative paths to asset
// identifers. The relative paths are relative to the bootloader's
// rootdir. The asset identifiers correspond to the backward
// compatible names recorded in the modeenv (CurrentTrustedBootAssets
// and CurrentTrustedRecoveryBootAssets).
func (g *grub) TrustedAssets() (map[string]string, error) {
if !g.nativePartitionLayout {
return nil, fmt.Errorf("internal error: trusted assets called without native host-partition layout")
}
ret := make(map[string]string)
var chains [][]taggedPath
var err error
if g.recovery {
chains, err = g.getGrubRecoveryModeTrustedAssets()
} else {
chains, err = g.getGrubRunModeTrustedAssets()
}
if err != nil {
return nil, err
}
for _, chain := range chains {
for _, asset := range chain {
ret[asset.path] = asset.Id()
}
}
return ret, nil
}
// RecoveryBootChains returns the list of load chains for recovery modes.
// It should be called on a RoleRecovery bootloader.
func (g *grub) RecoveryBootChains(kernelPath string) ([][]BootFile, error) {
if !g.recovery {
return nil, fmt.Errorf("not a recovery bootloader")
}
// add trusted assets to the recovery chain
assetsSet, err := g.getGrubRecoveryModeTrustedAssets()
if err != nil {
return nil, err
}
chains := make([][]BootFile, 0, len(assetsSet))
for _, assets := range assetsSet {
chain := make([]BootFile, 0, len(assets)+1)
for _, ta := range assets {
chain = append(chain, NewBootFile("", ta.path, RoleRecovery))
}
// add recovery kernel to the recovery chain
chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRecovery))
chains = append(chains, chain)
}
return chains, nil
}
// BootChains returns the list of load chains for run mode.
// It should be called on a RoleRecovery bootloader passing the
// RoleRunMode bootloader.
func (g *grub) BootChains(runBl Bootloader, kernelPath string) ([][]BootFile, error) {
if !g.recovery {
return nil, fmt.Errorf("not a recovery bootloader")
}
if runBl.Name() != "grub" {
return nil, fmt.Errorf("run mode bootloader must be grub")
}
// add trusted assets to the recovery chain
recoveryModeAssetsSet, err := g.getGrubRecoveryModeTrustedAssets()
if err != nil {
return nil, err
}
runModeAssetsSet, err := g.getGrubRunModeTrustedAssets()
if err != nil {
return nil, err
}
chains := make([][]BootFile, 0, len(recoveryModeAssetsSet)*len(runModeAssetsSet))
for _, recoveryModeAssets := range recoveryModeAssetsSet {
for _, runModeAssets := range runModeAssetsSet {
chain := make([]BootFile, 0, len(recoveryModeAssets)+len(runModeAssets)+1)
for _, ta := range recoveryModeAssets {
chain = append(chain, NewBootFile("", ta.path, RoleRecovery))
}
for _, ta := range runModeAssets {
chain = append(chain, NewBootFile("", ta.path, RoleRunMode))
}
// add kernel to the boot chain
chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRunMode))
chains = append(chains, chain)
}
}
return chains, nil
}
// ParametersForEfiLoadOption returns a serialized load option for the
// shim binary. It should be called on a UefiBootloader.
// updatedAssets is a list of assets that were installed/updated. This
// only expects trusted assets.
func (g *grub) ParametersForEfiLoadOption(updatedAssets []string) (description string, assetPath string, optionalData []byte, err error) {
if !g.recovery {
return "", "", nil, fmt.Errorf("internal error: run grub does not provide a boot entry")
}
knownAssets, err := g.getGrubBootAssetsForArch()
if err != nil {
return "", "", nil, err
}
foundFallbackShim := false
foundShim := false
// XXX: it would be nice to also check for fb.efi. However it
// is not part of a trusted boot chain, so we will not appear
// in updatedAssets
// Let's look for the shim binary
for _, updated := range updatedAssets {
if updated == knownAssets.shimBinary.Id() {
foundShim = true
}
if updated == knownAssets.defaultShimBinary.Id() {
foundFallbackShim = true
}
}
if foundShim {
assetPath = filepath.Join(g.rootdir, knownAssets.shimBinary.path)
} else if foundFallbackShim {
assetPath = filepath.Join(g.rootdir, knownAssets.defaultShimBinary.path)
} else {
return "", "", nil, ErrNoBootChainFound
}
description = "ubuntu"
optionalData = nil
return description, assetPath, optionalData, nil
}