From 92eef5592582a55340daf66706a242a408ca06ad Mon Sep 17 00:00:00 2001 From: Ondrej Kubik Date: Thu, 30 Jul 2026 16:45:44 +0300 Subject: [PATCH] bootloader/lkenv,lk: recover devices wedged by duplicated boot partitions A device updated by a snapd carrying the duplicate boot image partition bug can be left with the same kernel revision recorded in more than one boot image partition. Fixing the assignment logic stops new duplicates from appearing but does not help such a device: the duplicate is persisted in the snapbootsel partition, no free boot image partition can be found any more and kernel refreshes keep failing. Nothing in the existing code repairs it - RemoveKernelFromBootPartition() looks a partition up by the kernel revision it holds and clears the first match only, so even removing the kernel leaves the second reference behind. Add DuplicateKernelBootPartitions(), reporting each kernel revision that is referenced by more than one boot image partition together with all the partitions referencing it, and ClearKernelBootPartition(), clearing the value of a boot image partition addressed by label rather than by the value it holds, which is what is needed to clear one specific partition out of several holding the same value. Use both from lk.ExtractKernelAssets() to repair the boot image matrix before looking for a free boot image partition, so an affected device recovers on its next kernel refresh with no manual intervention. How much has to be established before a redundant reference can be cleared depends on whether the kernel revision is referenced for booting at all. Add IsKernelReferenced() to report whether a kernel revision is named by snap_kernel or snap_try_kernel. These are the only two variables that direct the bootloader at the kernel matrix; the recovery system variables name recovery systems in a separate matrix that this repair never touches, so they are deliberately not considered. A kernel revision named by neither 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 - there is no boot image to lose. This matters in practice, because the revision left duplicated is frequently one that has already been superseded. For a kernel revision that is referenced, a redundant reference is only cleared once the two boot image partitions have been confirmed to hold identical content. If they differ, then the reference is the only record of where a distinct boot image lives: clearing it would leave a boot image the bootloader can no longer find and would let the extraction that follows overwrite an image that is still needed. In that case the matrix is left untouched and the extraction fails as before rather than silently discarding a boot image. The reference in the partition appearing first in the matrix is the one kept, matching the partition that GetKernelBootPartition() resolves the kernel revision to, so the bootloader's view of the device does not change. Repair is wired into the kernel path only. The recovery system matrix can duplicate as well, but it reserves every assigned recovery system, so it leaks a boot image partition instead of wedging the device, and the boot images of two distinct recovery systems may legitimately be identical, which makes the content check unsafe as a criterion there. While at it, factor the boot image partition path resolution that ExtractKernelAssets() already did out into bootPartitionPath(), as the repair needs the same lookup. Signed-off-by: Ondrej Kubik --- bootloader/lk.go | 126 +++++++++++-- bootloader/lk_test.go | 312 +++++++++++++++++++++++++++++++++ bootloader/lkenv/lkenv.go | 111 ++++++++++++ bootloader/lkenv/lkenv_test.go | 166 ++++++++++++++++++ 4 files changed, 705 insertions(+), 10 deletions(-) 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)