Skip to content

bootloader/lkenv: fix same kernel occupying multiple boot partitions - #17420

Open
kubiko wants to merge 1 commit into
canonical:masterfrom
kubiko:bootloader-lk-fix-slot-search
Open

bootloader/lkenv: fix same kernel occupying multiple boot partitions#17420
kubiko wants to merge 1 commit into
canonical:masterfrom
kubiko:bootloader-lk-fix-slot-search

Conversation

@kubiko

@kubiko kubiko commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

findFreeBootPartition() checked whether the requested value was already assigned to a boot image partition inside the same loop that looks for a free one. Whichever case matched first in boot image matrix order won, so when an earlier partition looked free, it was returned without ever examining the later partition that already held the value. The caller then went on to assign the value to that partition too, leaving the very same kernel revision recorded in both:

  bootimg_matrix [boot-a][pi-kernel_387.snap]
  bootimg_matrix [boot-b][pi-kernel_387.snap]

Why it isn't hit every time

The guard therefore only worked while the value sat in the first row of the matrix. That is always the case at image build time, which is the situation the existing comment describes, but not in the steady state at run time, where the installed kernel alternates between boot image partitions on every refresh. Re-extracting the assets of the currently installed kernel - on a revert, on undo of a failed refresh or when re-seeding - then duplicates it whenever it happens to sit in a later row(b slot), which is why a device can be updated a number of times before this shows up.

Impact

The consequence is permanent. With the same kernel in every boot image partition and snap_kernel reserved, no free boot image partition can be found at all and every subsequent kernel refresh fails with

"cannot find free boot image partition".

Fix

Split the check into its own pass over the whole matrix, before any partition is considered free, so that a value which is already assigned always resolves to the partition holding it regardless of its position. The pre-pass keeps its own empty label check so an empty value can never match an unset row.

This affects the kernel matrix of v1 and v2 run environments, and the recovery system matrix of v2 recovery environments, where it leaks a boot image partition per duplicate rather than wedging the device, because all assigned recovery systems are reserved.

Tests

Two tests added to stress this scenario, fails without the fix, pass when fix is applied.

Note

This stops new duplicates from appearing, but does not recover devices which already hit this scenario.
This is addressed in a separate PR: #17419

findFreeBootPartition() checked whether the requested value was already
assigned to a boot image partition inside the same loop that looks for a
free one. Whichever case matched first in boot image matrix order won, so
when an earlier partition looked free it was returned without ever
examining the later partition that already held the value. The caller
then went on to assign the value to that partition too, leaving the very
same kernel revision recorded in both:

  bootimg_matrix [boa][pi-kernel_387.snap]
  bootimg_matrix [bob][pi-kernel_387.snap]

The guard therefore only worked while the value sat in the first row of
the matrix. That is always the case at image build time, which is the
situation the existing comment describes, but not in the steady state at
run time, where the installed kernel alternates between boot image
partitions on every refresh. Re-extracting the assets of the currently
installed kernel - on a revert, on undo of a failed refresh or when
re-seeding - then duplicates it whenever it happens to sit in a later
row, which is why a device can be updated a number of times before this
shows up.

The consequence is permanent. With the same kernel in every boot image
partition and snap_kernel reserved, no free boot image partition can be
found at all and every subsequent kernel refresh fails with "cannot find
free boot image partition".

Split the check into its own pass over the whole matrix, before any
partition is considered free, so that a value which is already assigned
always resolves to the partition holding it regardless of its position.
The pre-pass keeps its own empty label check so an empty value can never
match an unset row.

This affects the kernel matrix of v1 and v2 run environments, and the
recovery system matrix of v2 recovery environments, where it leaks a boot
image partition per duplicate rather than wedging the device, because all
assigned recovery systems are reserved.

The bug has been present since the lk bootloader was introduced in 2.42.

Signed-off-by: Ondrej Kubik <ondrej.kubik@canonical.com>
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.81%. Comparing base (d7c520b) to head (3ae2f96).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #17420   +/-   ##
=======================================
  Coverage   78.81%   78.81%           
=======================================
  Files        1406     1408    +2     
  Lines      197084   197103   +19     
  Branches     2498     2498           
=======================================
+ Hits       155330   155350   +20     
- Misses      32387    32396    +9     
+ Partials     9367     9357   -10     
Flag Coverage Δ
unittests 78.81% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown

Thu Jul 30 18:07:41 UTC 2026
The following results are from: https://github.com/canonical/snapd/actions/runs/30557037414

Failures:

Preparing:

  • openstack-ext:ubuntu-18.04-64:tests/nested/classic/hotplug
  • openstack-ext:ubuntu-18.04-64:tests/nested/classic/activate-state
  • openstack-ext:ubuntu-22.04-64:tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs:core
  • openstack-ext:ubuntu-22.04-64:tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs:snapd
  • openstack-arm-ext:ubuntu-24.04-arm-64:tests/nested/manual/optee-fde:optee
  • openstack-arm-ext:ubuntu-24.04-arm-64:tests/nested/manual/optee-fde:hooks
  • openstack-arm-ext:ubuntu-24.04-arm-64:tests/nested/core/core20-basic
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs:core
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs:snapd

Executing:

  • openstack-ext:ubuntu-20.04-64:tests/nested/core/core20-tpm
  • openstack-ext:ubuntu-20.04-64:tests/nested/manual/core-factory-reset-new-secboot:tpm
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/remodel-to-installed-kernel
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/muinstaller-real:plain
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/kernel-modules-components:encrypted
  • openstack-ext:ubuntu-26.04-64:tests/nested/core/core20-kernel-reseal
  • openstack-ext:ubuntu-26.04-64:tests/nested/core/core20-factory-reset:fde
  • openstack:ubuntu-core-18-64:tests/core/gadget-update-pc
  • openstack:ubuntu-core-18-64:tests/core/remodel-base
  • openstack:ubuntu-core-18-64:tests/main/security-device-cgroups-serial-port
  • openstack:ubuntu-core-18-64:tests/main/interfaces-devlxd:vm
  • openstack:ubuntu-core-18-64:tests/main/interfaces-raw-usb
  • openstack:ubuntu-26.04-64:tests/main/auto-refresh-pre-download:ignore

Restoring:

  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/kernel-modules-components:encrypted
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/
  • openstack-ext:ubuntu-24.04-64:
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/muinstaller-real:plain
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/
  • openstack-ext:ubuntu-24.04-64:
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/remodel-to-installed-kernel
  • openstack-ext:ubuntu-24.04-64:tests/nested/manual/
  • openstack-ext:ubuntu-24.04-64:
  • openstack:ubuntu-core-18-64:tests/core/gadget-update-pc
  • openstack:ubuntu-core-18-64:tests/core/
  • openstack:ubuntu-core-18-64:
  • openstack:ubuntu-core-18-64:tests/main/cgroup-tracking-failure
  • openstack:ubuntu-core-18-64:tests/core/remodel-base
  • openstack:ubuntu-core-18-64:tests/core/
  • openstack:ubuntu-core-18-64:
  • openstack:ubuntu-core-18-64:tests/main/interfaces-system-dbus

Skipped tests from snapd-testing-skip

If you wish to have any of the below tests run in your PR, in your PR description, add 'unskip:' followed by a copy-and-pasted list of the below tests you wish to run (unskip plus test list must be valid yaml)

  • openstack:ubuntu-24.04-64:tests/main/i18n
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-26.04-64:tests/main/i18n
  • openstack:ubuntu-26.04-64:tests/main/interfaces-requests-activates-handlers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant