mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* boot: added function to set EFI variables Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: renamed trustedShimFallbackBinary to seedShimPath Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: refactored setting EFI boot variables at install Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: adjusted variable names and fixed variable initialization Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: improve setting Boot#### EFI variable Notably, splits off the process of reading a Boot#### variable and extracting its DevicePath into its own function `readBootVariable` which can be mocked and otherwise simplifies the `setBootNumberVariable` function. Also, fixes behavior around the final BootFFFF variable. Previously, it was not possible to select the BootFFFF variable if it was unused, due to overflow concerns on uint16. Now, the behavior around BootFFFF is identical to that of any other boot variable, by using an int internally instead of uint16, which also allows a more robust check for whether there were no matching variables. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: added unit tests for setting EFI Boot#### variable Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: refactored setting EFI boot variables Rewrote EFI boot variable functions to more closely match the behavior of shim fallback: https://github.com/rhboot/shim/blob/main/fallback.c In particular, the following have changed: 1. Existing Boot#### variables must fully match the new load option to be considered a match. In particular, the load option attributes, label, and device path must all be byte-for-byte identical. Previously, only the device paths were compared. 2. Matching Boot#### variables are no longer overwritten. Since the variable data must now byte-for-byte match the new load option, there is no need to overwrite the existing variable. 3. Since existing Boot#### variables are no longer overwritten, the variable attributes are no longer checked for those variables. Instead, it is assumed that the Boot#### variable attributes are viable for it to be used as a boot option. This matches the behavior of `rhboot/shim/fallback.c`, for better or for worse. 4. When modifying the BootOrder variable, boot option numbers are no longer pruned if there is no matching Boot#### variable. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot,bootloader: introduce UefiBootloader to build EFI load options Previously, the path of the shim binary relative to the EFI partition was passed into `SetEfiBootVariables`. However, different bootloaders may wish to set up `OptionalData` in the load option. Additionally, not all `TrustedAssetBootloaders` will attempt to set EFI boot variables, and not all bootloaders which should set EFI boot variables necessarily support secure boot. Thus, these should be decoupled. This commit adds a new `UefiBootloader` interface with the `ConstructShimEfiLoadOption` method, which builds an EFI load option from the shim path for the given bootloader. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot,bootloader: fixed linting errors and improved EFI boot variable test clarity Signed-off-by: Oliver Calder <oliver.calder@canonical.com> bootloader: improved unit test for grub EFI load option creation Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: set EFI boot variables in `MakeRunnableSystem` Previously, attempted to set boot variables in `MakeRecoverySystemBootable`, which is called by `MakeBootableImage`, which is called when building the image file, rather than during install mode. `MakeRunnableSystem` is called on first boot during install mode, and thus should be responsible for setting EFI boot variables. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: use seed bootloader when setting EFI variables In install mode, the bootloader located in ubuntu-seed should be used when setting the EFI boot variables. Previously, the bootloader in ubuntu-boot was accidentally re-used. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: added simple test to execute setefibootvar.go code Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: fixed standalone set EFI vars code test to work with different layouts Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: moved simple setefibootvar.go check to nested test Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: added check for idempotence when setting EFI boot variables Signed-off-by: Oliver Calder <oliver.calder@canonical.com> bootloader: adjust comments, organization, and add TODO Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot,bootloader: fix setting EFI boot variables Make function to search for EFI asset device path and construct load option common so each UefiBootloader does not have to re-implement it. Instead, the bootloader returns the description, asset file path, and optional data, which can then be used to create the EFI load option. Also, in `makeRunnableSystem`, the bootloader in ubuntu-seed must have `NoSlashBoot` in order to correctly find the grub.cfg file and thus the grub bootloader. This commit fixes this bug, and refactors a bit to account for the changes in responsibilities between the bootloader and the setefibootvars.go code. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> bootloader: fixed grub EFI load option test with tmp rootdir Signed-off-by: Oliver Calder <oliver.calder@canonical.com> go.mod: move golang.org/x/text import next to other golang.org/x/ imports Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: adjust opts to look for recovery bootloader when setting EFI variables Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: do not overwrite BootOrder if unchanged, and unexport EFI variable helper functions Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: unexport `setEfiBootOrderVariable` Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: move code to detect bootloader and set EFI variables accordingly into dedicated function Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: unexport `setUbuntuSeedEfiBootVariables` and accompanying error Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot,bootloader: ensure nil optionalData for EFI variable is equivalent to 0-length slice Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: handle empty boot order and other boot var improvements Signed-off-by: Oliver Calder <oliver.calder@canonical.com> boot: make setefibootvars functions linux-only Signed-off-by: Oliver Calder <oliver.calder@canonical.com> * tests: add nested spread test for setting EFI vars The test checks that EFI boot variables exist for the following: 1. A Boot#### variable pointing to the shim file path. 2. A BootOrder variable with the #### from the above Boot#### as first. Since the layout of EFI assets is dependent on the gadget snap, the test downloads and unpacks the gadget, then modifies the contents so that one variant has the shim and grub binaries in `EFI/boot/` and another variant has the shim and grub binaries in `EFI/ubuntu/` and the fallback binary in `EFI/boot/`. After building a core image around that modified gadget, the VM is booted and the test checks that the EFI variables are set correctly. Then, the test modifies the gadget to match the other variant's initial layout, and then installs the newly modified gadget. This should trigger re-setting EFI boot variables as well. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: fix problems in spread test for setting EFI boot variables Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: disabled TPM on EFI boot vars test and separated gadget script Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: fixed EFI vars test to use correct toolbox and include all EFI assets Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: modify-gadget.sh re-use existing gadget so edition is incremented Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: fix mangled EFI var search string and other improvements Signed-off-by: Oliver Calder <oliver.calder@canonical.com> tests: polish tests for setting EFI boot variables Notably, allow tests/nested/core/core20-set-efi-boot-variables to run on arm64 as well as amd64, simplify setefivars.go to search for multiple assets on multiple architectures, and allow tests/nested/manual/core20-set-efi-boot-vars to run on any ubuntu-2*. Signed-off-by: Oliver Calder <oliver.calder@canonical.com> * bootloader/grub.go: only consider new shim asset in boot entry for now * tests/nested/core/core20-set-efi-boot-variables: fix details * boot: update uefi variables on gadget update * tests/nested/manual/core20-set-efi-boot-vars: work-around file not deleted * tests/nested/manual/core20-set-efi-boot-vars: use fb.efi like other tests * tests/nested/manual/core20-set-efi-boot-vars: drop use of toolbox snap * tests/nested/manual/core20-set-efi-boot-vars: drop work-around for not deleted files * tests/nested/manual/core20-set-efi-boot-vars: verify install does add a boot entry * tests/nested/manual/core20-set-efi-boot-vars: run only on version that have UC * tests/nested/manual/core20-set-efi-boot-vars: obey GADGET_CHANNEL * tests/nested/manual/core20-set-efi-boot-vars: move get_boot_entry.py to libs * tests/nested/manual/core20-set-efi-boot-vars: factorize copy of variables ... so we can reuse the script in other tests * tests/nested/core/core20-set-efi-boot-variables: stop using toolbox snap * tests/nested/core/core20-set-efi-boot-variables: only run on versions with UC available * overlord/devicestate: test using EfiLoadOptionParameters * boot: test that variables are set * boot: test observers' UpdateBootEntry * tests/nested/manual/core20-set-efi-boot-vars: also test without secure boot * many: use trusted install observer when UEFI variables are supported * boot/makebootable.go: rename sealer to observer * boot/grub.go: fix function name in doc * cmd/snap-bootstrap: verify that ObserveExistingTrustedRecoveryAssets is called * boot: add tests for SetEfiBootVariables * many: comment on calls to ObserveExistingTrustedRecoveryAssets --------- Signed-off-by: Oliver Calder <oliver.calder@canonical.com> Co-authored-by: Oliver Calder <oliver.calder@canonical.com>
2411 lines
88 KiB
Go
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, "", "")
|
|
}
|