mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* 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>
667 lines
23 KiB
Go
667 lines
23 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 boot
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
"github.com/snapcore/snapd/bootloader"
|
|
"github.com/snapcore/snapd/dirs"
|
|
"github.com/snapcore/snapd/gadget"
|
|
"github.com/snapcore/snapd/logger"
|
|
"github.com/snapcore/snapd/osutil"
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/snap/snapfile"
|
|
"github.com/snapcore/snapd/strutil"
|
|
)
|
|
|
|
var sealKeyToModeenv = sealKeyToModeenvImpl
|
|
|
|
// BootableSet represents the boot snaps of a system to be made bootable.
|
|
type BootableSet struct {
|
|
Base *snap.Info
|
|
BasePath string
|
|
Kernel *snap.Info
|
|
KernelPath string
|
|
Gadget *snap.Info
|
|
GadgetPath string
|
|
|
|
RecoverySystemLabel string
|
|
// RecoverySystemDir is a path to a directory with recovery system
|
|
// assets. The path is relative to the recovery bootloader root
|
|
// directory.
|
|
RecoverySystemDir string
|
|
|
|
UnpackedGadgetDir string
|
|
|
|
// Recovery is set when making the recovery partition bootable.
|
|
Recovery bool
|
|
}
|
|
|
|
// MakeBootableImage sets up the given bootable set and target filesystem
|
|
// such that the image can be booted.
|
|
//
|
|
// rootdir points to an image filesystem (UC 16/18) or an image recovery
|
|
// filesystem (UC20 at prepare-image time).
|
|
// On UC20, bootWith.Recovery must be true, as this function makes the recovery
|
|
// system bootable. It does not make a run system bootable, for that
|
|
// functionality see MakeRunnableSystem, which is meant to be used at runtime
|
|
// from UC20 install mode.
|
|
// For a UC20 image a set of boot flags that will be set in the recovery
|
|
// boot environment can be specified.
|
|
func MakeBootableImage(model *asserts.Model, rootdir string, bootWith *BootableSet, bootFlags []string) error {
|
|
if model.Grade() == asserts.ModelGradeUnset {
|
|
if len(bootFlags) != 0 {
|
|
return fmt.Errorf("no boot flags support for UC16/18")
|
|
}
|
|
return makeBootable16(model, rootdir, bootWith)
|
|
}
|
|
|
|
if !bootWith.Recovery {
|
|
return fmt.Errorf("internal error: MakeBootableImage called at runtime, use MakeRunnableSystem instead")
|
|
}
|
|
return makeBootable20(model, rootdir, bootWith, bootFlags)
|
|
}
|
|
|
|
// MakeBootablePartition configures a partition mounted on rootdir
|
|
// using information from bootWith and bootFlags. Contrarily to
|
|
// MakeBootableImage this happens in a live system.
|
|
func MakeBootablePartition(partDir string, opts *bootloader.Options, bootWith *BootableSet, bootMode string, bootFlags []string) error {
|
|
if bootWith.RecoverySystemDir != "" {
|
|
return fmt.Errorf("internal error: RecoverySystemDir unexpectedly set for MakeBootablePartition")
|
|
}
|
|
return configureBootloader(partDir, opts, bootWith, bootMode, bootFlags)
|
|
}
|
|
|
|
// makeBootable16 setups the image filesystem for boot with UC16
|
|
// and UC18 models. This entails:
|
|
// - installing the bootloader configuration from the gadget
|
|
// - creating symlinks for boot snaps from seed to the runtime blob dir
|
|
// - setting boot env vars pointing to the revisions of the boot snaps to use
|
|
// - extracting kernel assets as needed by the bootloader
|
|
func makeBootable16(model *asserts.Model, rootdir string, bootWith *BootableSet) error {
|
|
opts := &bootloader.Options{
|
|
PrepareImageTime: true,
|
|
}
|
|
|
|
// install the bootloader configuration from the gadget
|
|
if err := bootloader.InstallBootConfig(bootWith.UnpackedGadgetDir, rootdir, opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
// setup symlinks for kernel and boot base from the blob directory
|
|
// to the seed snaps
|
|
|
|
snapBlobDir := dirs.SnapBlobDirUnder(rootdir)
|
|
if err := os.MkdirAll(snapBlobDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, fn := range []string{bootWith.BasePath, bootWith.KernelPath} {
|
|
dst := filepath.Join(snapBlobDir, filepath.Base(fn))
|
|
// construct a relative symlink from the blob dir
|
|
// to the seed snap file
|
|
relSymlink, err := filepath.Rel(snapBlobDir, fn)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot build symlink for boot snap: %v", err)
|
|
}
|
|
if err := os.Symlink(relSymlink, dst); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Set bootvars for kernel/core snaps so the system boots and
|
|
// does the first-time initialization. There is also no
|
|
// mounted kernel/core/base snap, but just the blobs.
|
|
bl, err := bootloader.Find(rootdir, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot set kernel/core boot variables: %s", err)
|
|
}
|
|
|
|
m := map[string]string{
|
|
"snap_mode": "",
|
|
"snap_try_core": "",
|
|
"snap_try_kernel": "",
|
|
}
|
|
if model.DisplayName() != "" {
|
|
m["snap_menuentry"] = model.DisplayName()
|
|
}
|
|
|
|
setBoot := func(name, fn string) {
|
|
m[name] = filepath.Base(fn)
|
|
}
|
|
// base
|
|
setBoot("snap_core", bootWith.BasePath)
|
|
|
|
// kernel
|
|
kernelf, err := snapfile.Open(bootWith.KernelPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := bl.ExtractKernelAssets(bootWith.Kernel, kernelf); err != nil {
|
|
return err
|
|
}
|
|
setBoot("snap_kernel", bootWith.KernelPath)
|
|
|
|
if err := bl.SetBootVars(m); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configureBootloader(rootdir string, opts *bootloader.Options, bootWith *BootableSet, bootMode string, bootFlags []string) error {
|
|
blVars := make(map[string]string, 3)
|
|
if len(bootFlags) != 0 {
|
|
if err := setImageBootFlags(bootFlags, blVars); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// install the bootloader configuration from the gadget
|
|
if err := bootloader.InstallBootConfig(bootWith.UnpackedGadgetDir, rootdir, opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
// now install the recovery system specific boot config
|
|
bl, err := bootloader.Find(rootdir, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("internal error: cannot find bootloader: %v", err)
|
|
}
|
|
|
|
blVars["snapd_recovery_mode"] = bootMode
|
|
if bootWith.RecoverySystemLabel != "" {
|
|
// record which recovery system is to be used on the bootloader, note
|
|
// that this goes on the main bootloader environment, and not on the
|
|
// recovery system bootloader environment, for example for grub
|
|
// bootloader, this env var is set on the ubuntu-seed root grubenv, and
|
|
// not on the recovery system grubenv in the systems/20200314/ subdir on
|
|
// ubuntu-seed
|
|
blVars["snapd_recovery_system"] = bootWith.RecoverySystemLabel
|
|
}
|
|
|
|
if err := bl.SetBootVars(blVars); err != nil {
|
|
return fmt.Errorf("cannot set recovery environment: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet, bootFlags []string) error {
|
|
// we can only make a single recovery system bootable right now
|
|
recoverySystems, err := filepath.Glob(filepath.Join(rootdir, "systems/*"))
|
|
if err != nil {
|
|
return fmt.Errorf("cannot validate recovery systems: %v", err)
|
|
}
|
|
if len(recoverySystems) > 1 {
|
|
return fmt.Errorf("cannot make multiple recovery systems bootable yet")
|
|
}
|
|
|
|
if bootWith.RecoverySystemLabel == "" {
|
|
return fmt.Errorf("internal error: recovery system label unset")
|
|
}
|
|
|
|
opts := &bootloader.Options{
|
|
PrepareImageTime: true,
|
|
// setup the recovery bootloader
|
|
Role: bootloader.RoleRecovery,
|
|
}
|
|
if err := configureBootloader(rootdir, opts, bootWith, ModeInstall, bootFlags); err != nil {
|
|
return fmt.Errorf("cannot install bootloader: %v", err)
|
|
}
|
|
|
|
return MakeRecoverySystemBootable(model, rootdir, bootWith.RecoverySystemDir, &RecoverySystemBootableSet{
|
|
Kernel: bootWith.Kernel,
|
|
KernelPath: bootWith.KernelPath,
|
|
GadgetSnapOrDir: bootWith.UnpackedGadgetDir,
|
|
PrepareImageTime: true,
|
|
})
|
|
}
|
|
|
|
// RecoverySystemBootableSet is a set of snaps relevant to booting a recovery
|
|
// system.
|
|
type RecoverySystemBootableSet struct {
|
|
Kernel *snap.Info
|
|
KernelPath string
|
|
GadgetSnapOrDir string
|
|
// PrepareImageTime is true when the structure is being used when
|
|
// preparing a bootable system image.
|
|
PrepareImageTime bool
|
|
}
|
|
|
|
// MakeRecoverySystemBootable prepares a recovery system under a path relative
|
|
// to recovery bootloader's rootdir for booting.
|
|
func MakeRecoverySystemBootable(model *asserts.Model, rootdir string, relativeRecoverySystemDir string, bootWith *RecoverySystemBootableSet) error {
|
|
opts := &bootloader.Options{
|
|
// XXX: this is only needed by LK, it is unclear whether LK does
|
|
// too much when extracting recovery kernel assets, in the end
|
|
// it is currently not possible to create a recovery system at
|
|
// runtime when using LK.
|
|
PrepareImageTime: bootWith.PrepareImageTime,
|
|
// setup the recovery bootloader
|
|
Role: bootloader.RoleRecovery,
|
|
}
|
|
|
|
bl, err := bootloader.Find(rootdir, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("internal error: cannot find bootloader: %v", err)
|
|
}
|
|
|
|
// on e.g. ARM we need to extract the kernel assets on the recovery
|
|
// system as well, but the bootloader does not load any environment from
|
|
// the recovery system
|
|
erkbl, ok := bl.(bootloader.ExtractedRecoveryKernelImageBootloader)
|
|
if ok {
|
|
kernelf, err := snapfile.Open(bootWith.KernelPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = erkbl.ExtractRecoveryKernelAssets(
|
|
relativeRecoverySystemDir,
|
|
bootWith.Kernel,
|
|
kernelf,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot extract recovery system kernel assets: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
rbl, ok := bl.(bootloader.RecoveryAwareBootloader)
|
|
if !ok {
|
|
return fmt.Errorf("cannot use %s bootloader: does not support recovery systems", bl.Name())
|
|
}
|
|
kernelPath, err := filepath.Rel(rootdir, bootWith.KernelPath)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot construct kernel boot path: %v", err)
|
|
}
|
|
recoveryBlVars := map[string]string{
|
|
"snapd_recovery_kernel": filepath.Join("/", kernelPath),
|
|
}
|
|
if tbl, ok := bl.(bootloader.TrustedAssetsBootloader); ok {
|
|
// Look at gadget default values for system.kernel.*cmdline-append options
|
|
cmdlineAppend, err := buildOptionalKernelCommandLine(model, bootWith.GadgetSnapOrDir)
|
|
if err != nil {
|
|
return fmt.Errorf("while retrieving system.kernel.*cmdline-append defaults: %v", err)
|
|
}
|
|
candidate := false
|
|
defaultCmdLine, err := tbl.DefaultCommandLine(candidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// to set cmdlineAppend.
|
|
recoveryCmdlineArgs, err := bootVarsForTrustedCommandLineFromGadget(bootWith.GadgetSnapOrDir, cmdlineAppend, defaultCmdLine, model)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot obtain recovery system command line: %v", err)
|
|
}
|
|
for k, v := range recoveryCmdlineArgs {
|
|
recoveryBlVars[k] = v
|
|
}
|
|
}
|
|
|
|
if err := rbl.SetRecoverySystemEnv(relativeRecoverySystemDir, recoveryBlVars); err != nil {
|
|
return fmt.Errorf("cannot set recovery system environment: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type makeRunnableOptions struct {
|
|
Standalone bool
|
|
AfterDataReset bool
|
|
SeedDir string
|
|
StateUnlocker Unlocker
|
|
}
|
|
|
|
func copyBootSnap(orig string, dstInfo *snap.Info, dstSnapBlobDir string) error {
|
|
// if the source path is a symlink, don't copy the symlink, copy the
|
|
// target file instead of copying the symlink, as the initramfs won't
|
|
// follow the symlink when it goes to mount the base and kernel snaps by
|
|
// design as the initramfs should only be using trusted things from
|
|
// ubuntu-data to boot in run mode
|
|
if osutil.IsSymlink(orig) {
|
|
link, err := os.Readlink(orig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
orig = link
|
|
}
|
|
// note that we need to use the "Filename()" here because unasserted
|
|
// snaps will have names like pc-kernel_5.19.4.snap but snapd expects
|
|
// "pc-kernel_x1.snap"
|
|
dst := filepath.Join(dstSnapBlobDir, dstInfo.Filename())
|
|
if err := osutil.CopyFile(orig, dst, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeRunnableSystem(model *asserts.Model, bootWith *BootableSet, observer TrustedAssetsInstallObserver, makeOpts makeRunnableOptions) error {
|
|
if model.Grade() == asserts.ModelGradeUnset {
|
|
return fmt.Errorf("internal error: cannot make pre-UC20 system runnable")
|
|
}
|
|
if bootWith.RecoverySystemDir != "" {
|
|
return fmt.Errorf("internal error: RecoverySystemDir unexpectedly set for MakeRunnableSystem")
|
|
}
|
|
modeenvLock()
|
|
defer modeenvUnlock()
|
|
|
|
// TODO:UC20:
|
|
// - figure out what to do for uboot gadgets, currently we require them to
|
|
// install the boot.sel onto ubuntu-boot directly, but the file should be
|
|
// managed by snapd instead
|
|
|
|
// copy kernel/base/gadget into the ubuntu-data partition
|
|
snapBlobDir := dirs.SnapBlobDirUnder(InstallHostWritableDir(model))
|
|
if err := os.MkdirAll(snapBlobDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
for _, origDest := range []struct {
|
|
orig string
|
|
destInfo *snap.Info
|
|
}{
|
|
{orig: bootWith.BasePath, destInfo: bootWith.Base},
|
|
{orig: bootWith.KernelPath, destInfo: bootWith.Kernel},
|
|
{orig: bootWith.GadgetPath, destInfo: bootWith.Gadget}} {
|
|
if err := copyBootSnap(origDest.orig, origDest.destInfo, snapBlobDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// replicate the boot assets cache in host's writable
|
|
if err := CopyBootAssetsCacheToRoot(InstallHostWritableDir(model)); err != nil {
|
|
return fmt.Errorf("cannot replicate boot assets cache: %v", err)
|
|
}
|
|
|
|
var currentTrustedBootAssets bootAssetsMap
|
|
var currentTrustedRecoveryBootAssets bootAssetsMap
|
|
var observerImpl *trustedAssetsInstallObserverImpl
|
|
if observer != nil {
|
|
impl, ok := observer.(*trustedAssetsInstallObserverImpl)
|
|
if !ok {
|
|
return fmt.Errorf("internal error: expected a trustedAssetsInstallObserverImpl")
|
|
}
|
|
observerImpl = impl
|
|
currentTrustedBootAssets = observerImpl.currentTrustedBootAssetsMap()
|
|
currentTrustedRecoveryBootAssets = observerImpl.currentTrustedRecoveryBootAssetsMap()
|
|
}
|
|
recoverySystemLabel := bootWith.RecoverySystemLabel
|
|
// write modeenv on the ubuntu-data partition
|
|
modeenv := &Modeenv{
|
|
Mode: "run",
|
|
RecoverySystem: recoverySystemLabel,
|
|
// default to the system we were installed from
|
|
CurrentRecoverySystems: []string{recoverySystemLabel},
|
|
// which is also considered to be good
|
|
GoodRecoverySystems: []string{recoverySystemLabel},
|
|
CurrentTrustedBootAssets: currentTrustedBootAssets,
|
|
CurrentTrustedRecoveryBootAssets: currentTrustedRecoveryBootAssets,
|
|
// kernel command lines are set later once a boot config is
|
|
// installed
|
|
CurrentKernelCommandLines: nil,
|
|
// keep this comment to make gofmt 1.9 happy
|
|
Gadget: bootWith.Gadget.Filename(),
|
|
CurrentKernels: []string{bootWith.Kernel.Filename()},
|
|
BrandID: model.BrandID(),
|
|
Model: model.Model(),
|
|
// TODO: test this
|
|
Classic: model.Classic(),
|
|
Grade: string(model.Grade()),
|
|
ModelSignKeyID: model.SignKeyID(),
|
|
}
|
|
// Note on classic systems there is no boot base, the system boots
|
|
// from debs.
|
|
if !model.Classic() {
|
|
modeenv.Base = bootWith.Base.Filename()
|
|
}
|
|
|
|
// get the ubuntu-boot bootloader and extract the kernel there
|
|
opts := &bootloader.Options{
|
|
// Bootloader for run mode
|
|
Role: bootloader.RoleRunMode,
|
|
// At this point the run mode bootloader is under the native
|
|
// run partition layout, no /boot mount.
|
|
NoSlashBoot: true,
|
|
}
|
|
// the bootloader config may have been installed when the ubuntu-boot
|
|
// partition was created, but for a trusted assets the bootloader config
|
|
// will be installed further down; for now identify the run mode
|
|
// bootloader by looking at the gadget
|
|
bl, err := bootloader.ForGadget(bootWith.UnpackedGadgetDir, InitramfsUbuntuBootDir, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("internal error: cannot identify run system bootloader: %v", err)
|
|
}
|
|
|
|
// extract the kernel first and mark kernel_status ready
|
|
kernelf, err := snapfile.Open(bootWith.KernelPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = bl.ExtractKernelAssets(bootWith.Kernel, kernelf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
blVars := map[string]string{
|
|
"kernel_status": "",
|
|
}
|
|
|
|
ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader)
|
|
if ok {
|
|
// the bootloader supports additional extracted kernel handling
|
|
|
|
// enable the kernel on the bootloader and finally transition to
|
|
// run-mode last in case we get rebooted in between anywhere here
|
|
|
|
// it's okay to enable the kernel before writing the boot vars, because
|
|
// we haven't written snapd_recovery_mode=run, which is the critical
|
|
// thing that will inform the bootloader to try booting from ubuntu-boot
|
|
if err := ebl.EnableKernel(bootWith.Kernel); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// the bootloader does not support additional handling of
|
|
// extracted kernel images, we must name the kernel to be used
|
|
// explicitly in bootloader variables
|
|
blVars["snap_kernel"] = bootWith.Kernel.Filename()
|
|
}
|
|
|
|
// set the ubuntu-boot bootloader variables before triggering transition to
|
|
// try and boot from ubuntu-boot (that transition happens when we write
|
|
// snapd_recovery_mode below)
|
|
if err := bl.SetBootVars(blVars); err != nil {
|
|
return fmt.Errorf("cannot set run system environment: %v", err)
|
|
}
|
|
|
|
tbl, ok := bl.(bootloader.TrustedAssetsBootloader)
|
|
if ok {
|
|
// the bootloader can manage its boot config
|
|
|
|
// installing boot config must be performed after the boot
|
|
// partition has been populated with gadget data
|
|
if err := bl.InstallBootConfig(bootWith.UnpackedGadgetDir, opts); err != nil {
|
|
return fmt.Errorf("cannot install managed bootloader assets: %v", err)
|
|
}
|
|
// determine the expected command line
|
|
cmdline, err := ComposeCandidateCommandLine(model, bootWith.UnpackedGadgetDir)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot compose the candidate command line: %v", err)
|
|
}
|
|
modeenv.CurrentKernelCommandLines = bootCommandLines{cmdline}
|
|
|
|
// Look at gadget default values for system.kernel.*cmdline-append options
|
|
cmdlineAppend, err := buildOptionalKernelCommandLine(model, bootWith.UnpackedGadgetDir)
|
|
if err != nil {
|
|
return fmt.Errorf("while retrieving system.kernel.*cmdline-append defaults: %v", err)
|
|
}
|
|
|
|
candidate := false
|
|
defaultCmdLine, err := tbl.DefaultCommandLine(candidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmdlineVars, err := bootVarsForTrustedCommandLineFromGadget(bootWith.UnpackedGadgetDir, cmdlineAppend, defaultCmdLine, model)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot prepare bootloader variables for kernel command line: %v", err)
|
|
}
|
|
if err := bl.SetBootVars(cmdlineVars); err != nil {
|
|
return fmt.Errorf("cannot set run system kernel command line arguments: %v", err)
|
|
}
|
|
}
|
|
|
|
// all fields that needed to be set in the modeenv must have been set by
|
|
// now, write modeenv to disk
|
|
if err := modeenv.WriteTo(InstallHostWritableDir(model)); err != nil {
|
|
return fmt.Errorf("cannot write modeenv: %v", err)
|
|
}
|
|
|
|
if observer != nil && observerImpl.useEncryption {
|
|
hasHook, err := HasFDESetupHook(bootWith.Kernel)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot check for fde-setup hook: %v", err)
|
|
}
|
|
|
|
flags := sealKeyToModeenvFlags{
|
|
HasFDESetupHook: hasHook,
|
|
FactoryReset: makeOpts.AfterDataReset,
|
|
SeedDir: makeOpts.SeedDir,
|
|
StateUnlocker: makeOpts.StateUnlocker,
|
|
}
|
|
if makeOpts.Standalone {
|
|
flags.SnapsDir = snapBlobDir
|
|
}
|
|
// seal the encryption key to the parameters specified in modeenv
|
|
if err := sealKeyToModeenv(observerImpl.dataEncryptionKey, observerImpl.saveEncryptionKey, model, modeenv, flags); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// so far so good, we managed to install the system, so it can be used
|
|
// for recovery as well
|
|
if err := MarkRecoveryCapableSystem(recoverySystemLabel); err != nil {
|
|
return fmt.Errorf("cannot record %q as a recovery capable system: %v", recoverySystemLabel, err)
|
|
}
|
|
|
|
if observer != nil {
|
|
if err := observer.UpdateBootEntry(); err != nil {
|
|
logger.Debugf("WARNING: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func buildOptionalKernelCommandLine(model *asserts.Model, gadgetSnapOrDir string) (string, error) {
|
|
sf, err := snapfile.Open(gadgetSnapOrDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot open gadget snap: %v", err)
|
|
}
|
|
gadgetInfo, err := gadget.ReadInfoFromSnapFile(sf, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot read gadget data: %v", err)
|
|
}
|
|
|
|
defaults := gadget.SystemDefaults(gadgetInfo.Defaults)
|
|
|
|
var cmdlineAppend, cmdlineAppendDangerous string
|
|
|
|
if cmdlineAppendIf, ok := defaults["system.kernel.cmdline-append"]; ok {
|
|
cmdlineAppend, ok = cmdlineAppendIf.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("system.kernel.cmdline-append is not a string")
|
|
}
|
|
}
|
|
|
|
if cmdlineAppendIf, ok := defaults["system.kernel.dangerous-cmdline-append"]; ok {
|
|
cmdlineAppendDangerous, ok = cmdlineAppendIf.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("system.kernel.dangerous-cmdline-append is not a string")
|
|
}
|
|
if model.Grade() != asserts.ModelDangerous {
|
|
// Print a warning and ignore
|
|
logger.Noticef("WARNING: system.kernel.dangerous-cmdline-append ignored by non-dangerous models")
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
if cmdlineAppend != "" {
|
|
// TODO perform validation against what is allowed by the gadget
|
|
}
|
|
|
|
cmdlineAppend = strutil.JoinNonEmpty([]string{cmdlineAppend, cmdlineAppendDangerous}, " ")
|
|
|
|
return cmdlineAppend, nil
|
|
}
|
|
|
|
// MakeRunnableSystem is like MakeBootableImage in that it sets up a system to
|
|
// be able to boot, but is unique in that it is intended to be called from UC20
|
|
// install mode and makes the run system bootable (hence it is called
|
|
// "runnable").
|
|
// Note that this function does not update the recovery bootloader env to
|
|
// actually transition to run mode here, that is left to the caller via
|
|
// something like boot.EnsureNextBootToRunMode(). This is to enable separately
|
|
// setting up a run system and actually transitioning to it, with hooks, etc.
|
|
// running in between.
|
|
func MakeRunnableSystem(model *asserts.Model, bootWith *BootableSet, observer TrustedAssetsInstallObserver) error {
|
|
return makeRunnableSystem(model, bootWith, observer, makeRunnableOptions{
|
|
SeedDir: dirs.SnapSeedDir,
|
|
})
|
|
}
|
|
|
|
// MakeRunnableStandaloneSystem operates like MakeRunnableSystem but does
|
|
// not assume that the run system being set up is related to the current
|
|
// system. This is appropriate e.g when installing from a classic installer.
|
|
func MakeRunnableStandaloneSystem(model *asserts.Model, bootWith *BootableSet, observer TrustedAssetsInstallObserver, unlocker Unlocker) error {
|
|
// TODO consider merging this back into MakeRunnableSystem but need
|
|
// to consider the properties of the different input used for sealing
|
|
return makeRunnableSystem(model, bootWith, observer, makeRunnableOptions{
|
|
Standalone: true,
|
|
SeedDir: dirs.SnapSeedDir,
|
|
StateUnlocker: unlocker,
|
|
})
|
|
}
|
|
|
|
// MakeRunnableStandaloneSystemFromInitrd is the same as MakeRunnableStandaloneSystem
|
|
// but uses seed dir path expected in initrd.
|
|
func MakeRunnableStandaloneSystemFromInitrd(model *asserts.Model, bootWith *BootableSet, observer TrustedAssetsInstallObserver) error {
|
|
// TODO consider merging this back into MakeRunnableSystem but need
|
|
// to consider the properties of the different input used for sealing
|
|
return makeRunnableSystem(model, bootWith, observer, makeRunnableOptions{
|
|
Standalone: true,
|
|
SeedDir: filepath.Join(InitramfsRunMntDir, "ubuntu-seed"),
|
|
})
|
|
}
|
|
|
|
// MakeRunnableSystemAfterDataReset sets up the system to be able to boot, but it is
|
|
// intended to be called from UC20 factory reset mode right before switching
|
|
// back to the new run system.
|
|
func MakeRunnableSystemAfterDataReset(model *asserts.Model, bootWith *BootableSet, observer TrustedAssetsInstallObserver) error {
|
|
return makeRunnableSystem(model, bootWith, observer, makeRunnableOptions{
|
|
AfterDataReset: true,
|
|
SeedDir: dirs.SnapSeedDir,
|
|
})
|
|
}
|