Files
snapd/boot/makebootable_test.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

2411 lines
88 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2022 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_test
import (
"fmt"
"os"
"path/filepath"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/arch/archtest"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/bootloader/assets"
"github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/bootloader/grubenv"
"github.com/snapcore/snapd/bootloader/ubootenv"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/secboot/keys"
"github.com/snapcore/snapd/seed"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapfile"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/testutil"
"github.com/snapcore/snapd/timings"
)
type makeBootableSuite struct {
baseBootenvSuite
bootloader *bootloadertest.MockBootloader
}
var _ = Suite(&makeBootableSuite{})
func (s *makeBootableSuite) SetUpTest(c *C) {
s.baseBootenvSuite.SetUpTest(c)
s.bootloader = bootloadertest.Mock("mock", c.MkDir())
s.forceBootloader(s.bootloader)
s.AddCleanup(archtest.MockArchitecture("amd64"))
snippets := []assets.ForEditions{
{FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")},
}
s.AddCleanup(assets.MockSnippetsForEdition("grub.cfg:static-cmdline", snippets))
s.AddCleanup(assets.MockSnippetsForEdition("grub-recovery.cfg:static-cmdline", snippets))
}
func makeSnap(c *C, name, yaml string, revno snap.Revision) (fn string, info *snap.Info) {
return makeSnapWithFiles(c, name, yaml, revno, nil)
}
func makeSnapWithFiles(c *C, name, yaml string, revno snap.Revision, files [][]string) (fn string, info *snap.Info) {
si := &snap.SideInfo{
RealName: name,
Revision: revno,
}
fn = snaptest.MakeTestSnapWithFiles(c, yaml, files)
snapf, err := snapfile.Open(fn)
c.Assert(err, IsNil)
info, err = snap.ReadInfoFromSnapFile(snapf, si)
c.Assert(err, IsNil)
return fn, info
}
func (s *makeBootableSuite) TestMakeBootableImage(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockModel()
grubCfg := []byte("#grub cfg")
unpackedGadgetDir := c.MkDir()
err := os.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644)
c.Assert(err, IsNil)
seedSnapsDirs := filepath.Join(s.rootdir, "/var/lib/snapd/seed", "snaps")
err = os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core18", `name: core18
type: base
version: 4.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnap(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 4.0
`, snap.R(5))
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
UnpackedGadgetDir: unpackedGadgetDir,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, IsNil)
// check the bootloader config
seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "boot/grub/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snap_kernel"), Equals, "pc-kernel_5.snap")
c.Check(seedGenv.Get("snap_core"), Equals, "core18_3.snap")
c.Check(seedGenv.Get("snap_menuentry"), Equals, "My Model")
// check symlinks from snap blob dir
kernelBlob := filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), kernelInfo.Filename())
dst, err := os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), kernelInfo.Filename()))
c.Assert(err, IsNil)
c.Check(dst, Equals, "../seed/snaps/pc-kernel_5.snap")
c.Check(kernelBlob, testutil.FilePresent)
baseBlob := filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), baseInfo.Filename())
dst, err = os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), baseInfo.Filename()))
c.Assert(err, IsNil)
c.Check(dst, Equals, "../seed/snaps/core18_3.snap")
c.Check(baseBlob, testutil.FilePresent)
// check that the bootloader (grub here) configuration was copied
c.Check(filepath.Join(s.rootdir, "boot", "grub/grub.cfg"), testutil.FileEquals, grubCfg)
}
type makeBootable20Suite struct {
baseBootenvSuite
bootloader *bootloadertest.MockRecoveryAwareBootloader
}
type makeBootable20UbootSuite struct {
baseBootenvSuite
bootloader *bootloadertest.MockExtractedRecoveryKernelImageBootloader
}
var _ = Suite(&makeBootable20Suite{})
var _ = Suite(&makeBootable20UbootSuite{})
func (s *makeBootable20Suite) SetUpTest(c *C) {
s.baseBootenvSuite.SetUpTest(c)
s.bootloader = bootloadertest.Mock("mock", c.MkDir()).RecoveryAware()
s.forceBootloader(s.bootloader)
s.AddCleanup(archtest.MockArchitecture("amd64"))
snippets := []assets.ForEditions{
{FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")},
}
s.AddCleanup(assets.MockSnippetsForEdition("grub.cfg:static-cmdline", snippets))
s.AddCleanup(assets.MockSnippetsForEdition("grub-recovery.cfg:static-cmdline", snippets))
}
func (s *makeBootable20UbootSuite) SetUpTest(c *C) {
s.baseBootenvSuite.SetUpTest(c)
s.bootloader = bootloadertest.Mock("mock", c.MkDir()).ExtractedRecoveryKernelImage()
s.forceBootloader(s.bootloader)
}
const gadgetYaml = `
volumes:
pc:
bootloader: grub
structure:
- name: ubuntu-seed
role: system-seed
filesystem: vfat
type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
size: 1200M
- name: ubuntu-boot
role: system-boot
filesystem: ext4
type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
size: 750M
- name: ubuntu-data
role: system-data
filesystem: ext4
type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
size: 1G
`
func (s *makeBootable20Suite) TestMakeBootableImage20(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := "#grub-recovery cfg"
grubRecoveryCfgAsset := "#grub-recovery cfg from assets"
grubCfg := "#grub cfg"
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", grubRecoveryCfg},
{"grub.conf", grubCfg},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", []byte(grubRecoveryCfgAsset))
defer restore()
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, IsNil)
// ensure only a single file got copied (the grub.cfg)
files, err := filepath.Glob(filepath.Join(s.rootdir, "EFI/ubuntu/*"))
c.Assert(err, IsNil)
// grub.cfg and grubenv
c.Check(files, HasLen, 2)
// check that the recovery bootloader configuration was installed with
// the correct content
c.Check(filepath.Join(s.rootdir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, grubRecoveryCfgAsset)
// ensure no /boot was setup
c.Check(filepath.Join(s.rootdir, "boot"), testutil.FileAbsent)
// ensure the correct recovery system configuration was set
seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, label)
systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
}
func (s *makeBootable20Suite) TestMakeBootableImage20BootFlags(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := "#grub-recovery cfg"
grubRecoveryCfgAsset := "#grub-recovery cfg from assets"
grubCfg := "#grub cfg"
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", grubRecoveryCfg},
{"grub.conf", grubCfg},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", []byte(grubRecoveryCfgAsset))
defer restore()
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
bootFlags := []string{"factory"}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, bootFlags)
c.Assert(err, IsNil)
// ensure the correct recovery system configuration was set
seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, label)
c.Check(seedGenv.Get("snapd_boot_flags"), Equals, "factory")
systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
}
func (s *makeBootable20Suite) testMakeBootableImage20CustomKernelArgs(c *C, whichFile, content, errMsg string) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
grubCfg := "#grub cfg"
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub.conf", grubCfg},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
{whichFile, content},
})
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
if errMsg != "" {
c.Assert(err, ErrorMatches, errMsg)
return
}
c.Assert(err, IsNil)
// ensure the correct recovery system configuration was set
seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, label)
// and kernel command line
systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
switch whichFile {
case "cmdline.extra":
blopts := &bootloader.Options{
Role: bootloader.RoleRecovery,
}
bl, err := bootloader.Find(s.rootdir, blopts)
c.Assert(err, IsNil)
tbl, ok := bl.(bootloader.TrustedAssetsBootloader)
if ok {
candidate := false
defaultCmdLine, err := tbl.DefaultCommandLine(candidate)
c.Assert(err, IsNil)
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, strutil.JoinNonEmpty([]string{defaultCmdLine, content}, " "))
} else {
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, content)
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, "")
}
case "cmdline.full":
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, content)
}
}
func (s *makeBootable20Suite) TestMakeBootableImage20CustomKernelExtraArgs(c *C) {
s.testMakeBootableImage20CustomKernelArgs(c, "cmdline.extra", "foo bar baz", "")
}
func (s *makeBootable20Suite) TestMakeBootableImage20CustomKernelFullArgs(c *C) {
s.testMakeBootableImage20CustomKernelArgs(c, "cmdline.full", "foo bar baz", "")
}
func (s *makeBootable20Suite) TestMakeBootableImage20CustomKernelInvalidArgs(c *C) {
errMsg := `cannot obtain recovery system command line: cannot use kernel command line from gadget: invalid kernel command line in cmdline.extra: disallowed kernel argument "snapd_foo=bar"`
s.testMakeBootableImage20CustomKernelArgs(c, "cmdline.extra", "snapd_foo=bar", errMsg)
}
func (s *makeBootable20Suite) TestMakeBootableImage20UnsetRecoverySystemLabelError(c *C) {
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
err := os.WriteFile(filepath.Join(unpackedGadgetDir, "grub-recovery.conf"), grubRecoveryCfg, 0644)
c.Assert(err, IsNil)
grubCfg := []byte("#grub cfg")
err = os.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
RecoverySystemDir: recoverySystemDir,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, ErrorMatches, "internal error: recovery system label unset")
}
func (s *makeBootable20Suite) TestMakeBootableImage20MultipleRecoverySystemsError(c *C) {
model := boottest.MakeMockUC20Model()
bootWith := &boot.BootableSet{Recovery: true}
err := os.MkdirAll(filepath.Join(s.rootdir, "systems/20191204"), 0755)
c.Assert(err, IsNil)
err = os.MkdirAll(filepath.Join(s.rootdir, "systems/20191205"), 0755)
c.Assert(err, IsNil)
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, ErrorMatches, "cannot make multiple recovery systems bootable yet")
}
func (s *makeBootable20Suite) TestMakeSystemRunnable16Fails(c *C) {
model := boottest.MakeMockModel()
err := boot.MakeRunnableSystem(model, nil, nil)
c.Assert(err, ErrorMatches, `internal error: cannot make pre-UC20 system runnable`)
}
func (s *makeBootable20Suite) testMakeSystemRunnable20(c *C, standalone, factoryReset, classic bool, fromInitrd bool) {
restore := release.MockOnClassic(classic)
defer restore()
dirs.SetRootDir(dirs.GlobalRootDir)
bootloader.Force(nil)
uefiVariableSet := 0
defer boot.MockSetEfiBootVariables(func(description string, assetPath string, optionalData []byte) error {
uefiVariableSet += 1
c.Check(description, Equals, "ubuntu")
return nil
})()
var model *asserts.Model
if classic {
model = boottest.MakeMockUC20Model(map[string]interface{}{
"classic": "true",
"distribution": "ubuntu",
})
} else {
model = boottest.MakeMockUC20Model()
}
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644)
c.Assert(err, IsNil)
genv := grubenv.NewEnv(filepath.Join(mockSeedGrubDir, "grubenv"))
c.Assert(genv.Save(), IsNil)
// setup recovery boot assets
err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot"), 0755)
c.Assert(err, IsNil)
// SHA3-384: 39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/bootx64.efi"),
[]byte("recovery shim content"), 0644)
c.Assert(err, IsNil)
// SHA3-384: aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/grubx64.efi"),
[]byte("recovery grub content"), 0644)
c.Assert(err, IsNil)
// grub on ubuntu-boot
mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu")
mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockBootGrubCfg, nil, 0644)
c.Assert(err, IsNil)
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets")
grubCfg := []byte("#grub cfg")
grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets")
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", string(grubRecoveryCfg)},
{"grub.conf", string(grubCfg)},
{"bootx64.efi", "shim content"},
{"grubx64.efi", "grub content"},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore = assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset)
defer restore()
restore = assets.MockInternal("grub.cfg", grubCfgAsset)
defer restore()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20191216",
BasePath: baseInSeed,
Base: baseInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
// set up observer state
useEncryption := true
obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption)
c.Assert(obs, NotNil)
c.Assert(err, IsNil)
// only grubx64.efi gets installed to system-boot
_, err = obs.Observe(gadget.ContentWrite, gadget.SystemBoot, boot.InitramfsUbuntuBootDir, "EFI/boot/grubx64.efi",
&gadget.ContentChange{After: filepath.Join(unpackedGadgetDir, "grubx64.efi")})
c.Assert(err, IsNil)
// observe recovery assets
err = obs.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir)
c.Assert(err, IsNil)
// set encryption key
myKey := keys.EncryptionKey{}
myKey2 := keys.EncryptionKey{}
for i := range myKey {
myKey[i] = byte(i)
myKey2[i] = byte(128 + i)
}
obs.ChosenEncryptionKeys(myKey, myKey2)
// set a mock recovery kernel
readSystemEssentialCalls := 0
restore = boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
if fromInitrd {
c.Assert(seedDir, Equals, filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"))
} else {
c.Assert(seedDir, Equals, dirs.SnapSeedDir)
}
readSystemEssentialCalls++
return model, []*seed.Snap{mockKernelSeedSnap(snap.R(1)), mockGadgetSeedSnap(c, nil)}, nil
})
defer restore()
provisionCalls := 0
restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error {
provisionCalls++
c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth"))
if factoryReset {
c.Check(mode, Equals, secboot.TPMPartialReprovision)
} else {
c.Check(mode, Equals, secboot.TPMProvisionFull)
}
return nil
})
defer restore()
pcrHandleOfKeyCalls := 0
restore = boot.MockSecbootPCRHandleOfSealedKey(func(p string) (uint32, error) {
pcrHandleOfKeyCalls++
c.Check(provisionCalls, Equals, 0)
if !factoryReset {
c.Errorf("unexpected call in non-factory-reset scenario")
return 0, fmt.Errorf("unexpected call")
}
c.Check(p, Equals,
filepath.Join(s.rootdir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key"))
// trigger use of alt handles as current key is using the main handle
return secboot.FallbackObjectPCRPolicyCounterHandle, nil
})
defer restore()
releasePCRHandleCalls := 0
restore = boot.MockSecbootReleasePCRResourceHandles(func(handles ...uint32) error {
c.Check(factoryReset, Equals, true)
releasePCRHandleCalls++
c.Check(handles, DeepEquals, []uint32{
secboot.AltRunObjectPCRPolicyCounterHandle,
secboot.AltFallbackObjectPCRPolicyCounterHandle,
})
return nil
})
defer restore()
hasFDESetupHookCalled := false
restore = boot.MockHasFDESetupHook(func(kernel *snap.Info) (bool, error) {
c.Check(kernel, Equals, kernelInfo)
hasFDESetupHookCalled = true
return false, nil
})
defer restore()
// set mock key sealing
sealKeysCalls := 0
restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error {
c.Assert(provisionCalls, Equals, 1, Commentf("TPM must have been provisioned before"))
sealKeysCalls++
switch sealKeysCalls {
case 1:
c.Check(keys, HasLen, 1)
c.Check(keys[0].Key, DeepEquals, myKey)
c.Check(keys[0].KeyFile, Equals,
filepath.Join(s.rootdir, "/run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key"))
if factoryReset {
c.Check(params.PCRPolicyCounterHandle, Equals, secboot.AltRunObjectPCRPolicyCounterHandle)
} else {
c.Check(params.PCRPolicyCounterHandle, Equals, secboot.RunObjectPCRPolicyCounterHandle)
}
case 2:
c.Check(keys, HasLen, 2)
c.Check(keys[0].Key, DeepEquals, myKey)
c.Check(keys[1].Key, DeepEquals, myKey2)
c.Check(keys[0].KeyFile, Equals,
filepath.Join(s.rootdir,
"/run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key"))
if factoryReset {
c.Check(params.PCRPolicyCounterHandle, Equals, secboot.AltFallbackObjectPCRPolicyCounterHandle)
c.Check(keys[1].KeyFile, Equals,
filepath.Join(s.rootdir,
"/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key.factory-reset"))
} else {
c.Check(params.PCRPolicyCounterHandle, Equals, secboot.FallbackObjectPCRPolicyCounterHandle)
c.Check(keys[1].KeyFile, Equals,
filepath.Join(s.rootdir,
"/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key"))
}
default:
c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls)
}
c.Assert(params.ModelParams, HasLen, 1)
shim := bootloader.NewBootFile("", filepath.Join(s.rootdir,
"var/lib/snapd/boot-assets/grub/bootx64.efi-39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"),
bootloader.RoleRecovery)
grub := bootloader.NewBootFile("", filepath.Join(s.rootdir,
"var/lib/snapd/boot-assets/grub/grubx64.efi-aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"),
bootloader.RoleRecovery)
runGrub := bootloader.NewBootFile("", filepath.Join(s.rootdir,
"var/lib/snapd/boot-assets/grub/grubx64.efi-5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"),
bootloader.RoleRunMode)
kernel := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery)
var runKernelPath string
var runKernel bootloader.BootFile
switch {
case !standalone:
runKernelPath = "/var/lib/snapd/snaps/pc-kernel_5.snap"
case classic:
runKernelPath = "/run/mnt/ubuntu-data/var/lib/snapd/snaps/pc-kernel_5.snap"
case !classic:
runKernelPath = "/run/mnt/ubuntu-data/system-data/var/lib/snapd/snaps/pc-kernel_5.snap"
}
runKernel = bootloader.NewBootFile(filepath.Join(s.rootdir, runKernelPath), "kernel.efi", bootloader.RoleRunMode)
switch sealKeysCalls {
case 1:
c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{
secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(kernel))),
secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(runGrub, secboot.NewLoadChain(runKernel)))),
})
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
"snapd_recovery_mode=factory-reset snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1",
"snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1",
"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1",
})
case 2:
c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{
secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(kernel))),
})
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
"snapd_recovery_mode=factory-reset snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1",
"snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1",
})
default:
c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls)
}
c.Assert(params.ModelParams[0].Model.Model(), Equals, "my-model-uc20")
return nil
})
defer restore()
switch {
case standalone && fromInitrd:
err = boot.MakeRunnableStandaloneSystemFromInitrd(model, bootWith, obs)
case standalone && !fromInitrd:
u := mockUnlocker{}
err = boot.MakeRunnableStandaloneSystem(model, bootWith, obs, u.unlocker)
c.Check(u.unlocked, Equals, 1)
case factoryReset && !fromInitrd:
err = boot.MakeRunnableSystemAfterDataReset(model, bootWith, obs)
default:
err = boot.MakeRunnableSystem(model, bootWith, obs)
}
c.Assert(err, IsNil)
// also do the logical thing and make the next boot go to run mode
err = boot.EnsureNextBootToRunMode("20191216")
c.Assert(err, IsNil)
c.Check(uefiVariableSet, Equals, 1)
// ensure grub.cfg in boot was installed from internal assets
c.Check(mockBootGrubCfg, testutil.FileEquals, string(grubCfgAsset))
var installHostWritableDir string
if classic {
installHostWritableDir = filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data")
} else {
installHostWritableDir = filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")
}
// ensure base/gadget/kernel got copied to /var/lib/snapd/snaps
core20Snap := filepath.Join(dirs.SnapBlobDirUnder(installHostWritableDir), "core20_3.snap")
gadgetSnap := filepath.Join(dirs.SnapBlobDirUnder(installHostWritableDir), "pc_4.snap")
pcKernelSnap := filepath.Join(dirs.SnapBlobDirUnder(installHostWritableDir), "pc-kernel_5.snap")
c.Check(core20Snap, testutil.FilePresent)
c.Check(gadgetSnap, testutil.FilePresent)
c.Check(pcKernelSnap, testutil.FilePresent)
c.Check(osutil.IsSymlink(core20Snap), Equals, false)
c.Check(osutil.IsSymlink(pcKernelSnap), Equals, false)
// ensure the bootvars got updated the right way
mockSeedGrubenv := filepath.Join(mockSeedGrubDir, "grubenv")
c.Assert(mockSeedGrubenv, testutil.FilePresent)
c.Check(mockSeedGrubenv, testutil.FileContains, "snapd_recovery_mode=run")
c.Check(mockSeedGrubenv, testutil.FileContains, "snapd_good_recovery_systems=20191216")
mockBootGrubenv := filepath.Join(mockBootGrubDir, "grubenv")
c.Check(mockBootGrubenv, testutil.FilePresent)
// ensure that kernel_status is empty, we specifically want this to be set
// to the empty string
// use (?m) to match multi-line file in the regex here, because the file is
// a grubenv with padding #### blocks
c.Check(mockBootGrubenv, testutil.FileMatches, `(?m)^kernel_status=$`)
// check that we have the extracted kernel in the right places, both in the
// old uc16/uc18 location and the new ubuntu-boot partition grub dir
extractedKernel := filepath.Join(mockBootGrubDir, "pc-kernel_5.snap", "kernel.efi")
c.Check(extractedKernel, testutil.FilePresent)
// the new uc20 location
extractedKernelSymlink := filepath.Join(mockBootGrubDir, "kernel.efi")
c.Check(extractedKernelSymlink, testutil.FilePresent)
// ensure modeenv looks correct
var ubuntuDataModeEnvPath, classicLine, base string
if classic {
base = ""
ubuntuDataModeEnvPath = filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/var/lib/snapd/modeenv")
classicLine = "\nclassic=true"
} else {
base = "\nbase=core20_3.snap"
ubuntuDataModeEnvPath = filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv")
}
expectedModeenv := fmt.Sprintf(`mode=run
recovery_system=20191216
current_recovery_systems=20191216
good_recovery_systems=20191216%s
gadget=pc_4.snap
current_kernels=pc-kernel_5.snap
model=my-brand/my-model-uc20%s
grade=dangerous
model_sign_key_id=Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
current_trusted_boot_assets={"grubx64.efi":["5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"]}
current_trusted_recovery_boot_assets={"bootx64.efi":["39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"],"grubx64.efi":["aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"]}
current_kernel_command_lines=["snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1"]
`, base, classicLine)
c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, expectedModeenv)
copiedGrubBin := filepath.Join(
dirs.SnapBootAssetsDirUnder(installHostWritableDir),
"grub",
"grubx64.efi-5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d",
)
copiedRecoveryGrubBin := filepath.Join(
dirs.SnapBootAssetsDirUnder(installHostWritableDir),
"grub",
"grubx64.efi-aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5",
)
copiedRecoveryShimBin := filepath.Join(
dirs.SnapBootAssetsDirUnder(installHostWritableDir),
"grub",
"bootx64.efi-39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37",
)
// only one file in the cache under new root
checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDirUnder(installHostWritableDir), "grub", "*"), []string{
copiedRecoveryShimBin,
copiedGrubBin,
copiedRecoveryGrubBin,
})
// with the right content
c.Check(copiedGrubBin, testutil.FileEquals, "grub content")
c.Check(copiedRecoveryGrubBin, testutil.FileEquals, "recovery grub content")
c.Check(copiedRecoveryShimBin, testutil.FileEquals, "recovery shim content")
// we checked for fde-setup-hook
c.Check(hasFDESetupHookCalled, Equals, true)
// make sure TPM was provisioned
c.Check(provisionCalls, Equals, 1)
// make sure SealKey was called for the run object and the fallback object
c.Check(sealKeysCalls, Equals, 2)
// PCR handle checks
if factoryReset {
c.Check(pcrHandleOfKeyCalls, Equals, 1)
c.Check(releasePCRHandleCalls, Equals, 1)
} else {
c.Check(pcrHandleOfKeyCalls, Equals, 0)
c.Check(releasePCRHandleCalls, Equals, 0)
}
// make sure the marker file for sealed key was created
c.Check(filepath.Join(installHostWritableDir, "/var/lib/snapd/device/fde/sealed-keys"), testutil.FilePresent)
// make sure we wrote the boot chains data file
c.Check(filepath.Join(installHostWritableDir, "/var/lib/snapd/device/fde/boot-chains"), testutil.FilePresent)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20Install(c *C) {
const standalone = false
const factoryReset = false
const classic = false
const fromInitrd = false
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20InstallOnClassic(c *C) {
const standalone = false
const factoryReset = false
const classic = true
const fromInitrd = false
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20FactoryReset(c *C) {
const standalone = false
const factoryReset = true
const classic = false
const fromInitrd = false
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20FactoryResetOnClassic(c *C) {
const standalone = false
const factoryReset = true
const classic = true
const fromInitrd = false
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20InstallFromInitrd(c *C) {
const standalone = true
const factoryReset = false
const classic = false
const fromInitrd = true
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) TestMakeRunnableSystem20ModeInstallBootConfigErr(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
err = os.MkdirAll(mockSeedGrubDir, 0755)
c.Assert(err, IsNil)
// no recovery grub.cfg so that test fails if it ever reaches that point
// grub on ubuntu-boot
mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu")
mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockBootGrubCfg, nil, 0644)
c.Assert(err, IsNil)
unpackedGadgetDir := c.MkDir()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20191216",
BasePath: baseInSeed,
Base: baseInfo,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
// no grub marker in gadget directory raises an error
err = boot.MakeRunnableSystem(model, bootWith, nil)
c.Assert(err, ErrorMatches, "internal error: cannot identify run system bootloader: cannot determine bootloader")
// set up grub.cfg in gadget
grubCfg := []byte("#grub cfg")
err = os.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644)
c.Assert(err, IsNil)
// no write access to destination directory
restore := assets.MockInternal("grub.cfg", nil)
defer restore()
err = boot.MakeRunnableSystem(model, bootWith, nil)
c.Assert(err, ErrorMatches, `cannot install managed bootloader assets: internal error: no boot asset for "grub.cfg"`)
}
func (s *makeBootable20Suite) TestMakeRunnableSystem20RunModeSealKeyErr(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644)
c.Assert(err, IsNil)
// setup recovery boot assets
err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot"), 0755)
c.Assert(err, IsNil)
// SHA3-384: 39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/bootx64.efi"),
[]byte("recovery shim content"), 0644)
c.Assert(err, IsNil)
// SHA3-384: aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/grubx64.efi"),
[]byte("recovery grub content"), 0644)
c.Assert(err, IsNil)
// grub on ubuntu-boot
mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu")
mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockBootGrubCfg, nil, 0644)
c.Assert(err, IsNil)
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets")
grubCfg := []byte("#grub cfg")
grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets")
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", string(grubRecoveryCfg)},
{"grub.conf", string(grubCfg)},
{"bootx64.efi", "shim content"},
{"grubx64.efi", "grub content"},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset)
defer restore()
restore = assets.MockInternal("grub.cfg", grubCfgAsset)
defer restore()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20191216",
BasePath: baseInSeed,
Base: baseInfo,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
// set up observer state
useEncryption := true
obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption)
c.Assert(obs, NotNil)
c.Assert(err, IsNil)
// only grubx64.efi gets installed to system-boot
_, err = obs.Observe(gadget.ContentWrite, gadget.SystemBoot, boot.InitramfsUbuntuBootDir, "EFI/boot/grubx64.efi",
&gadget.ContentChange{After: filepath.Join(unpackedGadgetDir, "grubx64.efi")})
c.Assert(err, IsNil)
// observe recovery assets
err = obs.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir)
c.Assert(err, IsNil)
// set encryption key
myKey := keys.EncryptionKey{}
myKey2 := keys.EncryptionKey{}
for i := range myKey {
myKey[i] = byte(i)
myKey2[i] = byte(128 + i)
}
obs.ChosenEncryptionKeys(myKey, myKey2)
// set a mock recovery kernel
readSystemEssentialCalls := 0
restore = boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
readSystemEssentialCalls++
return model, []*seed.Snap{mockKernelSeedSnap(snap.R(1)), mockGadgetSeedSnap(c, nil)}, nil
})
defer restore()
provisionCalls := 0
restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error {
provisionCalls++
c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth"))
c.Check(mode, Equals, secboot.TPMProvisionFull)
return nil
})
defer restore()
// set mock key sealing
sealKeysCalls := 0
restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error {
sealKeysCalls++
switch sealKeysCalls {
case 1:
c.Check(keys, HasLen, 1)
c.Check(keys[0].Key, DeepEquals, myKey)
case 2:
c.Check(keys, HasLen, 2)
c.Check(keys[0].Key, DeepEquals, myKey)
c.Check(keys[1].Key, DeepEquals, myKey2)
default:
c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls)
}
c.Assert(params.ModelParams, HasLen, 1)
shim := bootloader.NewBootFile("", filepath.Join(s.rootdir,
"var/lib/snapd/boot-assets/grub/bootx64.efi-39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"),
bootloader.RoleRecovery)
grub := bootloader.NewBootFile("", filepath.Join(s.rootdir,
"var/lib/snapd/boot-assets/grub/grubx64.efi-aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"),
bootloader.RoleRecovery)
runGrub := bootloader.NewBootFile("", filepath.Join(s.rootdir,
"var/lib/snapd/boot-assets/grub/grubx64.efi-5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"),
bootloader.RoleRunMode)
kernel := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery)
runKernel := bootloader.NewBootFile(filepath.Join(s.rootdir, "var/lib/snapd/snaps/pc-kernel_5.snap"), "kernel.efi", bootloader.RoleRunMode)
c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{
secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(kernel))),
secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(runGrub, secboot.NewLoadChain(runKernel)))),
})
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{
"snapd_recovery_mode=factory-reset snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1",
"snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1",
"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1",
})
c.Assert(params.ModelParams[0].Model.Model(), Equals, "my-model-uc20")
return fmt.Errorf("seal error")
})
defer restore()
err = boot.MakeRunnableSystem(model, bootWith, obs)
c.Assert(err, ErrorMatches, "cannot seal the encryption keys: seal error")
// the TPM was provisioned
c.Check(provisionCalls, Equals, 1)
}
func (s *makeBootable20Suite) testMakeSystemRunnable20WithCustomKernelArgs(c *C, whichFile, content, errMsg string, cmdlines map[string]string) {
if cmdlines == nil {
cmdlines = map[string]string{}
}
bootloader.Force(nil)
uefiVariableSet := 0
defer boot.MockSetEfiBootVariables(func(description string, assetPath string, optionalData []byte) error {
uefiVariableSet += 1
c.Check(description, Equals, "ubuntu")
return nil
})()
model := boottest.MakeMockUC20Model()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644)
c.Assert(err, IsNil)
genv := grubenv.NewEnv(filepath.Join(mockSeedGrubDir, "grubenv"))
c.Assert(genv.Save(), IsNil)
// setup recovery boot assets
err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot"), 0755)
c.Assert(err, IsNil)
// SHA3-384: 39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/bootx64.efi"),
[]byte("recovery shim content"), 0644)
c.Assert(err, IsNil)
// SHA3-384: aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/grubx64.efi"),
[]byte("recovery grub content"), 0644)
c.Assert(err, IsNil)
// grub on ubuntu-boot
mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu")
mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockBootGrubCfg, nil, 0644)
c.Assert(err, IsNil)
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets")
grubCfg := []byte("#grub cfg")
grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets")
gadgetFiles := [][]string{
{"grub-recovery.conf", string(grubRecoveryCfg)},
{"grub.conf", string(grubCfg)},
{"bootx64.efi", "shim content"},
{"grubx64.efi", "grub content"},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
{whichFile, content},
}
snaptest.PopulateDir(unpackedGadgetDir, gadgetFiles)
restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset)
defer restore()
restore = assets.MockInternal("grub.cfg", grubCfgAsset)
defer restore()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20191216",
BasePath: baseInSeed,
Base: baseInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
// set up observer state
useEncryption := true
obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption)
c.Assert(obs, NotNil)
c.Assert(err, IsNil)
// only grubx64.efi gets installed to system-boot
_, err = obs.Observe(gadget.ContentWrite, gadget.SystemBoot, boot.InitramfsUbuntuBootDir, "EFI/boot/grubx64.efi",
&gadget.ContentChange{After: filepath.Join(unpackedGadgetDir, "grubx64.efi")})
c.Assert(err, IsNil)
// observe recovery assets
err = obs.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir)
c.Assert(err, IsNil)
obs.ChosenEncryptionKeys(keys.EncryptionKey{}, keys.EncryptionKey{})
// set a mock recovery kernel
readSystemEssentialCalls := 0
restore = boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
readSystemEssentialCalls++
return model, []*seed.Snap{mockKernelSeedSnap(snap.R(1)), mockGadgetSeedSnap(c, gadgetFiles)}, nil
})
defer restore()
provisionCalls := 0
restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error {
provisionCalls++
c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth"))
c.Check(mode, Equals, secboot.TPMProvisionFull)
return nil
})
defer restore()
// set mock key sealing
sealKeysCalls := 0
restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error {
sealKeysCalls++
switch sealKeysCalls {
case 1, 2:
// expecting only 2 calls
default:
c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls)
}
c.Assert(params.ModelParams, HasLen, 1)
switch sealKeysCalls {
case 1:
c.Assert(params.ModelParams[0].KernelCmdlines, HasLen, 3)
c.Assert(params.ModelParams[0].KernelCmdlines, testutil.Contains, cmdlines["recover"])
c.Assert(params.ModelParams[0].KernelCmdlines, testutil.Contains, cmdlines["factory-reset"])
c.Assert(params.ModelParams[0].KernelCmdlines, testutil.Contains, cmdlines["run"])
case 2:
c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{cmdlines["factory-reset"], cmdlines["recover"]})
default:
c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls)
}
c.Assert(params.ModelParams[0].Model.Model(), Equals, "my-model-uc20")
return nil
})
defer restore()
err = boot.MakeRunnableSystem(model, bootWith, obs)
if errMsg != "" {
c.Assert(err, ErrorMatches, errMsg)
return
}
c.Assert(err, IsNil)
// also do the logical thing and make the next boot go to run mode
err = boot.EnsureNextBootToRunMode("20191216")
c.Assert(err, IsNil)
c.Check(uefiVariableSet, Equals, 1)
// ensure grub.cfg in boot was installed from internal assets
c.Check(mockBootGrubCfg, testutil.FileEquals, string(grubCfgAsset))
// ensure the bootvars got updated the right way
mockSeedGrubenv := filepath.Join(mockSeedGrubDir, "grubenv")
c.Assert(mockSeedGrubenv, testutil.FilePresent)
c.Check(mockSeedGrubenv, testutil.FileContains, "snapd_recovery_mode=run")
c.Check(mockSeedGrubenv, testutil.FileContains, "snapd_good_recovery_systems=20191216")
mockBootGrubenv := filepath.Join(mockBootGrubDir, "grubenv")
c.Check(mockBootGrubenv, testutil.FilePresent)
systemGenv := grubenv.NewEnv(mockBootGrubenv)
c.Assert(systemGenv.Load(), IsNil)
switch whichFile {
case "cmdline.extra":
blopts := &bootloader.Options{
Role: bootloader.RoleRunMode,
NoSlashBoot: true,
}
bl, err := bootloader.Find(boot.InitramfsUbuntuBootDir, blopts)
c.Assert(err, IsNil)
tbl, ok := bl.(bootloader.TrustedAssetsBootloader)
if ok {
candidate := false
defaultCmdLine, err := tbl.DefaultCommandLine(candidate)
c.Assert(err, IsNil)
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, strutil.JoinNonEmpty([]string{defaultCmdLine, content}, " "))
} else {
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, content)
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, "")
}
case "cmdline.full":
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, content)
}
// ensure modeenv looks correct
ubuntuDataModeEnvPath := filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv")
c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, fmt.Sprintf(`mode=run
recovery_system=20191216
current_recovery_systems=20191216
good_recovery_systems=20191216
base=core20_3.snap
gadget=pc_4.snap
current_kernels=pc-kernel_5.snap
model=my-brand/my-model-uc20
grade=dangerous
model_sign_key_id=Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
current_trusted_boot_assets={"grubx64.efi":["5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"]}
current_trusted_recovery_boot_assets={"bootx64.efi":["39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"],"grubx64.efi":["aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"]}
current_kernel_command_lines=["%v"]
`, cmdlines["run"]))
// make sure the TPM was provisioned
c.Check(provisionCalls, Equals, 1)
// make sure SealKey was called for the run object and the fallback object
c.Check(sealKeysCalls, Equals, 2)
// make sure the marker file for sealed key was created
c.Check(filepath.Join(dirs.SnapFDEDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "sealed-keys"), testutil.FilePresent)
// make sure we wrote the boot chains data file
c.Check(filepath.Join(dirs.SnapFDEDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "boot-chains"), testutil.FilePresent)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20WithCustomKernelExtraArgs(c *C) {
cmdlines := map[string]string{
"run": "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz",
"recover": "snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1 foo bar baz",
"factory-reset": "snapd_recovery_mode=factory-reset snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1 foo bar baz",
}
s.testMakeSystemRunnable20WithCustomKernelArgs(c, "cmdline.extra", "foo bar baz", "", cmdlines)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20WithCustomKernelFullArgs(c *C) {
cmdlines := map[string]string{
"run": "snapd_recovery_mode=run foo bar baz",
"recover": "snapd_recovery_mode=recover snapd_recovery_system=20191216 foo bar baz",
"factory-reset": "snapd_recovery_mode=factory-reset snapd_recovery_system=20191216 foo bar baz",
}
s.testMakeSystemRunnable20WithCustomKernelArgs(c, "cmdline.full", "foo bar baz", "", cmdlines)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20WithCustomKernelInvalidArgs(c *C) {
errMsg := `cannot compose the candidate command line: cannot use kernel command line from gadget: invalid kernel command line in cmdline.extra: disallowed kernel argument "snapd=unhappy"`
s.testMakeSystemRunnable20WithCustomKernelArgs(c, "cmdline.extra", "foo bar snapd=unhappy", errMsg, nil)
}
func (s *makeBootable20Suite) TestMakeSystemRunnable20UnhappyMarkRecoveryCapable(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644)
c.Assert(err, IsNil)
// there is no grubenv in ubuntu-seed so loading from it will fail
// setup recovery boot assets
err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot"), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/bootx64.efi"),
[]byte("recovery shim content"), 0644)
c.Assert(err, IsNil)
err = os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/grubx64.efi"),
[]byte("recovery grub content"), 0644)
c.Assert(err, IsNil)
// grub on ubuntu-boot
mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu")
mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockBootGrubCfg, nil, 0644)
c.Assert(err, IsNil)
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets")
grubCfg := []byte("#grub cfg")
grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets")
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", string(grubRecoveryCfg)},
{"grub.conf", string(grubCfg)},
{"bootx64.efi", "shim content"},
{"grubx64.efi", "grub content"},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset)
defer restore()
restore = assets.MockInternal("grub.cfg", grubCfgAsset)
defer restore()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20191216",
BasePath: baseInSeed,
Base: baseInfo,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
// set a mock recovery kernel
readSystemEssentialCalls := 0
restore = boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) {
readSystemEssentialCalls++
return model, []*seed.Snap{mockKernelSeedSnap(snap.R(1)), mockGadgetSeedSnap(c, nil)}, nil
})
defer restore()
err = boot.MakeRunnableSystem(model, bootWith, nil)
c.Assert(err, ErrorMatches, `cannot record "20191216" as a recovery capable system: open .*/run/mnt/ubuntu-seed/EFI/ubuntu/grubenv: no such file or directory`)
}
func (s *makeBootable20UbootSuite) TestUbootMakeBootableImage20TraditionalUbootenvFails(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
ubootEnv := []byte("#uboot env")
err := os.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), ubootEnv, 0644)
c.Assert(err, IsNil)
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err = os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.img", "I'm a kernel"},
{"initrd.img", "...and I'm an initrd"},
{"dtbs/foo.dtb", "foo dtb"},
{"dtbs/bar.dto", "bar dtbo"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
// TODO:UC20: enable this use case
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, ErrorMatches, `cannot install bootloader: non-empty uboot.env not supported on UC20\+ yet`)
}
func (s *makeBootable20UbootSuite) TestUbootMakeBootableImage20BootScr(c *C) {
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
// the uboot.conf must be empty for this to work/do the right thing
err := os.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), nil, 0644)
c.Assert(err, IsNil)
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err = os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.img", "I'm a kernel"},
{"initrd.img", "...and I'm an initrd"},
{"dtbs/foo.dtb", "foo dtb"},
{"dtbs/bar.dto", "bar dtbo"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, IsNil)
// since uboot.conf was absent, we won't have installed the uboot.env, as
// it is expected that the gadget assets would have installed boot.scr
// instead
c.Check(filepath.Join(s.rootdir, "uboot.env"), testutil.FileAbsent)
c.Check(s.bootloader.BootVars, DeepEquals, map[string]string{
"snapd_recovery_system": label,
"snapd_recovery_mode": "install",
})
// ensure the correct recovery system configuration was set
c.Check(
s.bootloader.ExtractRecoveryKernelAssetsCalls,
DeepEquals,
[]bootloadertest.ExtractedRecoveryKernelCall{{
RecoverySystemDir: recoverySystemDir,
S: kernelInfo,
}},
)
}
func (s *makeBootable20UbootSuite) TestUbootMakeBootableImage20BootSelNoHeaderFlagByte(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
unpackedGadgetDir := c.MkDir()
// the uboot.conf must be empty for this to work/do the right thing
err := os.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), nil, 0644)
c.Assert(err, IsNil)
sampleEnv, err := ubootenv.Create(filepath.Join(unpackedGadgetDir, "boot.sel"), 4096, ubootenv.CreateOptions{HeaderFlagByte: false})
c.Assert(err, IsNil)
err = sampleEnv.Save()
c.Assert(err, IsNil)
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err = os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.img", "I'm a kernel"},
{"initrd.img", "...and I'm an initrd"},
{"dtbs/foo.dtb", "foo dtb"},
{"dtbs/bar.dto", "bar dtbo"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
c.Assert(err, IsNil)
// since uboot.conf was absent, we won't have installed the uboot.env, as
// it is expected that the gadget assets would have installed boot.scr
// instead
c.Check(filepath.Join(s.rootdir, "uboot.env"), testutil.FileAbsent)
env, err := ubootenv.Open(filepath.Join(s.rootdir, "/uboot/ubuntu/boot.sel"))
c.Assert(err, IsNil)
// Since we have a boot.sel w/o a header flag byte in our gadget,
// our recovery boot sel also should not have a header flag byte
c.Check(env.HeaderFlagByte(), Equals, false)
}
func (s *makeBootable20UbootSuite) TestUbootMakeRunnableSystem20RunModeBootSel(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// uboot on ubuntu-seed
mockSeedUbootBootSel := filepath.Join(boot.InitramfsUbuntuSeedDir, "uboot/ubuntu/boot.sel")
err = os.MkdirAll(filepath.Dir(mockSeedUbootBootSel), 0755)
c.Assert(err, IsNil)
env, err := ubootenv.Create(mockSeedUbootBootSel, 4096, ubootenv.CreateOptions{HeaderFlagByte: true})
c.Assert(err, IsNil)
c.Assert(env.Save(), IsNil)
// uboot on ubuntu-boot (as if it was installed when creating the partition)
mockBootUbootBootSel := filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/boot.sel")
err = os.MkdirAll(filepath.Dir(mockBootUbootBootSel), 0755)
c.Assert(err, IsNil)
env, err = ubootenv.Create(mockBootUbootBootSel, 4096, ubootenv.CreateOptions{HeaderFlagByte: true})
c.Assert(err, IsNil)
c.Assert(env.Save(), IsNil)
unpackedGadgetDir := c.MkDir()
c.Assert(os.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), nil, 0644), IsNil)
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
kernelSnapFiles := [][]string{
{"kernel.img", "I'm a kernel"},
{"initrd.img", "...and I'm an initrd"},
{"dtbs/foo.dtb", "foo dtb"},
{"dtbs/bar.dto", "bar dtbo"},
}
kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel
type: kernel
version: 5.0
`, snap.R(5), kernelSnapFiles)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20191216",
BasePath: baseInSeed,
Base: baseInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
err = boot.MakeRunnableSystem(model, bootWith, nil)
c.Assert(err, IsNil)
// also do the logical next thing which is to ensure that the system
// reboots into run mode
err = boot.EnsureNextBootToRunMode("20191216")
c.Assert(err, IsNil)
// ensure base/kernel got copied to /var/lib/snapd/snaps
c.Check(filepath.Join(dirs.SnapBlobDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "core20_3.snap"), testutil.FilePresent)
c.Check(filepath.Join(dirs.SnapBlobDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "arm-kernel_5.snap"), testutil.FilePresent)
// ensure the bootvars on ubuntu-seed got updated the right way
mockSeedUbootenv := filepath.Join(boot.InitramfsUbuntuSeedDir, "uboot/ubuntu/boot.sel")
uenvSeed, err := ubootenv.Open(mockSeedUbootenv)
c.Assert(err, IsNil)
c.Assert(uenvSeed.Get("snapd_recovery_mode"), Equals, "run")
c.Assert(uenvSeed.HeaderFlagByte(), Equals, true)
// now check ubuntu-boot boot.sel
mockBootUbootenv := filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/boot.sel")
uenvBoot, err := ubootenv.Open(mockBootUbootenv)
c.Assert(err, IsNil)
c.Assert(uenvBoot.Get("snap_try_kernel"), Equals, "")
c.Assert(uenvBoot.Get("snap_kernel"), Equals, "arm-kernel_5.snap")
c.Assert(uenvBoot.Get("kernel_status"), Equals, boot.DefaultStatus)
c.Assert(uenvBoot.HeaderFlagByte(), Equals, true)
// check that we have the extracted kernel in the right places, in the
// old uc16/uc18 location
for _, file := range kernelSnapFiles {
fName := file[0]
c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/arm-kernel_5.snap", fName), testutil.FilePresent)
}
// ensure modeenv looks correct
ubuntuDataModeEnvPath := filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv")
c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, `mode=run
recovery_system=20191216
current_recovery_systems=20191216
good_recovery_systems=20191216
base=core20_3.snap
gadget=pc_4.snap
current_kernels=arm-kernel_5.snap
model=my-brand/my-model-uc20
grade=dangerous
model_sign_key_id=Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
`)
}
func (s *makeBootable20Suite) TestMakeRecoverySystemBootableAtRuntime20(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
// on uc20 the seed layout if different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
gadgets := map[string]string{}
for _, rev := range []snap.Revision{snap.R(1), snap.R(5)} {
gadgetFn, gadgetInfo := makeSnapWithFiles(c, "pc", gadgetSnapYaml, rev, [][]string{
{"grub.conf", ""},
{"meta/snap.yaml", gadgetSnapYaml},
{"cmdline.full", fmt.Sprintf("args from gadget rev %s", rev.String())},
{"meta/gadget.yaml", gadgetYaml},
})
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Rename(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
// keep track of the gadgets
gadgets[rev.String()] = gadgetInSeed
}
snaptest.PopulateDir(s.rootdir, [][]string{
{"EFI/ubuntu/grub.cfg", "this is grub"},
{"EFI/ubuntu/grubenv", "canary"},
})
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
err = boot.MakeRecoverySystemBootable(model, s.rootdir, recoverySystemDir, &boot.RecoverySystemBootableSet{
Kernel: kernelInfo,
KernelPath: kernelInSeed,
// use gadget revision 1
GadgetSnapOrDir: gadgets["1"],
// like it's called when creating a new recovery system
PrepareImageTime: false,
})
c.Assert(err, IsNil)
// the recovery partition grubenv was not modified
c.Check(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv"), testutil.FileEquals, "canary")
systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, "args from gadget rev 1")
// create another system under a new label
newLabel := "20210420"
newRecoverySystemDir := filepath.Join("/systems", newLabel)
// with a different gadget revision, but same kernel
err = boot.MakeRecoverySystemBootable(model, s.rootdir, newRecoverySystemDir, &boot.RecoverySystemBootableSet{
Kernel: kernelInfo,
KernelPath: kernelInSeed,
GadgetSnapOrDir: gadgets["5"],
// like it's called when creating a new recovery system
PrepareImageTime: false,
})
c.Assert(err, IsNil)
systemGenv = grubenv.NewEnv(filepath.Join(s.rootdir, newRecoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, "args from gadget rev 5")
}
func (s *makeBootable20Suite) TestMakeBootablePartition(c *C) {
bootloader.Force(nil)
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := "#grub-recovery cfg"
grubRecoveryCfgAsset := "#grub-recovery cfg from assets"
grubCfg := "#grub cfg"
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", grubRecoveryCfg},
{"grub.conf", grubCfg},
{"meta/snap.yaml", gadgetSnapYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", []byte(grubRecoveryCfgAsset))
defer restore()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core22", `name: core22
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
RecoverySystemLabel: "",
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: false,
}
opts := &bootloader.Options{
PrepareImageTime: false,
// We need the same configuration that a recovery partition,
// as we will chainload to grub in the boot partition.
Role: bootloader.RoleRecovery,
}
partMntDir := filepath.Join(s.rootdir, "/partition")
err = os.MkdirAll(partMntDir, 0755)
c.Assert(err, IsNil)
err = boot.MakeBootablePartition(partMntDir, opts, bootWith, boot.ModeRun, []string{})
c.Assert(err, IsNil)
// ensure we have only grub.cfg and grubenv
files, err := filepath.Glob(filepath.Join(partMntDir, "EFI/ubuntu/*"))
c.Assert(err, IsNil)
c.Check(files, HasLen, 2)
// and nothing else
files, err = filepath.Glob(filepath.Join(partMntDir, "EFI/*"))
c.Assert(err, IsNil)
c.Check(files, HasLen, 1)
files, err = filepath.Glob(filepath.Join(partMntDir, "*"))
c.Assert(err, IsNil)
c.Check(files, HasLen, 1)
// check that the recovery bootloader configuration was installed with
// the correct content
c.Check(filepath.Join(partMntDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, grubRecoveryCfgAsset)
// ensure the correct recovery system configuration was set
seedGenv := grubenv.NewEnv(filepath.Join(partMntDir, "EFI/ubuntu/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, "")
c.Check(seedGenv.Get("snapd_recovery_mode"), Equals, boot.ModeRun)
c.Check(seedGenv.Get("snapd_good_recovery_systems"), Equals, "")
}
func (s *makeBootable20Suite) TestMakeRunnableSystemNoGoodRecoverySystems(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644)
c.Assert(err, IsNil)
genv := grubenv.NewEnv(filepath.Join(mockSeedGrubDir, "grubenv"))
c.Assert(genv.Save(), IsNil)
// mock grub so it is detected as the current bootloader
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets")
grubCfg := []byte("#grub cfg")
grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets")
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", string(grubRecoveryCfg)},
{"grub.conf", string(grubCfg)},
{"bootx64.efi", "shim content"},
{"grubx64.efi", "grub content"},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset)
defer restore()
restore = assets.MockInternal("grub.cfg", grubCfgAsset)
defer restore()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 5.0
`, snap.R(4))
gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename())
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
BasePath: baseInSeed,
Base: baseInfo,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
err = boot.MakeRunnableSystem(model, bootWith, nil)
c.Assert(err, IsNil)
// ensure that there are no good recovery systems as RecoverySystemLabel was empty
mockSeedGrubenv := filepath.Join(mockSeedGrubDir, "grubenv")
c.Check(mockSeedGrubenv, testutil.FilePresent)
systemGenv := grubenv.NewEnv(mockSeedGrubenv)
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_good_recovery_systems"), Equals, "")
}
func (s *makeBootable20Suite) TestMakeRunnableSystemStandaloneSnapsCopy(c *C) {
bootloader.Force(nil)
model := boottest.MakeMockUC20Model()
snapsDirs := filepath.Join(s.rootdir, "/somewhere")
err := os.MkdirAll(snapsDirs, 0755)
c.Assert(err, IsNil)
// grub on ubuntu-seed
mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu")
mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg")
err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755)
c.Assert(err, IsNil)
err = os.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644)
c.Assert(err, IsNil)
genv := grubenv.NewEnv(filepath.Join(mockSeedGrubDir, "grubenv"))
c.Assert(genv.Save(), IsNil)
// mock grub so it is detected as the current bootloader
unpackedGadgetDir := c.MkDir()
grubRecoveryCfg := []byte("#grub-recovery cfg")
grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets")
grubCfg := []byte("#grub cfg")
grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets")
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub-recovery.conf", string(grubRecoveryCfg)},
{"grub.conf", string(grubCfg)},
{"bootx64.efi", "shim content"},
{"grubx64.efi", "grub content"},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml},
})
restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset)
defer restore()
restore = assets.MockInternal("grub.cfg", grubCfgAsset)
defer restore()
// make the snaps symlinks so that we can ensure that makebootable follows
// the symlinks and copies the files and not the symlinks
baseFn, baseInfo := makeSnap(c, "core20", `name: core20
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(snapsDirs, "core20")
err = os.Symlink(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 4.1
`, snap.R(5),
[][]string{
{"kernel.efi", "I'm a kernel.efi"},
},
)
kernelInSeed := filepath.Join(snapsDirs, "pc-kernel_4.1.snap")
err = os.Symlink(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc
type: gadget
version: 3.0
`, snap.R(4))
gadgetInSeed := filepath.Join(snapsDirs, "pc_3.0.snap")
err = os.Symlink(gadgetFn, gadgetInSeed)
c.Assert(err, IsNil)
bootWith := &boot.BootableSet{
RecoverySystemLabel: "20221004",
BasePath: baseInSeed,
Base: baseInfo,
KernelPath: kernelInSeed,
Kernel: kernelInfo,
Gadget: gadgetInfo,
GadgetPath: gadgetInSeed,
Recovery: false,
UnpackedGadgetDir: unpackedGadgetDir,
}
err = boot.MakeRunnableSystem(model, bootWith, nil)
c.Assert(err, IsNil)
installHostWritableDir := filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")
// ensure base/gadget/kernel got copied to /var/lib/snapd/snaps
core20Snap := filepath.Join(dirs.SnapBlobDirUnder(installHostWritableDir), "core20_3.snap")
gadgetSnap := filepath.Join(dirs.SnapBlobDirUnder(installHostWritableDir), "pc_4.snap")
pcKernelSnap := filepath.Join(dirs.SnapBlobDirUnder(installHostWritableDir), "pc-kernel_5.snap")
c.Check(core20Snap, testutil.FilePresent)
c.Check(gadgetSnap, testutil.FilePresent)
c.Check(pcKernelSnap, testutil.FilePresent)
c.Check(osutil.IsSymlink(core20Snap), Equals, false)
c.Check(osutil.IsSymlink(pcKernelSnap), Equals, false)
c.Check(osutil.IsSymlink(gadgetSnap), Equals, false)
// check modeenv
ubuntuDataModeEnvPath := filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv")
expectedModeenv := `mode=run
recovery_system=20221004
current_recovery_systems=20221004
good_recovery_systems=20221004
base=core20_3.snap
gadget=pc_4.snap
current_kernels=pc-kernel_5.snap
model=my-brand/my-model-uc20
grade=dangerous
model_sign_key_id=Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
current_kernel_command_lines=["snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1"]
`
c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, expectedModeenv)
}
func (s *makeBootable20Suite) TestMakeStandaloneSystemRunnable20Install(c *C) {
const standalone = true
const factoryReset = false
const classic = false
const fromInitrd = false
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) TestMakeStandaloneSystemRunnable20InstallOnClassic(c *C) {
const standalone = true
const factoryReset = false
const classic = true
const fromInitrd = false
s.testMakeSystemRunnable20(c, standalone, factoryReset, classic, fromInitrd)
}
func (s *makeBootable20Suite) testMakeBootableImageOptionalKernelArgs(c *C, model *asserts.Model, options map[string]string, expectedCmdline, errMsg string) {
bootloader.Force(nil)
defaults := "defaults:\n system:\n"
for k, v := range options {
defaults += fmt.Sprintf(" %s: %s\n", k, v)
}
unpackedGadgetDir := c.MkDir()
grubCfg := "#grub cfg"
snaptest.PopulateDir(unpackedGadgetDir, [][]string{
{"grub.conf", grubCfg},
{"meta/snap.yaml", gadgetSnapYaml},
{"meta/gadget.yaml", gadgetYaml + defaults},
})
// on uc20 the seed layout is different
seedSnapsDirs := filepath.Join(s.rootdir, "/snaps")
err := os.MkdirAll(seedSnapsDirs, 0755)
c.Assert(err, IsNil)
baseFn, baseInfo := makeSnap(c, "core22", `name: core22
type: base
version: 5.0
`, snap.R(3))
baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename())
err = os.Rename(baseFn, baseInSeed)
c.Assert(err, IsNil)
kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel
type: kernel
version: 5.0
`, snap.R(5), [][]string{
{"kernel.efi", "I'm a kernel.efi"},
})
kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename())
err = os.Rename(kernelFn, kernelInSeed)
c.Assert(err, IsNil)
label := "20191209"
recoverySystemDir := filepath.Join("/systems", label)
bootWith := &boot.BootableSet{
Base: baseInfo,
BasePath: baseInSeed,
Kernel: kernelInfo,
KernelPath: kernelInSeed,
RecoverySystemDir: recoverySystemDir,
RecoverySystemLabel: label,
UnpackedGadgetDir: unpackedGadgetDir,
Recovery: true,
}
err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil)
if errMsg != "" {
c.Assert(err, ErrorMatches, errMsg)
return
}
c.Assert(err, IsNil)
// ensure the correct recovery system configuration was set
seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv"))
c.Assert(seedGenv.Load(), IsNil)
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, label)
// and kernel command line
systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv"))
c.Assert(systemGenv.Load(), IsNil)
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap")
blopts := &bootloader.Options{
Role: bootloader.RoleRecovery,
}
bl, err := bootloader.Find(s.rootdir, blopts)
c.Assert(err, IsNil)
tbl, ok := bl.(bootloader.TrustedAssetsBootloader)
if ok {
candidate := false
defaultCmdLine, err := tbl.DefaultCommandLine(candidate)
c.Assert(err, IsNil)
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, strutil.JoinNonEmpty([]string{defaultCmdLine, expectedCmdline}, " "))
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, "")
} else {
c.Check(systemGenv.Get("snapd_extra_cmdline_args"), Equals, expectedCmdline)
c.Check(systemGenv.Get("snapd_full_cmdline_args"), Equals, "")
}
}
func (s *makeBootable20Suite) TestMakeBootableImageOptionalKernelArgsHappy(c *C) {
model := boottest.MakeMockUC20Model()
const cmdline = "param1=val param2"
for _, opt := range []string{"system.kernel.cmdline-append", "system.kernel.dangerous-cmdline-append"} {
options := map[string]string{
opt: cmdline,
}
s.testMakeBootableImageOptionalKernelArgs(c, model, options, cmdline, "")
}
}
func (s *makeBootable20Suite) TestMakeBootableImageOptionalKernelArgsBothBootOptsSet(c *C) {
model := boottest.MakeMockUC20Model()
const cmdline = "param1=val param2"
const cmdlineDanger = "param3=val param4"
options := map[string]string{
"system.kernel.cmdline-append": cmdline,
"system.kernel.dangerous-cmdline-append": cmdlineDanger,
}
s.testMakeBootableImageOptionalKernelArgs(c, model, options, cmdline+" "+cmdlineDanger, "")
}
func (s *makeBootable20Suite) TestMakeBootableImageOptionalKernelArgsSignedAndDangerous(c *C) {
model := boottest.MakeMockUC20Model(map[string]interface{}{
"grade": "signed",
})
const cmdline = "param1=val param2"
options := map[string]string{
"system.kernel.dangerous-cmdline-append": cmdline,
}
// The option is ignored if non-dangerous model
s.testMakeBootableImageOptionalKernelArgs(c, model, options, "", "")
}