diff --git a/bootloader/lk.go b/bootloader/lk.go index 5b05b6b8f06..efe5315f8b8 100644 --- a/bootloader/lk.go +++ b/bootloader/lk.go @@ -402,6 +402,110 @@ func (l *lk) ExtractRecoveryKernelAssets(recoverySystemDir string, sn snap.Place return env.Save() } +// bootPartitionPath returns the path to the storage backing the named boot +// image partition. +func (l *lk) bootPartitionPath(bootPartition string) (string, error) { + // TODO: for RoleSole bootloaders this will eventually be the same + // codepath as for non-RoleSole bootloader + if l.role == RoleSole { + return filepath.Join(l.dir(), bootPartition), nil + } + path, _, err := l.devPathForPartName(bootPartition) + return path, err +} + +// bootPartitionsHoldSameImage returns whether the two named boot image +// partitions hold byte for byte identical content. +func (l *lk) bootPartitionsHoldSameImage(bootPartition1, bootPartition2 string) (bool, error) { + path1, err := l.bootPartitionPath(bootPartition1) + if err != nil { + return false, err + } + path2, err := l.bootPartitionPath(bootPartition2) + if err != nil { + return false, err + } + + return osutil.FilesAreEqual(path1, path2), nil +} + +// repairDuplicateKernelBootPartitions repairs a boot image matrix in which the +// same kernel revision is recorded in more than one boot image partition. +// +// Such a matrix is the result of a bug in which finding a free boot image +// partition for an already installed kernel revision could return a different, +// seemingly free boot image partition rather than the one the kernel revision +// was already assigned to, after which the kernel revision was assigned to both. +// This wastes the boot image partition that the next kernel refresh needs, and +// once every boot image partition is taken by the currently installed kernel no +// free boot image partition can be found at all, permanently breaking kernel +// refreshes on the device. +// +// A kernel revision named by neither snap_kernel nor snap_try_kernel is not +// used to boot the device. The bootloader never searches the matrix for it, so +// no boot image partition it points at can be selected, and the redundant +// references can be dropped without comparing the boot image partitions at all. +// +// For a kernel revision that is referenced, the redundant references are only +// cleared once the boot image partitions have been confirmed to hold identical +// content, so that a reference is never dropped while it is the only record of +// where a distinct boot image actually lives - clearing such a reference would +// leave a boot image partition the bootloader can no longer find, and would let +// a subsequent extraction overwrite a boot image that is still needed. The +// reference in the boot image partition appearing first in the matrix is kept, +// matching the partition that GetKernelBootPartition() resolves the kernel +// revision to. +func (l *lk) repairDuplicateKernelBootPartitions(env *lkenv.Env) error { + duplicates, err := env.DuplicateKernelBootPartitions() + if err != nil { + return err + } + if len(duplicates) == 0 { + return nil + } + + repaired := false + for kernel, bootPartitions := range duplicates { + // a kernel revision that nothing boots from cannot be booted from + // whichever boot image partition it is recorded in, so the redundant + // references can go regardless of what the boot image partitions hold + referenced := env.IsKernelReferenced(kernel) + + keep := bootPartitions[0] + for _, drop := range bootPartitions[1:] { + if referenced { + // only drop the redundant reference if both boot image + // partitions really do hold the same boot image, otherwise the + // reference is the only record of a distinct boot image that is + // still referenced for booting and dropping it would lose it + same, err := l.bootPartitionsHoldSameImage(keep, drop) + if err != nil { + return err + } + if !same { + logger.Noticef("cannot repair lk boot image matrix: kernel %s is recorded in boot image partitions %s and %s but their contents differ", kernel, keep, drop) + continue + } + + logger.Noticef("repairing lk boot image matrix: kernel %s is recorded in both boot image partitions %s and %s, freeing %s", kernel, keep, drop, drop) + } else { + logger.Noticef("repairing lk boot image matrix: unreferenced kernel %s is recorded in both boot image partitions %s and %s, freeing %s", kernel, keep, drop, drop) + } + + if err := env.ClearKernelBootPartition(drop); err != nil { + return err + } + repaired = true + } + } + + if !repaired { + return nil + } + + return env.Save() +} + // ExtractKernelAssets extract kernel assets per bootloader specifics // lk bootloader requires boot partition to hold valid boot image // there are two boot partition available, one holding current bootimage @@ -428,6 +532,15 @@ func (l *lk) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error { return err } + // a device that was updated by a snapd carrying the duplicate boot image + // partition bug may have the same kernel revision recorded in more than one + // boot image partition, which leaves no free boot image partition for this + // kernel and would make FindFreeKernelBootPartition() below fail forever. + // Repair the boot image matrix first so such a device can be updated again. + if err := l.repairDuplicateKernelBootPartitions(env); err != nil { + return err + } + bootPartition, err := env.FindFreeKernelBootPartition(blobName) if err != nil { return err @@ -459,16 +572,9 @@ func (l *lk) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error { return fmt.Errorf("cannot open unpacked %s: %v", bootImg, err) } defer bif.Close() - var bpart string - // TODO: for RoleSole bootloaders this will eventually be the same - // codepath as for non-RoleSole bootloader - if l.role == RoleSole { - bpart = filepath.Join(l.dir(), bootPartition) - } else { - bpart, _, err = l.devPathForPartName(bootPartition) - if err != nil { - return err - } + bpart, err := l.bootPartitionPath(bootPartition) + if err != nil { + return err } bpf, err := os.OpenFile(bpart, os.O_WRONLY, 0660) diff --git a/bootloader/lk_test.go b/bootloader/lk_test.go index 404748e32f6..badef2e38c2 100644 --- a/bootloader/lk_test.go +++ b/bootloader/lk_test.go @@ -36,6 +36,7 @@ import ( "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" ) type lkTestSuite struct { @@ -495,6 +496,317 @@ func (s *lkTestSuite) TestExtractKernelAssetsUnpacksAndRemoveInRuntimeModeUC20(c c.Assert(logbuf.String(), Equals, "") } +// wedgeBootImageMatrix puts the boot image matrix into the state left behind by +// the duplicate boot image partition bug: the same kernel revision recorded in +// both boot_a and boot_b, which leaves no free boot image partition. The kernel +// is pointed at by snap_kernel, i.e. it is referenced for booting, unless +// referenced is false. It returns the paths to the boot_a and boot_b partitions. +func (s *lkTestSuite) wedgeBootImageMatrix(c *C, kernel string, referenced bool) (env *lkenv.Env, bootA, bootB string) { + disk, err := disks.DiskFromDeviceName("lk-boot-disk") + c.Assert(err, IsNil) + + partUUIDFor := func(label string) string { + partUUID, err := disk.FindMatchingPartitionUUIDWithPartLabel(label) + c.Assert(err, IsNil) + return filepath.Join(s.rootdir, "/dev/disk/by-partuuid", partUUID) + } + + env = lkenv.NewEnv(partUUIDFor("snapbootsel"), "", lkenv.V2Run) + c.Assert(env.Load(), IsNil) + c.Assert(env.SetBootPartitionKernel("boot_a", kernel), IsNil) + c.Assert(env.SetBootPartitionKernel("boot_b", kernel), IsNil) + if referenced { + env.Set("snap_kernel", kernel) + } + c.Assert(env.Save(), IsNil) + + return env, partUUIDFor("boot_a"), partUUIDFor("boot_b") +} + +func (s *lkTestSuite) TestExtractKernelAssetsRepairsDuplicateBootPartitions(c *C) { + logbuf, r := logger.MockLogger() + defer r() + + opts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + } + r = bootloader.MockLkFiles(c, s.rootdir, opts) + defer r() + lk := bootloader.NewLk(s.rootdir, opts) + c.Assert(lk, NotNil) + + env, bootA, bootB := s.wedgeBootImageMatrix(c, "ubuntu-kernel_42.snap", true) + + // both boot image partitions hold the same boot image, as they would after + // the duplicate was created by re-extracting the same kernel + c.Assert(os.WriteFile(bootA, []byte("kernel 42 boot image"), 0755), IsNil) + c.Assert(os.WriteFile(bootB, []byte("kernel 42 boot image"), 0755), IsNil) + + // a device in this state cannot find a free boot image partition + _, err := env.FindFreeKernelBootPartition("ubuntu-kernel_43.snap") + c.Assert(err, ErrorMatches, "cannot find free boot image partition") + + // extracting a new kernel repairs the matrix and then succeeds + files := [][]string{ + {"boot.img", "kernel 43 boot image"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(43), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + c.Assert(lk.ExtractKernelAssets(info, snapf), IsNil) + + c.Check(logbuf.String(), testutil.Contains, "repairing lk boot image matrix: kernel ubuntu-kernel_42.snap is recorded in both boot image partitions boot_a and boot_b, freeing boot_b") + + // the old kernel keeps the first of the two boot image partitions, and the + // new kernel got the freed one + c.Assert(env.Load(), IsNil) + bootPart, err := env.GetKernelBootPartition("ubuntu-kernel_42.snap") + c.Assert(err, IsNil) + c.Check(bootPart, Equals, "boot_a") + bootPart, err = env.GetKernelBootPartition("ubuntu-kernel_43.snap") + c.Assert(err, IsNil) + c.Check(bootPart, Equals, "boot_b") + + // and the new boot image really was written to the freed partition + content, err := os.ReadFile(bootB) + c.Assert(err, IsNil) + c.Check(string(content), Equals, "kernel 43 boot image") + + // the duplicate is gone for good + duplicates, err := env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil) + c.Check(duplicates, HasLen, 0) +} + +func (s *lkTestSuite) TestExtractKernelAssetsRepairsUnreferencedDuplicateWithDifferingContent(c *C) { + logbuf, r := logger.MockLogger() + defer r() + + opts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + } + r = bootloader.MockLkFiles(c, s.rootdir, opts) + defer r() + lk := bootloader.NewLk(s.rootdir, opts) + c.Assert(lk, NotNil) + + // nothing points at the duplicated kernel, so the bootloader never looks it + // up in the matrix and neither of the boot image partitions it is recorded + // in can be selected for booting + env, bootA, bootB := s.wedgeBootImageMatrix(c, "ubuntu-kernel_42.snap", false) + c.Check(env.IsKernelReferenced("ubuntu-kernel_42.snap"), Equals, false) + + // the boot image partitions hold different content, which for a referenced + // kernel would block the repair, but here there is no boot image to lose + c.Assert(os.WriteFile(bootA, []byte("some boot image"), 0755), IsNil) + c.Assert(os.WriteFile(bootB, []byte("a different boot image"), 0755), IsNil) + + files := [][]string{ + {"boot.img", "kernel 43 boot image"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(43), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + c.Assert(lk.ExtractKernelAssets(info, snapf), IsNil) + + c.Check(logbuf.String(), testutil.Contains, "repairing lk boot image matrix: unreferenced kernel ubuntu-kernel_42.snap is recorded in both boot image partitions boot_a and boot_b, freeing boot_b") + + // the new kernel got a boot image partition and the duplicate is gone + c.Assert(env.Load(), IsNil) + bootPart, err := env.GetKernelBootPartition("ubuntu-kernel_43.snap") + c.Assert(err, IsNil) + duplicates, err := env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil) + c.Check(duplicates, HasLen, 0) + + // the repair dropped the redundant reference rather than leaving it for the + // extraction to overwrite: both references to the unreferenced kernel are + // gone, one cleared by the repair and one reused for the new kernel + _, err = env.GetKernelBootPartition("ubuntu-kernel_42.snap") + c.Check(err, ErrorMatches, `cannot find kernel "ubuntu-kernel_42.snap": no boot image partition has value "ubuntu-kernel_42.snap"`) + + // and the new boot image really was written to it + bootPartPath := map[string]string{"boot_a": bootA, "boot_b": bootB}[bootPart] + c.Assert(bootPartPath, Not(Equals), "") + content, err := os.ReadFile(bootPartPath) + c.Assert(err, IsNil) + c.Check(string(content), Equals, "kernel 43 boot image") +} + +func (s *lkTestSuite) TestExtractKernelAssetsDoesNotRepairDuplicateWithDifferingContent(c *C) { + logbuf, r := logger.MockLogger() + defer r() + + opts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + } + r = bootloader.MockLkFiles(c, s.rootdir, opts) + defer r() + lk := bootloader.NewLk(s.rootdir, opts) + c.Assert(lk, NotNil) + + env, bootA, bootB := s.wedgeBootImageMatrix(c, "ubuntu-kernel_42.snap", true) + + // the boot image partitions hold *different* content, so one of the two + // references is the only record of a distinct boot image and must not be + // dropped - clearing it would leave a boot image the bootloader can no + // longer find and let the extraction below overwrite it + c.Assert(os.WriteFile(bootA, []byte("some boot image"), 0755), IsNil) + c.Assert(os.WriteFile(bootB, []byte("a different boot image"), 0755), IsNil) + + files := [][]string{ + {"boot.img", "kernel 43 boot image"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(43), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + // the extraction fails rather than silently discarding a boot image + err = lk.ExtractKernelAssets(info, snapf) + c.Assert(err, ErrorMatches, "cannot find free boot image partition") + + c.Check(logbuf.String(), testutil.Contains, "cannot repair lk boot image matrix: kernel ubuntu-kernel_42.snap is recorded in boot image partitions boot_a and boot_b but their contents differ") + + // both boot image partitions are left exactly as they were + content, err := os.ReadFile(bootA) + c.Assert(err, IsNil) + c.Check(string(content), Equals, "some boot image") + content, err = os.ReadFile(bootB) + c.Assert(err, IsNil) + c.Check(string(content), Equals, "a different boot image") + + // and the matrix is untouched + c.Assert(env.Load(), IsNil) + duplicates, err := env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil) + c.Check(duplicates, DeepEquals, map[string][]string{ + "ubuntu-kernel_42.snap": {"boot_a", "boot_b"}, + }) +} + +func (s *lkTestSuite) TestExtractKernelAssetsDoesNotRepairTryKernelWithDifferingContent(c *C) { + logbuf, r := logger.MockLogger() + defer r() + + opts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + } + r = bootloader.MockLkFiles(c, s.rootdir, opts) + defer r() + lk := bootloader.NewLk(s.rootdir, opts) + c.Assert(lk, NotNil) + + // the duplicated kernel is the try kernel of an ongoing refresh rather than + // the current one, which makes it just as referenced for booting: leave + // snap_kernel unset so that only snap_try_kernel can account for it + env, bootA, bootB := s.wedgeBootImageMatrix(c, "ubuntu-kernel_42.snap", false) + env.Set("snap_try_kernel", "ubuntu-kernel_42.snap") + c.Assert(env.Save(), IsNil) + c.Check(env.IsKernelReferenced("ubuntu-kernel_42.snap"), Equals, true) + + c.Assert(os.WriteFile(bootA, []byte("some boot image"), 0755), IsNil) + c.Assert(os.WriteFile(bootB, []byte("a different boot image"), 0755), IsNil) + + files := [][]string{ + {"boot.img", "kernel 43 boot image"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(43), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + // note that the extraction itself still succeeds: it takes boot_a because + // FindFreeKernelBootPartition() only reserves snap_kernel, which is a + // separate pre-existing problem. What matters here is that the repair did + // not drop the reference from boot_b on the way + c.Assert(lk.ExtractKernelAssets(info, snapf), IsNil) + + c.Check(logbuf.String(), testutil.Contains, "cannot repair lk boot image matrix: kernel ubuntu-kernel_42.snap is recorded in boot image partitions boot_a and boot_b but their contents differ") + + // the try kernel is still recorded in the boot image partition holding its + // boot image, so the bootloader can still find it + c.Assert(env.Load(), IsNil) + bootPart, err := env.GetKernelBootPartition("ubuntu-kernel_42.snap") + c.Assert(err, IsNil) + c.Check(bootPart, Equals, "boot_b") + content, err := os.ReadFile(bootB) + c.Assert(err, IsNil) + c.Check(string(content), Equals, "a different boot image") +} + +func (s *lkTestSuite) TestExtractKernelAssetsNoRepairNeeded(c *C) { + logbuf, r := logger.MockLogger() + defer r() + + opts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + } + r = bootloader.MockLkFiles(c, s.rootdir, opts) + defer r() + lk := bootloader.NewLk(s.rootdir, opts) + c.Assert(lk, NotNil) + + files := [][]string{ + {"boot.img", "kernel 42 boot image"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + // extracting into a healthy matrix must not log any repair, and in + // particular must not be confused by the two boot image partitions both + // being empty + c.Assert(lk.ExtractKernelAssets(info, snapf), IsNil) + c.Check(logbuf.String(), Equals, "") + + // re-extracting the very same kernel reuses its partition rather than + // creating a duplicate + c.Assert(lk.ExtractKernelAssets(info, snapf), IsNil) + c.Check(logbuf.String(), Equals, "") + + disk, err := disks.DiskFromDeviceName("lk-boot-disk") + c.Assert(err, IsNil) + partUUID, err := disk.FindMatchingPartitionUUIDWithPartLabel("snapbootsel") + c.Assert(err, IsNil) + env := lkenv.NewEnv(filepath.Join(s.rootdir, "/dev/disk/by-partuuid", partUUID), "", lkenv.V2Run) + c.Assert(env.Load(), IsNil) + + duplicates, err := env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil) + c.Check(duplicates, HasLen, 0) +} + func (s *lkTestSuite) TestExtractRecoveryKernelAssetsAtRuntime(c *C) { opts := &bootloader.Options{ // as called when creating a recovery system at runtime diff --git a/bootloader/lkenv/lkenv.go b/bootloader/lkenv/lkenv.go index 87bc26bf08f..f09e9f6a0cf 100644 --- a/bootloader/lkenv/lkenv.go +++ b/bootloader/lkenv/lkenv.go @@ -424,6 +424,74 @@ func (l *Env) RemoveKernelFromBootPartition(kernel string) error { return matr.dropBootPartValue(kernel) } +// DuplicateKernelBootPartitions returns, for every kernel revision that is +// referenced by more than one boot image partition, the labels of all the boot +// image partitions referencing it, in boot image matrix order. An empty result +// means the boot image matrix is consistent. +// +// A kernel revision occupying more than one boot image partition is always +// corruption - it wastes the slot that the next kernel refresh needs, and once +// every slot is taken by the currently installed kernel no free boot image +// partition can be found at all and kernel refreshes fail permanently. It is +// left to the caller to decide whether the redundant references can safely be +// cleared, which requires checking that the boot image partitions really do +// hold the same content - see ClearKernelBootPartition. +func (l *Env) DuplicateKernelBootPartitions() (map[string][]string, error) { + matr, err := l.variant.bootImgKernelMatrix() + if err != nil { + return nil, err + } + + return matr.duplicateBootPartValues(), nil +} + +// ClearKernelBootPartition removes the kernel revision reference from the +// named boot image partition, leaving the boot image partition label itself +// intact. Unlike RemoveKernelFromBootPartition, which looks the partition up by +// the kernel revision it holds and clears the first match, this clears exactly +// the boot image partition named, which is what is needed to resolve a kernel +// revision that is referenced by more than one boot image partition. It returns +// a non-nil error if the boot image partition label was not found. +func (l *Env) ClearKernelBootPartition(bootpart string) error { + matr, err := l.variant.bootImgKernelMatrix() + if err != nil { + return err + } + + return matr.clearBootPart(bootpart) +} + +// kernelVariables are the environment variables through which the bootloader is +// directed at a kernel revision in the boot image matrix. A kernel revision +// named by one of them is referenced for booting, any other kernel revision in +// the matrix is not. +var kernelVariables = []string{ + "snap_kernel", + "snap_try_kernel", +} + +// IsKernelReferenced returns whether the given kernel revision is named by +// snap_kernel or snap_try_kernel, i.e. whether it is referenced for booting the +// device. +// +// A kernel revision named by neither is not used to boot: the bootloader never +// searches the boot image matrix for it, so the boot image partition it points +// at cannot be selected and its reference can be dropped without affecting how +// the device boots. +func (l *Env) IsKernelReferenced(kernel string) bool { + if kernel == "" { + return false + } + + for _, key := range kernelVariables { + if l.variant.get(key) == kernel { + return true + } + } + + return false +} + // FindFreeRecoverySystemBootPartition finds a free recovery system boot image // partition to be used for the recovery kernel from the recovery system. It // only considers boot image partitions that are currently not set to a recovery @@ -537,6 +605,49 @@ func (matr bootimgMatrixGeneric) dropBootPartValue(bootPartValue string) error { return fmt.Errorf("cannot find %q in boot image partitions", bootPartValue) } +// clearBootPart removes the value from the named boot image partition, leaving +// the boot image partition label itself intact. Unlike dropBootPartValue, the +// boot image partition is identified by its label rather than by the value it +// holds, so it can address one specific partition out of several holding the +// same value. +func (matr bootimgMatrixGeneric) clearBootPart(bootpart string) error { + for x := range matr { + if bootpart == cToGoString(matr[x][MATRIX_ROW_PARTITION][:]) { + // clear the string by setting the first element to 0 or NUL + matr[x][MATRIX_ROW_VALUE][0] = 0 + return nil + } + } + + return fmt.Errorf("cannot find boot image partition %s", bootpart) +} + +// duplicateBootPartValues returns every value that is referenced by more than +// one boot image partition, mapped to the labels of all the boot image +// partitions referencing it, in boot image matrix order. +func (matr bootimgMatrixGeneric) duplicateBootPartValues() map[string][]string { + bootPartsForValue := make(map[string][]string) + for x := range matr { + bootPartLabel := cToGoString(matr[x][MATRIX_ROW_PARTITION][:]) + if bootPartLabel == "" { + continue + } + bootPartValue := cToGoString(matr[x][MATRIX_ROW_VALUE][:]) + if bootPartValue == "" { + continue + } + bootPartsForValue[bootPartValue] = append(bootPartsForValue[bootPartValue], bootPartLabel) + } + + for bootPartValue, bootPartLabels := range bootPartsForValue { + if len(bootPartLabels) < 2 { + delete(bootPartsForValue, bootPartValue) + } + } + + return bootPartsForValue +} + // setBootPart associates the specified boot image partition label to the // specified value. func (matr bootimgMatrixGeneric) setBootPart(bootpart, bootPartValue string) error { diff --git a/bootloader/lkenv/lkenv_test.go b/bootloader/lkenv/lkenv_test.go index e0dec0ef9a0..b7ae92751a7 100644 --- a/bootloader/lkenv/lkenv_test.go +++ b/bootloader/lkenv/lkenv_test.go @@ -808,6 +808,172 @@ func (l *lkenvTestSuite) TestGetAndSetAndFindBootPartition(c *C) { } } +func (l *lkenvTestSuite) TestDuplicateKernelBootPartitions(c *C) { + tt := []struct { + version lkenv.Version + comment string + }{ + {lkenv.V1, "v1"}, + {lkenv.V2Run, "v2 run"}, + } + + for _, t := range tt { + comment := Commentf(t.comment) + + env := lkenv.NewEnv(l.envPath, "", t.version) + c.Assert(env, NotNil, comment) + err := env.InitializeBootPartitions("boot_a", "boot_b") + c.Assert(err, IsNil, comment) + + // an empty matrix has no duplicates - in particular the two unset + // values must not be reported as duplicates of each other + duplicates, err := env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil, comment) + c.Check(duplicates, HasLen, 0, comment) + + // neither do two distinct kernels + err = env.SetBootPartitionKernel("boot_a", "kernel-1") + c.Assert(err, IsNil, comment) + err = env.SetBootPartitionKernel("boot_b", "kernel-2") + c.Assert(err, IsNil, comment) + duplicates, err = env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil, comment) + c.Check(duplicates, HasLen, 0, comment) + + // the same kernel in both boot image partitions is a duplicate, and is + // reported in boot image matrix order + err = env.SetBootPartitionKernel("boot_b", "kernel-1") + c.Assert(err, IsNil, comment) + duplicates, err = env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil, comment) + c.Check(duplicates, DeepEquals, map[string][]string{ + "kernel-1": {"boot_a", "boot_b"}, + }, comment) + + // clearing one of them by label resolves the duplicate and leaves the + // other reference alone + err = env.ClearKernelBootPartition("boot_b") + c.Assert(err, IsNil, comment) + duplicates, err = env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil, comment) + c.Check(duplicates, HasLen, 0, comment) + bootPart, err := env.GetKernelBootPartition("kernel-1") + c.Assert(err, IsNil, comment) + c.Check(bootPart, Equals, "boot_a", comment) + + // clearing an unknown boot image partition is an error + err = env.ClearKernelBootPartition("boot_c") + c.Assert(err, ErrorMatches, "cannot find boot image partition boot_c", comment) + } +} + +func (l *lkenvTestSuite) TestDuplicateKernelBootPartitionsAllSlots(c *C) { + env := lkenv.NewEnv(l.envPath, "", lkenv.V2Run) + c.Assert(env, NotNil) + c.Assert(env.InitializeBootPartitions("boot_a", "boot_b"), IsNil) + + // the fully wedged state: the currently installed kernel occupies every + // boot image partition, so no free one can be found at all + c.Assert(env.SetBootPartitionKernel("boot_a", "kernel-1"), IsNil) + c.Assert(env.SetBootPartitionKernel("boot_b", "kernel-1"), IsNil) + env.Set("snap_kernel", "kernel-1") + + _, err := env.FindFreeKernelBootPartition("kernel-2") + c.Assert(err, ErrorMatches, "cannot find free boot image partition") + + duplicates, err := env.DuplicateKernelBootPartitions() + c.Assert(err, IsNil) + c.Assert(duplicates, DeepEquals, map[string][]string{ + "kernel-1": {"boot_a", "boot_b"}, + }) + + // freeing the redundant reference unwedges the device + c.Assert(env.ClearKernelBootPartition("boot_b"), IsNil) + bootPart, err := env.FindFreeKernelBootPartition("kernel-2") + c.Assert(err, IsNil) + c.Check(bootPart, Equals, "boot_b") +} + +func (l *lkenvTestSuite) TestDuplicateKernelBootPartitionsV2RecoveryUnsupported(c *C) { + env := lkenv.NewEnv(l.envPath, "", lkenv.V2Recovery) + c.Assert(env, NotNil) + + _, err := env.DuplicateKernelBootPartitions() + c.Assert(err, ErrorMatches, "internal error: v2 recovery lkenv has no boot image partition kernel matrix") + + err = env.ClearKernelBootPartition("boot_ra") + c.Assert(err, ErrorMatches, "internal error: v2 recovery lkenv has no boot image partition kernel matrix") +} + +func (l *lkenvTestSuite) TestIsKernelReferenced(c *C) { + tt := []struct { + version lkenv.Version + // the kernel variables this lkenv version has, if any - a v2 recovery + // environment has no kernel matrix and therefore neither of them + keys []string + // the recovery system variables, which name recovery systems rather + // than kernel revisions and must never be considered + otherKeys []string + comment string + }{ + { + lkenv.V1, + []string{"snap_kernel", "snap_try_kernel"}, + []string{"snapd_recovery_system", "try_recovery_system"}, + "v1", + }, + { + lkenv.V2Run, + []string{"snap_kernel", "snap_try_kernel"}, + []string{"snapd_recovery_system", "try_recovery_system"}, + "v2 run", + }, + { + lkenv.V2Recovery, + nil, + []string{"snapd_recovery_system", "try_recovery_system"}, + "v2 recovery", + }, + } + + for _, t := range tt { + comment := Commentf(t.comment) + + env := lkenv.NewEnv(l.envPath, "", t.version) + c.Assert(env, NotNil, comment) + + // nothing is referenced in a pristine environment, and the empty kernel + // revision is never referenced even though every variable reads back + // empty + c.Check(env.IsKernelReferenced(""), Equals, false, comment) + c.Check(env.IsKernelReferenced("kernel-1"), Equals, false, comment) + + for _, key := range t.keys { + env.Set(key, "kernel-1") + c.Check(env.IsKernelReferenced("kernel-1"), Equals, true, + Commentf("%s: %s", t.comment, key)) + // an unrelated kernel revision is still not referenced + c.Check(env.IsKernelReferenced("kernel-2"), Equals, false, + Commentf("%s: %s", t.comment, key)) + // and the empty kernel revision stays unreferenced + c.Check(env.IsKernelReferenced(""), Equals, false, + Commentf("%s: %s", t.comment, key)) + env.Set(key, "") + c.Check(env.IsKernelReferenced("kernel-1"), Equals, false, + Commentf("%s: %s", t.comment, key)) + } + + // a recovery system variable never makes a kernel revision look + // referenced, whether or not this lkenv version has it + for _, key := range t.otherKeys { + env.Set(key, "kernel-1") + c.Check(env.IsKernelReferenced("kernel-1"), Equals, false, + Commentf("%s: %s", t.comment, key)) + env.Set(key, "") + } + } +} + func (l *lkenvTestSuite) TestV1NoRecoverySystemSupport(c *C) { env := lkenv.NewEnv(l.envPath, "", lkenv.V1) c.Assert(env, NotNil)