Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package sdbootutil for openSUSE:Factory 
checked in at 2026-09-30 16:21:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/sdbootutil (Old)
 and      /work/SRC/openSUSE:Factory/.sdbootutil.new.1465845 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "sdbootutil"

Wed Sep 30 16:21:53 2026 rev:113 rq:1381547 version:1+git20260929.26b6989

Changes:
--------
--- /work/SRC/openSUSE:Factory/sdbootutil/sdbootutil.changes    2026-09-29 
19:01:42.103659621 +0200
+++ /work/SRC/openSUSE:Factory/.sdbootutil.new.1465845/sdbootutil.changes       
2026-09-30 16:22:40.803416791 +0200
@@ -1,0 +2,43 @@
+Tue Sep 29 20:24:08 UTC 2026 - Alberto Planas Dominguez <[email protected]>
+
+- Update to version 1+git20260929.26b6989:
+  * Increase the timeout for the update-prediction service
+  * Keep /.snapshots mounted for the shutdown helper
+  * Serialize the update-predictions service and the shutdown helper
+  * Do not ask to fix ROOTFS when the root is encrypted
+  * Use >&2 instead of /dev/stderr
+  * Revert "Move back from oneshot the update-predictions service"
+  * Move back from oneshot the update-predictions service
+  * Fix SELinux AVC from grep redirector
+  * Use DSP for the LUKS2 swap partition
+  * Add missing tight ESP unit test scenario
+  * Don't count reused kernel when calculating free space
+
+-------------------------------------------------------------------
+Thu Sep 24 20:05:03 UTC 2026 - Alberto Planas Dominguez <[email protected]>
+
+- Update to version 1+git20260924.2b7b94e:
+  * Improve detection of encrypted device when RAID is used
+  * Support btrfs RAID1 configurations
+  * Move the service from oneshot to exec to avoid the wait
+  * Drop shift variations already present as a component
+  * Fix Supplement use of 'if' instead of 'and'
+  * Hide the warning for entries that uses @
+  * Accept _ instead of @ as snapshot prefix for version
+  * Drop chown and set ownership via install
+  * Use bootctl to generate the random seed
+  * Start the validation with the strongest bank
+  * Parse the JSON output of findmnt
+  * Use stdin for qrencode
+  * Fix log file permissions
+  * Improve PCR15 diagnosis in status command
+  * Avoid abrmd TCTI error message
+  * Do not fail if pcrlock lock verb cannot reproduce the event log
+  * The completion subpackage supplements the main one
+  * Detect when grubenv is full
+  * Use systemd-analyze to compare versions in status
+  * Fix bootcounter in GRUB2 EFI variable
+  * Drop lowercase in dd
+  * Fix loader_conf_set for paths
+
+-------------------------------------------------------------------

Old:
----
  sdbootutil-1+git20260909.7cfa1f0.obscpio

New:
----
  sdbootutil-1+git20260929.26b6989.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ sdbootutil.spec ++++++
--- /var/tmp/diff_new_pack.WBaDkZ/_old  2026-09-30 16:22:42.001466890 +0200
+++ /var/tmp/diff_new_pack.WBaDkZ/_new  2026-09-30 16:22:42.003466974 +0200
@@ -55,7 +55,7 @@
 %{nil}
 
 Name:           sdbootutil
-Version:        1+git20260909.7cfa1f0
+Version:        1+git20260929.26b6989
 Release:        0
 Summary:        Bootctl wrapper for BLS boot loaders
 License:        MIT
@@ -151,6 +151,7 @@
 Requires:       %{name} = %{version}
 Requires:       bash
 Requires:       bash-completion
+Supplements:    (%{name} and bash-completion)
 BuildArch:      noarch
 
 %description bash-completion

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.WBaDkZ/_old  2026-09-30 16:22:42.045468730 +0200
+++ /var/tmp/diff_new_pack.WBaDkZ/_new  2026-09-30 16:22:42.047468814 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/openSUSE/sdbootutil.git</param>
-              <param 
name="changesrevision">7cfa1f0ab1bba2c808ef5ff63aff22b26aa42d08</param></service></servicedata>
+              <param 
name="changesrevision">26b6989e346388bfa244b226c809a1e6a3824a0d</param></service></servicedata>
 (No newline at EOF)
 

++++++ sdbootutil-1+git20260909.7cfa1f0.obscpio -> 
sdbootutil-1+git20260929.26b6989.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sdbootutil-1+git20260909.7cfa1f0/.github/ISSUE_TEMPLATE/bug_report.yml 
new/sdbootutil-1+git20260929.26b6989/.github/ISSUE_TEMPLATE/bug_report.yml
--- old/sdbootutil-1+git20260909.7cfa1f0/.github/ISSUE_TEMPLATE/bug_report.yml  
2026-09-09 13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/.github/ISSUE_TEMPLATE/bug_report.yml  
2026-09-29 22:21:06.000000000 +0200
@@ -121,7 +121,7 @@
     attributes:
       label: Debug trace excerpts (sanitized)
       description: |
-        If you enabled the **opt‑in debug trace** (create 
`/var/log/sdbootutil.log` and set permissions to 600 before running), please 
paste only the **relevant, sanitized excerpts** here.
+        If you enabled the **opt‑in debug trace** (`sdbootutil 
--start-trace-code`, which creates `/var/log/sdbootutil.log` with mode 600), 
please paste only the **relevant, sanitized excerpts** here.
 
         ⚠️ **Privacy warning:** trace lines may include sensitive data (e.g. 
passwords typed as command arguments). **You must review and redact** any 
secrets before sharing. If in doubt, **omit** the trace and instead describe 
what you observed.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sdbootutil-1+git20260909.7cfa1f0/10-sdbootutil.snapper 
new/sdbootutil-1+git20260929.26b6989/10-sdbootutil.snapper
--- old/sdbootutil-1+git20260909.7cfa1f0/10-sdbootutil.snapper  2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/10-sdbootutil.snapper  2026-09-29 
22:21:06.000000000 +0200
@@ -94,8 +94,8 @@
 #   means that it is executed multiple times)
 #
 # * In case that a reboot or shutdown is commanded when the service is
-#   running, it will not accept the SIGTERM signal, and will require 2
-#   minutes before forcing it down
+#   running, it will not accept the SIGTERM signal, and will have 5
+#   minutes to complete before being forced down
 #
 # * Finally, if the reboot or shutdown command is launched before the
 #   timer triggers, a transient service will be executed to run the
@@ -105,6 +105,15 @@
 # * The same marker file is used to detect that there is a pending
 #   update-predictions
 #
+# * The transient service is ordered before the service, so at
+#   shutdown it is stopped only after the service has finished.  Both
+#   can be pending at once (a new snapshot while the service runs sets
+#   the marker again and re-creates the transient service), and two
+#   concurrent update-predictions are last-writer-wins: an iteration
+#   still predicting for the previous default snapshot can overwrite
+#   the new policy.  Serialized, the ExecStop only acts if the service
+#   left the marker set (it failed, or was killed), so it is the retry
+#
 # * If no device is unlocked with the TPM2 the predictions are a
 #   no-op, so no transient unit is created at all.  This is the same
 #   condition that guards sdbootutil-update-predictions.service
@@ -141,6 +150,13 @@
        # reboot: if it could not update PCR 15, the next boot halts in
        # measure-pcr-validator, and this journal entry is the only
        # warning that it was coming
+       #
+       # /.snapshots is required because update-predictions asks
+       # snapper which snapshots to include.  Being ordered after
+       # sdbootutil-update-predictions.service, this can run late in
+       # the shutdown, and without the mount snapper fails and the
+       # prediction silently loses the snapper default and the last
+       # working snapshots
        if ! systemctl --quiet is-active 
sdbootutil-update-predictions-shutdown.service; then
                systemd-run 
--unit=sdbootutil-update-predictions-shutdown.service \
                            --property=Description="Update TPM predictions 
before shutdown" \
@@ -149,8 +165,9 @@
                            --property=DefaultDependencies=no \
                            --property=Conflicts=umount.target \
                            --property=Before=umount.target \
-                           --property=RequiresMountsFor="/ /var /run /tmp 
/boot/efi" \
-                           --property=TimeoutStopSec=120 \
+                           
--property=Before=sdbootutil-update-predictions.service \
+                           --property=RequiresMountsFor="/ /.snapshots /var 
/run /tmp /boot/efi" \
+                           --property=TimeoutStopSec=300 \
                            --property=ExecStop='/bin/bash -c "[ $(systemctl 
is-system-running) = stopping ] || exit 0; [ -f 
/run/sdbootutil/update-predictions ] || exit 0; exec /usr/bin/sdbootutil -v 
update-predictions"' \
                            /bin/true
        fi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/ARCHITECTURE.md 
new/sdbootutil-1+git20260929.26b6989/ARCHITECTURE.md
--- old/sdbootutil-1+git20260909.7cfa1f0/ARCHITECTURE.md        2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/ARCHITECTURE.md        2026-09-29 
22:21:06.000000000 +0200
@@ -83,7 +83,7 @@
 An entry file might now look like this:
 
     title      openSUSE Tumbleweed
-    version    [email protected]
+    version    15_1.2.3-1-default
     machine-id 2ceda9f
     sort-key   opensuse-tumbleweed
     options    root=UUID=abc... rootflags=subvol=@/.snapshots/15/snapshot
@@ -107,7 +107,7 @@
 So that makes an entry look like this
 
     title      openSUSE Tumbleweed
-    version    [email protected]
+    version    15_1.2.3-1-default
     machine-id 2ceda9f
     sort-key   opensuse-tumbleweed
     options    root=UUID=abc... rootflags=subvol=@/.snapshots/15/snapshot
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/CLAUDE.md 
new/sdbootutil-1+git20260929.26b6989/CLAUDE.md
--- old/sdbootutil-1+git20260909.7cfa1f0/CLAUDE.md      2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/CLAUDE.md      2026-09-29 
22:21:06.000000000 +0200
@@ -185,7 +185,7 @@
 
 ```
 title      openSUSE Tumbleweed
-version    [email protected]
+version    15_6.2.1-1-default
 machine-id 2ceda9f
 sort-key   opensuse-tumbleweed
 options    root=UUID=... rootflags=subvol=@/.snapshots/15/snapshot
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sdbootutil-1+git20260909.7cfa1f0/measure-pcr-validator.sh 
new/sdbootutil-1+git20260929.26b6989/measure-pcr-validator.sh
--- old/sdbootutil-1+git20260909.7cfa1f0/measure-pcr-validator.sh       
2026-09-09 13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/measure-pcr-validator.sh       
2026-09-29 22:21:06.000000000 +0200
@@ -27,7 +27,8 @@
        fi
 
        local res=1
-       for sha in sha1 sha256 sha384 sha512; do
+       # Strongest bank first
+       for sha in sha512 sha384 sha256 sha1; do
                [ -e "/sys/class/tpm/tpm0/pcr-$sha/15" ] || continue
                read -r expected_pcr_15 < "/sys/class/tpm/tpm0/pcr-$sha/15"
                grep -Fixq "$expected_pcr_15" 
/var/lib/sdbootutil/measure-pcr-prediction; res="$?"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil 
new/sdbootutil-1+git20260929.26b6989/sdbootutil
--- old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil     2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/sdbootutil     2026-09-29 
22:21:06.000000000 +0200
@@ -33,7 +33,10 @@
 DEBUG_LOG="/var/log/sdbootutil.log"
 verbose=
 
-if [[ "$*" =~ "--start-trace-code" ]] && ! touch "$DEBUG_LOG" 2>/dev/null; then
+# The trace can record secrets (passwords and PINs passed as command
+# line arguments to the tools that this script calls), so the log is
+# created 0600 and never with whatever the umask happens to be
+if [[ "$*" =~ "--start-trace-code" ]] && ! (umask 077; touch "$DEBUG_LOG") 
2>/dev/null; then
        echo "Cannot enable the code trace, $DEBUG_LOG is not writable" >&2
        exit 1
 fi
@@ -43,9 +46,12 @@
 fi
 # The trace is only enabled if the log can be opened for writing, so a
 # trace left behind by root does not break the tool for the other users.
-# The completion data is also excluded, as the messages and the trace
-# itself interfere with the shell completion
-if [ "$1" != "_print_bash_completion_data" ] && [ -f "$DEBUG_LOG" ] && { exec 
3>>"$DEBUG_LOG"; } 2>/dev/null; then
+# The mode is reasserted on every run, so a log created by hand or by an
+# older version is tightened before anything is written to it, and the
+# trace stays off if it cannot be.  The completion data is also
+# excluded, as the messages and the trace itself interfere with the
+# shell completion
+if [ "$1" != "_print_bash_completion_data" ] && [ -f "$DEBUG_LOG" ] && { chmod 
0600 "$DEBUG_LOG" && exec 3>>"$DEBUG_LOG"; } 2>/dev/null; then
        verbose=3
        echo "The trace of the code is being stored in $DEBUG_LOG" >&2
        echo "Remove the file or use --stop-trace-code to stop tracing the 
code" >&2
@@ -582,9 +588,12 @@
                }
        done
 
-       # Force back "device" for ROOTFS if is encrypted
+       # Force back "device" for ROOTFS if is encrypted.  Not a user
+       # error: the config file is generated before disk-encryption-tool
+       # encrypts the root on first boot, so it keeps "uuid", and
+       # `status` already reports the value as forced
        if is_rootfs_crypt; then
-               [ "$ROOTFS" = "device" ] || info "Fix the 
/etc/default/sdbootutil to set ROOTFS as 'device'"
+               [ "$ROOTFS" = "device" ] || dbg "ROOTFS=$ROOTFS in the config 
file, forcing 'device' as the root is encrypted"
                ROOTFS="device"
        fi
 
@@ -853,7 +862,7 @@
 {
        local device
        read -r device < <(findmnt / -v -n -o SOURCE 2> /dev/null)
-       [ -n "$device" ] && [ "$(lsblk --noheadings -o TYPE "$device" 2> 
/dev/null)" = "crypt" ]
+       [ -n "$device" ] && lsblk --noheadings --list --inverse -o TYPE 
"$device" 2> /dev/null | grep -qx crypt
 }
 
 get_rootfs()
@@ -865,7 +874,9 @@
                label) read -r rootfs_data < <(findmnt / -v -n -o LABEL 2> 
/dev/null) ;;
                partuuid) read -r rootfs_data < <(findmnt / -v -n -o PARTUUID 
2> /dev/null) ;;
                partlabel) read -r rootfs_data < <(findmnt / -v -n -o PARTLABEL 
2> /dev/null) ;;
-               device) read -r rootfs_data < <(findmnt / -v -n -o SOURCE 2> 
/dev/null) ;;
+               # "/dev/mapper/NAME", as "/dev/dm-N" depends on the
+               # order in which the devices were opened
+               device) read -r rootfs_data < <(lsblk --noheadings --nodeps -o 
PATH "$(findmnt / -v -n -o SOURCE 2> /dev/null)" 2> /dev/null) ;;
                *)
                        info "Can't determine rootfs ($ROOTFS). Using UUID as 
default"
                        ROOTFS="uuid"
@@ -883,12 +894,18 @@
 
 get_all_rootfs()
 {
-       local rootfs rootfs_data
+       local rootfs rootfs_data kname path
 
        read -r rootfs_data < <(findmnt / -v -n -o SOURCE)
        rootfs="$rootfs_data"
 
+       # Every device of the file system (btrfs can span several), by
+       # both names, as older entries can use "/dev/dm-N"
        read -r rootfs_data < <(findmnt / -v -n -o UUID)
+       [ -z "$rootfs_data" ] || while read -r kname path; do
+               rootfs="$rootfs|/dev/$kname|$path"
+       done < <(lsblk --noheadings --raw -o KNAME,PATH -Q "UUID == 
\"$rootfs_data\"" 2> /dev/null)
+
        [ -z "$rootfs_data" ] || rootfs="$rootfs|UUID=$rootfs_data"
 
        read -r rootfs_data < <(findmnt / -v -n -o LABEL)
@@ -938,12 +955,33 @@
        sed "${sed_arguments[@]}"
 }
 
+# Entries written before the "N_" prefix say "N@", and systemd >= v262
+# warns about the "@" on every parse.  Only the warning, the entry is
+# accepted and sorts the same
+bootctl_noise="^.*: Version string '[0-9]+@[^']*' is not a valid version, 
accepting anyway\.$"
+
+# `bootctl` for the commands that parse the entries.  The noise is
+# kept when the user asked for more output, with `--verbose` or with
+# SYSTEMD_LOG_LEVEL
+bootctl_entries()
+{
+       if [ -n "$verbose" ] || [ -n "$SYSTEMD_LOG_LEVEL" ]; then
+               bootctl "$@"
+               return
+       fi
+       # No `2> >(...)`: it opens /proc/self/fd/N with O_CREAT, which
+       # SELinux denies (dac_override, add_name)
+       local rc=0
+       bootctl "$@" 2> "$tmpdir/bootctl.err" || rc=$?
+       grep -Ev "$bootctl_noise" "$tmpdir/bootctl.err" >&2 || :
+       return "$rc"
+}
 
 entry_filter=("cat")
 update_entries()
 {
        [ -z "$1" ] || entry_filter=("$@")
-       bootctl list --json=short | "${entry_filter[@]}" > "$entryfile"
+       bootctl_entries list --json=short | "${entry_filter[@]}" > "$entryfile"
        dbg "Entry filter: ${entry_filter[*]}"
        dbg_cat "$entryfile"
 }
@@ -971,7 +1009,7 @@
        local root
        root="$(get_all_rootfs)"
 
-       update_entries jq 
"[.[]|select(has(\"options\"))|select(.options|test(\"root=(?:$root)\"))]"
+       update_entries jq 
"[.[]|select(has(\"options\"))|select(.options|test(\"root=(?:$root)(?: 
|$)\"))]"
 }
 
 update_entries_for_extra()
@@ -1012,6 +1050,24 @@
        echo 
"${prefix:+$prefix-}$entry_token-$kernel_version${snapshot:+-$snapshot}${tries:++$tries}.conf"
 }
 
+# An entry name reduced to what identifies it, so that a name from
+# `bootctl` and one from a boot loader can be compared.
+#
+# `bootctl` reports the ID with its ".conf" suffix and without the boot
+# counter that "entry_conf_file" puts in the file name.  grub2-bls
+# writes "LoaderEntrySelected" the other way around: no suffix, and the
+# counter still there, because it records the name before
+# "systemd-bless-boot" renames the file
+entry_key()
+{
+       # "+3" until the loader counts a boot, "+2-1" afterwards
+       local name="${1%.conf}"
+
+       [[ ! "$name" =~ ^(.+)\+[0-9]+(-[0-9]+)?$ ]] || name="${BASH_REMATCH[1]}"
+
+       echo "$name"
+}
+
 find_conf_file()
 {
        local kernel_version="${1:?}"
@@ -1167,7 +1223,7 @@
                       | select(.type? == "type1")
                       | select(.path != null)
                       | .id, .path,
-                        ((.version // "") | 
capture("((?<snapshot>[0-9]*)@)?(?<kernel>.*)")) as $v
+                        ((.version // "") | 
capture("((?<snapshot>[0-9]*)[@_])?(?<kernel>.*)")) as $v
                       | $v.snapshot, $v.kernel')
 }
 
@@ -1304,7 +1360,7 @@
        local id
        id="$(entry_conf_file "$kernel_version" "$snapshot")"
        info "Removing boot entry $id"
-       bootctl unlink "$id"
+       bootctl_entries unlink "$id"
 
        # If we remove the default entry, `bootctl` will mark a new
        # default, but we still need to update the EFI var (or the
@@ -1339,8 +1395,7 @@
                mv "$old" "$old.bak" || return "$?"
        fi
        rollback+=("$old")
-       install -p -m 0644 "$src" "$dst" || return "$?"
-       chown root:root "$dst" 2> /dev/null || true
+       install -p -m 0644 -o root -g root "$src" "$dst" || return "$?"
        info "Installed $dst"
 }
 
@@ -1446,7 +1501,11 @@
                        info "Found existing initrd $v"
                        dstinitrd+=("$v")
                done < <(entry_field "$conf" initrd)
-               [ "${#dstinitrd[@]}" -eq 0 ] || return 0
+               # Freeing space must not remove the initrd reused here
+               [ "${#dstinitrd[@]}" -eq 0 ] || {
+                       reused_entry="$conf"
+                       return 0
+               }
        fi
 
        return 1
@@ -1459,38 +1518,36 @@
        # We include the rootfs (the first line usually), as is needed
        # to appear in the mounts under the chroot, allowing dracut to
        # properly detect the fs type and load the relevant module.
-       findmnt -o TARGET,FSTYPE,FSROOT -Rv --pairs / > "$tmpdir/mounts"
+       findmnt -o TARGET,FSTYPE,FSROOT -Rv --json / > "$tmpdir/mounts"
        mount --bind "$snapshot_dir" "$snapshot_dir"
        # Register the chroot before mounting anything else, so a
        # failure in the middle of the loop is unwound by `cleanup`
        chroot_dir="$snapshot_dir"
-       while read -r line; do
-               eval "$line"
-               # shellcheck disable=SC2153
-               [ "$FSTYPE" = "btrfs" ] || [ "$FSTYPE" = "vfat" ] || [ 
"$FSTYPE" = "xfs" ] || [[ "$FSTYPE" == ext* ]] || continue
-               [ "$TARGET" != "/" ] || continue
-               [ "$TARGET" = "/etc" ] && [ "$FSTYPE" = "btrfs" ] && continue
-               [[ "$TARGET" != /.snapshots* ]] || continue
-               [[ "$TARGET" != /run/media/* ]] || continue
+       local target fstype fsroot
+       while IFS=$'\t' read -r target fstype fsroot; do
+               [ "$fstype" = "btrfs" ] || [ "$fstype" = "vfat" ] || [ 
"$fstype" = "xfs" ] || [[ "$fstype" == ext* ]] || continue
+               [ "$target" != "/" ] || continue
+               [ "$target" = "/etc" ] && [ "$fstype" = "btrfs" ] && continue
+               [[ "$target" != /.snapshots* ]] || continue
+               [[ "$target" != /run/media/* ]] || continue
                # After a `transactional-update apply` the running
                # system has /usr and /boot bind mounted from the new
                # default snapshot.  Those belong to a different
                # snapshot and must not shadow the ones that
                # "$snapshot_dir" provides
-               # shellcheck disable=SC2153
-               [ -z "$(snapshot_from_fsroot "$FSROOT")" ] || continue
+               [ -z "$(snapshot_from_fsroot "$fsroot")" ] || continue
                # Not every mount point of the running system is present
                # in the snapshot.  For example the directory that
                # `transactional-update` mounts under /tmp while a
                # transaction is open, or any external media.  They are
                # not needed to generate the initrd, and the snapshot can
                # be read-only, so the directory cannot be created
-               if [ ! -d "$snapshot_dir$TARGET" ]; then
-                       dbg "Skipping $TARGET, not present in $snapshot_dir"
+               if [ ! -d "$snapshot_dir$target" ]; then
+                       dbg "Skipping $target, not present in $snapshot_dir"
                        continue
                fi
-               mountpoint --quiet "$snapshot_dir$TARGET" || mount --bind 
"$TARGET" "$snapshot_dir$TARGET"
-       done < "$tmpdir/mounts"
+               mountpoint --quiet "$snapshot_dir$target" || mount --bind 
"$target" "$snapshot_dir$target"
+       done < <(jq -r 
'..|objects|select(has("target"))|[.target,.fstype,.fsroot]|@tsv' 
"$tmpdir/mounts")
        rm "$tmpdir/mounts"
 
        mount -t tmpfs -o size=10m tmpfs "$snapshot_dir/run"
@@ -1544,6 +1601,10 @@
 
 pending_kernel_size()
 {
+       [ -n "$1" ] || {
+               echo 0
+               return 0
+       }
        echo $(($(stat -c %s "$1") / 1024 + 1))
 }
 
@@ -1660,20 +1721,20 @@
        #
        # The columns are never empty, as `read` and `sort` would
        # collapse consecutive tabs and shift the fields
-       jq -r --arg in_use "${in_use[*]}" --arg last "$root_snapshot" '
+       jq -r --arg in_use "${in_use[*]}" --arg last "$root_snapshot" --arg 
keep "${reused_entry:-}" '
                def snapshot_of:
                        ((.options // "") | capture("rootflags=subvol=[^ 
]*/\\.snapshots/(?<n>[0-9]+)/snapshot") | .n)
-                       // ((.version // "") | capture("^(?<n>[0-9]+)@") | .n)
+                       // ((.version // "") | capture("^(?<n>[0-9]+)[@_]") | 
.n)
                        // ((.id // "") | 
capture("-(?<n>[0-9]+)(\\+[0-9]+(-[0-9]+)?)?\\.conf$") | .n)
                        // "";
                def kernel_of:
                        ((.linux // "") | capture("^/[^/]+/(?<k>[^/]+)/[^/]+$") 
| .k)
-                       // ((.version // "") | capture("@(?<k>.+)$") | .k)
+                       // ((.version // "") | capture("^[0-9]+[@_](?<k>.+)$") 
| .k)
                        // "";
 
                ($in_use | split(" ") | map(select(. != ""))) as $in_use
                | .[]
-               | select(.isDefault != true and .isSelected != true)
+               | select(.isDefault != true and .isSelected != true and .path 
!= $keep)
                | snapshot_of as $s
                | kernel_of as $k
                | select(($in_use | index($s)) == null or $s == $last)
@@ -1911,6 +1972,7 @@
        [ -z "$have_snapshots" ] || 
subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
        local kernel_version="$2"
        local dstinitrd=()
+       local reused_entry=
        local 
src="${subvol#"${subvol_prefix}"}/lib/modules/$kernel_version/$image"
        [ -n "$kernel_version" ] || err "Missing kernel version"
        [ -e "$src" ] || err "Can't find $src"
@@ -2010,7 +2072,11 @@
                fi
        fi
 
-       make_free_space_for_kernel "$snapshot" "$src" "$tmpdir" 
"$devicetree_src" || err "No free space in ${boot_root} for new kernel"
+       # Files already in the ESP are reused, and need no space
+       local kernel_size_src="$src" devicetree_size_src="$devicetree_src"
+       [ ! -e "${boot_root}$dst" ] || kernel_size_src=
+       [ -z "$devicetree_dst" ] || [ ! -e "${boot_root}$devicetree_dst" ] || 
devicetree_size_src=
+       make_free_space_for_kernel "$snapshot" "$kernel_size_src" "$tmpdir" 
"$devicetree_size_src" || err "No free space in ${boot_root} for new kernel"
 
        local boot_options
        [ -z "$in_buildroot" ] || 
subvol="${subvol_prefix}/.snapshots/${snapshot}/snapshot"
@@ -2044,10 +2110,18 @@
        local entry_machine_id=
        [ "$entry_token" = "$machine_id" ] && entry_machine_id="$machine_id"
 
+       # The snapshot number goes in front of the kernel version, so
+       # that the boot loader sorts the entries by snapshot first.  The
+       # separator is "_": it is one of the characters that
+       # `strverscmp_improved()` drops as a plain segment separator,
+       # like the "@" used before, so both sort identically.  "@" is
+       # outside the UAPI.10 charset and systemd v262 warns about it on
+       # every entry it parses.  Entries written with "@" are still on
+       # disk, and every reader accepts both
        cat > "$tmpdir/entry.conf" <<-EOF
        # Boot Loader Specification type#1 entry
        title      $title
-       version    
${snapshot:+$snapshot@}$kernel_version${entry_machine_id:+${nl}machine-id 
$entry_machine_id}${sort_key:+${nl}sort-key   $sort_key}
+       version    
${snapshot:+${snapshot}_}$kernel_version${entry_machine_id:+${nl}machine-id 
$entry_machine_id}${sort_key:+${nl}sort-key   $sort_key}
        options    $boot_options
        linux      $dst${devicetree_dst:+${nl}devicetree ${devicetree_dst}}
        EOF
@@ -2206,7 +2280,7 @@
                        info "Cleaning boot entry $id"
                        rm "$path"
                }
-       done < <(jq -r '.[] | .id, .path, (.version | 
capture("((?<snapshot>[0-9]*)@)?(?<kernel>.*)")) as $v | $v.snapshot, 
$v.kernel' "$entryfile")
+       done < <(jq -r '.[] | .id, .path, (.version | 
capture("((?<snapshot>[0-9]*)[@_])?(?<kernel>.*)")) as $v | $v.snapshot, 
$v.kernel' "$entryfile")
 
        # The loop above drops the entry whose kernel is gone.  The
        # opposite case -- the kernel is still there and what went missing
@@ -2260,13 +2334,12 @@
        fi
 
        # `bootctl` builds "isSelected" by matching the entry ID against
-       # "LoaderEntrySelected", but grub2-bls writes the variable without
-       # the ".conf" suffix, so nothing matches there and "isSelected" is
-       # always null.  Read the variable to compare it here too, both
-       # spellings, until grub2-bls is fixed.  Note that the value is
-       # lower-cased by `bli_efi_var_get`
+       # "LoaderEntrySelected", but grub2-bls spells the variable
+       # differently and nothing matches there, so "isSelected" is always
+       # null.  Compare it here too, through "entry_key", until grub2-bls
+       # is fixed
        local selected=
-       [ -z "$interactive" ] || selected="$(bli_efi_var_get 
"LoaderEntrySelected")"
+       [ -z "$interactive" ] || selected="$(entry_key "$(bli_efi_var_get 
"LoaderEntrySelected")")"
 
        local isdefault isselected isreported type id root conf title marker 
booted
        while read -r isdefault isselected isreported type id root conf title; 
do
@@ -2283,8 +2356,7 @@
                marker=
                if [ -n "$interactive" ]; then
                        booted=
-                       if [ "$isselected" = "true" ] || [ "${id,,}" = 
"$selected" ] \
-                           || [ "${id,,}" = "$selected.conf" ]; then
+                       if [ "$isselected" = "true" ] || [ "${id%.conf}" = 
"$selected" ]; then
                                booted=1
                        fi
                        if [ "$isdefault" = "true" ]; then
@@ -3055,14 +3127,20 @@
        for ((i=0;i<${#s};i+=2)); do echo -ne "\x${s:$i:2}"; done
 }
 
+# `bootctl random-seed` hashes fresh entropy together with the seed
+# that is already in the ESP, it also writes the "LoaderSystemToken"
+# EFI variable.
 update_random_seed()
 {
        [ -z "$arg_no_random_seed" ] || return 0
-       local s _p
-       read -r s _p < <({ dd if=/dev/urandom bs=32 count=1 status=none; [ -e 
"${esp_root}/loader/random-seed" ] && dd if="${esp_root}/loader/random-seed" 
bs=32 count=1 status=none; } | sha256sum)
-       [ "${#s}" = 64 ] || { warn "Invalid random seed"; return 0; }
-       hex_to_binary "$s" > "${esp_root}/loader/random-seed.new"
-       mv "${esp_root}/loader/random-seed.new" "${esp_root}/loader/random-seed"
+
+       local extra=()
+       [ -z "$arg_no_variables" ] && [ -z "$arg_portable" ] && mountpoint -q 
"$esp_root" || extra=("--no-variables")
+
+       local output
+       output="$(bootctl "${extra[@]}" random-seed 2>&1)" || \
+               warn "Failed to update the random seed${output:+: $output}"
+       return 0
 }
 
 has_efivars()
@@ -3074,7 +3152,8 @@
 {
        # BLI uses this vendor UUID
        local 
efi_var="/sys/firmware/efi/efivars/${1:?}-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
-       [ ! -e "$efi_var" ] || dd "if=$efi_var" bs=2 skip=2 conv=lcase 
status=none | tr -d '\0'
+       # 4 bytes of attributes, then the value as UTF-16LE
+       [ ! -e "$efi_var" ] || dd "if=$efi_var" bs=2 skip=2 status=none | tr -d 
'\0'
 }
 
 bli_efi_var_set()
@@ -3096,7 +3175,8 @@
        [ -e "${esp_root}/loader/loader.conf" ] || touch 
"${esp_root}/loader/loader.conf"
 
        if grep -q "^$key " "${esp_root}/loader/loader.conf"; then
-               sed -i -e "s/^$key .*/$key $value/" 
"${esp_root}/loader/loader.conf"
+               # "|" as the delimiter, as a value can contain a path
+               sed -i -e "s|^$key .*|$key $value|" 
"${esp_root}/loader/loader.conf"
        else
                echo "$key $value" >> "${esp_root}/loader/loader.conf"
        fi
@@ -3127,9 +3207,15 @@
        done < "${esp_root}${esp_dst}/grubenv"
        echo "$key=$value" >> "$grubenv"
 
+       # GRUB2 reads a block of exactly 1024 bytes, and what is not a
+       # variable is padding.  One `printf` for the whole padding: the
+       # old `seq 1 $filler` loop printed nothing for a full block, and
+       # `printf` writes its format once when it has no arguments, so a
+       # block that was already full got a 1025th byte
        local filler
        filler=$((1024 - $(stat -c %s "$grubenv")))
-       printf '#%.0s' $(seq 1 $filler) >> "$grubenv"
+       [ "$filler" -ge 0 ] || err "grubenv has no room left for $key"
+       printf '%*s' "$filler" "" | tr ' ' '#' >> "$grubenv"
 
        mv "$grubenv" "${esp_root}${esp_dst}/grubenv"
 }
@@ -3299,19 +3385,14 @@
 # The snapshot that an entry describes, read from the entry list in
 # stdin.  Empty when the entry is not in the list or describes no
 # snapshot
-#
-# The ID is matched case insensitively: when it comes from
-# "LoaderEntryDefault" it has been through `bli_efi_var_get`, which
-# lower-cases what it reads, and an entry token is not always lower
-# case (same workaround as in "list_entries")
 entry_snapshot()
 {
        local id="${1:?}"
 
        jq -r --arg id "$id" '.[]
-               | select((.id | ascii_downcase) == ($id | ascii_downcase))
+               | select(.id == $id)
                | ((.options // "") | capture("rootflags=subvol=[^ 
]*/\\.snapshots/(?<n>[0-9]+)/snapshot") | .n)
-                 // ((.version // "") | capture("^(?<n>[0-9]+)@") | .n)
+                 // ((.version // "") | capture("^(?<n>[0-9]+)[@_]") | .n)
                  // empty'
 }
 
@@ -3547,6 +3628,22 @@
        return "$status"
 }
 
+# `systemd-pcrlock` refuses to generate a component when the event
+# log does not replay to the current value of one of the PCRs that
+# the component describes, and it checks all of them, not only the
+# ones that the policy is going to seal.  Two cases are routine:
+# PCR 7 when secure boot is disabled, and PCR 0 under a firmware
+# that does not log everything that it measures, like the vTPM of
+# VMware Workstation.  Neither is a reason to stop, as a PCR left
+# without a component is dropped by `predict` and reported by
+# `get_final_pcrs`
+pcrlock_lock()
+{
+       local err status=0
+       err="$(pcrlock "$@" 2>&1)" || status=$?
+       [ "$status" -eq 0 ] || dbg "No component for '$1': ${err:-exit $status}"
+}
+
 is_pcr_oracle()
 {
        [ -e /etc/systemd/tpm2-pcr-public-key.pem ] && \
@@ -3687,9 +3784,9 @@
        #     version has more priority
        #
        # Without snapshots
-       #   - The version of the entry has no "N@" prefix, so there is
-       #     no snapshot to order by and every entry shares the same
-       #     priority.  Only the kernel version separates them, the
+       #   - The version of the entry has no "N_" (or "N@") prefix, so
+       #     there is no snapshot to order by and every entry shares the
+       #     same priority.  Only the kernel version separates them, the
        #     higher one first
        #
        local filter
@@ -3697,7 +3794,7 @@
        # and by "as", so the filter has to reach jq unexpanded
        # shellcheck disable=SC2016
        if [ -n "$have_snapshots" ]; then
-               filter='def priority(id): id as $id | $ids | split(" ") | 
index($id); map(. + {"priority": priority(.version | scan("(\\d+)@") | .[]), 
"kernel": .version | scan(".*@(?:(\\d+).(\\d+).(\\d+)-(\\d+))") | map(. | 
tonumber)})'
+               filter='def priority(id): id as $id | $ids | split(" ") | 
index($id); map(. + {"priority": priority(.version | scan("^(\\d+)[@_]") | 
.[]), "kernel": .version | scan("^\\d+[@_](?:(\\d+).(\\d+).(\\d+)-(\\d+))") | 
map(. | tonumber)})'
        else
                filter='map(([.version // "" | 
scan("(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)")] | first) as $kernel | . + {"priority": 
0, "kernel": (($kernel // []) | map(tonumber))})'
        fi
@@ -3777,6 +3874,26 @@
        find /var/lib/pcrlock.d/"$component".pcrlock.d -name '*.pcrlock' ! 
-name 'shift-*.pcrlock' -delete
 }
 
+# The shifted variation matches the current event log, and when the
+# component did not change it is identical to the one that was just
+# generated.  `systemd-pcrlock` drops the duplicated values from the
+# prediction, but only after walking every combination of variations,
+# so each copy doubles the cost of `predict` and `make-policy`.
+drop_duplicated_shifts()
+{
+       local shifted variation
+
+       for shifted in /var/lib/pcrlock.d/*.pcrlock.d/shift-*.pcrlock; do
+               for variation in "${shifted%/*}"/*.pcrlock; do
+                       [[ "$(basename "$variation")" != shift-* ]] || continue
+                       cmp -s "$shifted" "$variation" || continue
+                       dbg "Dropping $shifted, identical to $variation"
+                       rm "$shifted"
+                       break
+               done
+       done
+}
+
 uint64_le()
 {
        # 64 bit little endian representation of a number, as escape
@@ -4417,6 +4534,10 @@
 
 get_predicted_hashes()
 {
+       # Nothing generated any component yet, which is a valid state
+       # and not a reason to print a `find` error
+       [ -d /var/lib/pcrlock.d ] || return 0
+
        find /var/lib/pcrlock.d/ -name "*.pcrlock" -type f -exec jq -r 
'.records[].digests[] | select(.hashAlg == "sha256") | .digest' {} + | sort -u
 }
 
@@ -4926,18 +5047,16 @@
 
        shift_component 250-firmware-code-early
        shift_component 550-firmware-code-late
-       pcrlock lock-firmware-code
+       pcrlock_lock lock-firmware-code
 
        shift_component 250-firmware-config-early
        shift_component 550-firmware-config-late
-       pcrlock lock-firmware-config
+       pcrlock_lock lock-firmware-config
 
-       # If secure boot is disabled, this can fail.  There is patch
-       # for the policy generation, and for the authority is planned
        shift_component 240-secureboot-policy
-       pcrlock lock-secureboot-policy &> /dev/null || true
+       pcrlock_lock lock-secureboot-policy
        shift_component 620-secureboot-authority
-       pcrlock lock-secureboot-authority &> /dev/null || true
+       pcrlock_lock lock-secureboot-authority
        # Generates 620-secureboot-authority when the verb cannot
        pcrlock_secureboot_sbatlevel
 
@@ -5003,6 +5122,8 @@
                pcrlock_grub2_bls
        fi
 
+       drop_duplicated_shifts
+
        # The copy in the ESP is imported by `dracut-pcr-signature`,
        # and can be missing after a new ESP installation.  Both copies
        # are identical, so a missing one can be restored from the
@@ -5181,7 +5302,7 @@
                echo "Recovery PIN: $pin"
                if [ -x /usr/bin/qrencode ]; then
                        echo "You can also scan it with your mobile phone:"
-                       qrencode -t utf8i "$pin"
+                       echo -n "$pin" | qrencode -t utf8i
                fi
 
                # Add the generated recovery PIN to the kernel
@@ -5407,8 +5528,8 @@
                # extensions after the switch root cannot participate
                # in abort the boot process from initrd itself
                #
-               # Note that dracut will add the cr_swap partition even
-               # if it is not marked as x-initrd.attach
+               # Note that dracut will add the swap partition even if
+               # it is not marked as x-initrd.attach
                [[ "$name" = \#* ]] && continue
                [[ "$opts" != *"tpm2-device="* ]] && continue
                [[ "$opts" != *"tpm2-measure-pcr="* ]] && continue
@@ -5793,7 +5914,21 @@
 in_lockout()
 {
        command -v tpm2_getcap &> /dev/null || { warn "tpm2_getcap not found"; 
return 1; }
-       tpm2_getcap properties-variable | grep -q 'inLockout: *1'
+
+       # `tpm2-tools` probes a fixed list of TCTIs and `tabrmd` comes
+       # before the device.  A system that has `libtss2-tcti-tabrmd0`
+       # installed but no `tpm2-abrmd` running loads the library, fails
+       # to reach the service on the bus, and prints a GDBus warning
+       # plus "Could not initialize TCTI file" before falling back to
+       # /dev/tpmrm0.  Nothing is broken by that, but it is the first
+       # thing on stderr, and an installer that collects stderr reports
+       # it as the reason for whatever fails next.  Naming the device
+       # skips the probe.
+       #
+       # This is the same device that every `systemd-cryptenroll` and
+       # `systemd-pcrlock` call here already talks to: `tpm2_context_new`
+       # hardcodes it, and for the same reason
+       TPM2TOOLS_TCTI="device:/dev/tpmrm0" tpm2_getcap properties-variable | 
grep -q 'inLockout: *1'
 }
 
 is_same_device()
@@ -6178,7 +6313,7 @@
                echo "Recovery key: $key"
                if [ -x /usr/bin/qrencode ]; then
                        echo "You can also scan it with your mobile phone:"
-                       qrencode -t utf8i "$key"
+                       echo -n "$key" | qrencode -t utf8i
                fi
        fi
 
@@ -6610,6 +6745,7 @@
 status_tpm2_device=
 status_pin_reachable=
 status_dangling=
+status_pcr15_halt=
 
 # Whether "$status_dangling" holds this entry.  The names in it carry
 # the ".conf" that the boot loader interface variables leave out, so
@@ -6984,11 +7120,27 @@
        fi
        status_row "Enabled" "yes"
 
+       # The same three checks that `measure-pcr-validator` makes at
+       # boot, in the same order.  Any of them failing ends in
+       # `FailureAction=poweroff-immediate`, so this is not a detail of
+       # the report but the answer to "why did the machine power off"
+       if [ ! -e "$prediction" ]; then
+               status_pcr15_halt="there is no prediction file"
+       elif [ ! -e "$prediction.sha256" ]; then
+               status_pcr15_halt="the prediction is not signed"
+       elif ! openssl dgst -sha256 \
+                       -verify /var/lib/sdbootutil/measure-pcr-public.pem \
+                       -signature "$prediction.sha256" \
+                       "$prediction" &> /dev/null; then
+               status_pcr15_halt="the signature of the prediction is not valid"
+       fi
+
        if [ -n "$arg_full" ]; then
-               local present="MISSING" signed=", NOT SIGNED"
-               [ ! -e "$prediction" ] || present="present"
-               [ ! -e "$prediction.sha256" ] || signed=", signed"
-               status_row "Prediction" "$present$signed"
+               if [ -n "$status_pcr15_halt" ]; then
+                       status_row "Prediction" "UNUSABLE, $status_pcr15_halt"
+               else
+                       status_row "Prediction" "present, signed"
+               fi
 
                if [ -e /var/lib/sdbootutil/measure-pcr-public.pem ]; then
                        status_row "Public key" "yes"
@@ -7201,6 +7353,28 @@
        EOF
 }
 
+# One row for a binary in the ESP against the one that the system ships,
+# decided the same way that `bootloader_needs_update` decides
+status_version_row()
+{
+       local label="$1" deployed="$2" system="$3"
+
+       if [ -z "$system" ]; then
+               status_row "$label" "$deployed, the version of the system 
cannot be read"
+               return
+       fi
+
+       # The comparison goes to stdout ("261.2 == 261.2"), and only the
+       # exit status is wanted here
+       local status=0
+       systemd-analyze compare-versions "$deployed" "$system" > /dev/null 2>&1 
|| status="$?"
+       case "$status" in
+               11) status_row "$label" "$deployed, newer than the $system of 
the system" ;;
+               12) status_row "$label" "OUTDATED: $deployed in the ESP, 
$system in the system" ;;
+               *)  status_row "$label" "up to date ($deployed)" ;;
+       esac
+}
+
 # What the machine booted, and what it will boot next
 status_boot()
 {
@@ -7220,18 +7394,19 @@
 
        if [ -z "$deployed" ]; then
                status_row "In the ESP" "no bootloader found in the ESP"
-       elif [ -z "$system" ]; then
-               status_row "In the ESP" "$deployed, the version of the system 
cannot be read"
        else
-               # The comparison goes to stdout ("261.2 == 261.2"), and
-               # only the exit status is wanted here
-               local cmp=0
-               systemd-analyze compare-versions "$deployed" "$system" > 
/dev/null 2>&1 || cmp="$?"
-               case "$cmp" in
-                       11) status_row "In the ESP" "$deployed, newer than the 
$system of the system" ;;
-                       12) status_row "In the ESP" "OUTDATED: $deployed in the 
ESP, $system in the system" ;;
-                       *)  status_row "In the ESP" "up to date ($deployed)" ;;
-               esac
+               status_version_row "In the ESP" "$deployed" "$system"
+       fi
+
+       # `install_bootloader` deploys the shim and the bootloader
+       # together, so a shim that is not the one of the system is also a
+       # reason to run it, the way `bootloader_needs_update` counts it.
+       # Without a shim in the ESP there is nothing to compare
+       local deployed_shim="" system_shim=""
+       deployed_shim="$(shim_version 2> /dev/null)" || deployed_shim=""
+       if [ -n "$deployed_shim" ]; then
+               system_shim="$(shim_version "$(find_shim)" 2> /dev/null)" || 
system_shim=""
+               status_version_row "Shim" "$deployed_shim" "$system_shim"
        fi
 
        # Its own copy of the entry list, and not "update_entries": that
@@ -7242,17 +7417,16 @@
        [ -n "$entries" ] || entries="[]"
 
        # `bootctl` builds "isSelected" by matching the entry ID against
-       # "LoaderEntrySelected", but grub2-bls writes the variable without
-       # the ".conf" suffix, so nothing matches there and "isSelected" is
-       # always null.  Compare both spellings against the variable, which
-       # `bli_efi_var_get` returns lower-cased (same workaround as in
-       # "list_entries")
-       local selected="" booted=""
+       # "LoaderEntrySelected", but grub2-bls spells the variable
+       # differently and nothing matches there, so "isSelected" is always
+       # null.  Compare it here too, through "entry_key" (same workaround
+       # as in "list_entries")
+       local selected="" key="" booted=""
        selected="$(bli_efi_var_get "LoaderEntrySelected" 2> /dev/null)" || 
selected=""
-       booted="$(jq -r --arg s "$selected" \
+       key="$(entry_key "$selected")"
+       booted="$(jq -r --arg k "$key" \
                     'first(.[] | select(.isSelected == true
-                                        or (.id | ascii_downcase) == $s
-                                        or (.id | ascii_downcase) == ($s + 
".conf"))
+                                        or (.id | sub("\\.conf$"; "")) == $k)
                           | .id) // empty' <<<"$entries")"
        # No entry claims it, so report the raw variable: on a machine
        # that booted something the ESP no longer offers, the name is the
@@ -7416,6 +7590,16 @@
                warn "Run 'sdbootutil cleanup --repair' to write them again 
from the kernels that are still installed"
        }
 
+       # The validator runs on every boot and powers the machine off
+       # when the prediction it needs is not usable.  Nothing else in
+       # the report says that the next boot does not finish, and the
+       # rows that carry the two halves of it are behind `--full`
+       [ -z "$status_pcr15_halt" ] || {
+               echo
+               warn "/etc/crypttab asks to measure PCR 15, but 
$status_pcr15_halt. 'measure-pcr-validator' powers the machine off at the next 
boot"
+               warn "Run 'sdbootutil update-predictions' to write it, or boot 
once with 'measure-pcr-validator.ignore=yes' in the cmdline"
+       }
+
        # A policy with no device behind it is not an error, but it is
        # never what the reader assumes when they see "Backend: pcrlock".
        # It is what a partial unenroll leaves, and it makes the whole
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil-enroll 
new/sdbootutil-1+git20260929.26b6989/sdbootutil-enroll
--- old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil-enroll      2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/sdbootutil-enroll      2026-09-29 
22:21:06.000000000 +0200
@@ -183,7 +183,7 @@
 # This is not an error: the service is enabled by the image, while the
 # credentials come from the deployment
 [ -n "$rk$pw$tpm2_pin$tpm2$fido2" ] || {
-       echo "No enrollment method requested. The device is still opened only 
by the key that disk-encryption-tool generated, readable from %user:cryptenroll 
until this boot ends" > /dev/stderr
+       echo "No enrollment method requested. The device is still opened only 
by the key that disk-encryption-tool generated, readable from %user:cryptenroll 
until this boot ends" >&2
        exit 0
 }
 
@@ -220,7 +220,7 @@
                           KEY="$key" sdbootutil enroll --method=recovery-key 
"$extra")"; then
                [ -z "$recovery_key" ] || write_issue_file "$recovery_key"
        else
-               echo "Failed to enroll the recovery key" > /dev/stderr
+               echo "Failed to enroll the recovery key" >&2
                error=1
        fi
 }
@@ -236,7 +236,7 @@
        fi
        CURRENT_PW="$current_pw" RECOVERY_PIN="$recovery_pin" \
                PW="$pw" sdbootutil enroll --method=password "$extra" || {
-               echo "Failed to enroll the password" > /dev/stderr
+               echo "Failed to enroll the password" >&2
                error=1
        }
 }
@@ -247,7 +247,7 @@
                  SDB_ADD_INITIAL_COMPONENT=1 PIN="$tpm2_pin" sdbootutil enroll 
--method=tpm2+pin)"; then
                write_recovery_pin "$out"
        else
-               echo "Failed to enroll the TPM2 with PIN" > /dev/stderr
+               echo "Failed to enroll the TPM2 with PIN" >&2
                error=1
        fi
 elif [ -n "$tpm2" ]; then
@@ -256,7 +256,7 @@
                  SDB_ADD_INITIAL_COMPONENT=1 sdbootutil enroll 
--method=tpm2)"; then
                write_recovery_pin "$out"
        else
-               echo "Failed to enroll the TPM2" > /dev/stderr
+               echo "Failed to enroll the TPM2" >&2
                error=1
        fi
 fi
@@ -265,7 +265,7 @@
        echo "Enrolling a FIDO2 key"
        CURRENT_PW="$current_pw" RECOVERY_PIN="$recovery_pin" \
                sdbootutil enroll --method=fido2 || {
-               echo "Failed to enroll the FIDO2 key" > /dev/stderr
+               echo "Failed to enroll the FIDO2 key" >&2
                error=1
        }
 }
@@ -277,7 +277,7 @@
 # open it.  Exiting non-zero also makes the failure visible, as
 # sdbootutil-enroll.service is the only trace that this ran
 [ "$error" -eq 0 ] || {
-       echo "Keeping the enrollment key, as some enrollment failed" > 
/dev/stderr
+       echo "Keeping the enrollment key, as some enrollment failed" >&2
        exit 1
 }
 
@@ -287,9 +287,9 @@
 while read -r dev; do
        wipe_enrollment_key "$dev"
        case "$?" in
-               1) echo "Keeping the enrollment key of $dev, as no other method 
can open it" > /dev/stderr
+               1) echo "Keeping the enrollment key of $dev, as no other method 
can open it" >&2
                   error=1 ;;
-               2) echo "Failed to remove the enrollment key of $dev" > 
/dev/stderr
+               2) echo "Failed to remove the enrollment key of $dev" >&2
                   error=1 ;;
        esac
 done < <(sdbootutil list-devices)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil-update-predictions.service 
new/sdbootutil-1+git20260929.26b6989/sdbootutil-update-predictions.service
--- old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil-update-predictions.service  
2026-09-09 13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/sdbootutil-update-predictions.service  
2026-09-29 22:21:06.000000000 +0200
@@ -3,7 +3,7 @@
 ConditionSecurity=tpm2
 
 [Service]
-Type=oneshot
+Type=exec
 KeyringMode=shared
 PrivateTmp=yes
 # Predictions are only needed when a device is unlocked with the TPM2.
@@ -39,14 +39,22 @@
         else \
             echo "Command failed. Reason: $SERVICE_RESULT (Status: 
$EXIT_STATUS). No backup found, keeping the current predictions"; \
         fi; \
+        [ ! -d /run/sdbootutil ] || touch /run/sdbootutil/update-predictions; \
     fi; \
     rm -rf "/tmp/$INVOCATION_ID"'
 ImportCredential=sdbootutil-update-predictions.*
+# On failure the marker is set again, so the predictions are still
+# pending: if this was interrupted by a reboot, the transient
+# sdbootutil-update-predictions-shutdown.service (snapper plugin) is
+# ordered after this service and retries them.  Without the marker the
+# loop has removed it already, and nothing would redo them
+#
 # The service can be triggered by a timer (snapper plugin), this
 # prevents systemd from killing the script mid-run during a reboot,
-# and gives 2 minutes to complete
+# and gives 5 minutes to complete.  The loop can run update-predictions
+# more than once, and a single run takes about a minute on a slow VM
 KillSignal=SIGCONT
-TimeoutStopSec=120
+TimeoutStopSec=300
 
 [Install]
 WantedBy=default.target
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil.spec 
new/sdbootutil-1+git20260929.26b6989/sdbootutil.spec
--- old/sdbootutil-1+git20260909.7cfa1f0/sdbootutil.spec        2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/sdbootutil.spec        2026-09-29 
22:21:06.000000000 +0200
@@ -151,6 +151,7 @@
 Requires:       %{name} = %{version}
 Requires:       bash
 Requires:       bash-completion
+Supplements:    (%{name} and bash-completion)
 BuildArch:      noarch
 
 %description bash-completion
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/tests/README.md 
new/sdbootutil-1+git20260929.26b6989/tests/README.md
--- old/sdbootutil-1+git20260909.7cfa1f0/tests/README.md        2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/tests/README.md        2026-09-29 
22:21:06.000000000 +0200
@@ -61,6 +61,8 @@
 | `update-all-entries` | editing an entry in place is not a silent no-op, and 
is deterministic |
 | `boot-counter` | a `+N` counter is kept, and does not hide the entry from 
removal |
 | `repair-entry` | `cleanup --repair` restores the file and keeps a 
hand-edited command line |
+| `set-default` | the default entry is written where the same loader reads it 
back |
+| `tight-esp` | on a nearly full ESP, reinstalling a kernel that is already 
there evicts nothing |
 
 ### What it reports today
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sdbootutil-1+git20260909.7cfa1f0/tests/scenarios/set-default 
new/sdbootutil-1+git20260929.26b6989/tests/scenarios/set-default
--- old/sdbootutil-1+git20260909.7cfa1f0/tests/scenarios/set-default    
1970-01-01 01:00:00.000000000 +0100
+++ new/sdbootutil-1+git20260929.26b6989/tests/scenarios/set-default    
2026-09-29 22:21:06.000000000 +0200
@@ -0,0 +1,36 @@
+#!/bin/bash
+# scenario: set-default
+# Set the default boot entry and read it back
+#
+# The default is written to a different place on each loader --
+# loader.conf for systemd-boot, grubenv for grub2-bls -- and the reader
+# has to look in the same one. It fails silently: the write lands
+# somewhere nothing reads, get-default keeps answering with bootctl's
+# guess, and the machine boots the entry it booted before.
+#
+# --no-variables is what puts the on-disk path under test, and is also
+# what keeps this out of the real EFI variables.
+sdb()
+{
+       "$SDBOOTUTIL" --esp-path "$ESP" --no-variables --disable-predictions 
"$@"
+}
+
+# Prefer an entry that is not the default already, so that the write has
+# something to change, and settle for the only one there is otherwise.
+# `first` over an empty stream prints nothing rather than "null", so the
+# preference has to be expressed inside jq: a guard out here would read
+# the empty output as an answer and skip the guest
+target="$(bootctl list --json=short | jq -r '
+       [.[] | select(.type == "type1")] as $entries
+       | (first($entries[] | select(.isDefault != true))
+          // first($entries[]) // empty) | .id')"
+[ -n "$target" ] || exit 77
+
+sdb set-default "$target"
+
+got="$(sdb get-default)"
+[ "$got" = "$target" ] || {
+       echo "get-default returned '$got', expected '$target'"
+       exit 1
+}
+echo "default round trip: $target"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/sdbootutil-1+git20260909.7cfa1f0/tests/scenarios/tight-esp 
new/sdbootutil-1+git20260929.26b6989/tests/scenarios/tight-esp
--- old/sdbootutil-1+git20260909.7cfa1f0/tests/scenarios/tight-esp      
1970-01-01 01:00:00.000000000 +0100
+++ new/sdbootutil-1+git20260929.26b6989/tests/scenarios/tight-esp      
2026-09-29 22:21:06.000000000 +0200
@@ -0,0 +1,90 @@
+#!/bin/bash
+# scenario: tight-esp
+# Re-installing a kernel that is already in the ESP needs no space
+#
+# make_free_space unlinks boot entries, from the least to the most
+# valuable one, until the new kernel and initrd fit.  A kernel whose
+# file is already in the ESP is reused, and so is an initrd taken from
+# an existing entry, so neither needs any space.  Counting them anyway
+# (github issue 449) evicts entries that nothing needed gone, and those
+# can be the ones of the default snapshot.
+#
+# The copy of the ESP normally lives in /var/tmp, where the free space
+# is never short and the eviction never runs.  So it is moved to a
+# tmpfs with half a kernel of free space, and the reserve is set to
+# zero: the reinstallation must then leave every entry in place.
+#
+# Scoped to the running subvolume, like boot-counter: that is the entry
+# that add-kernel finds and reinstalls with --force
+version="$(uname -r)"
+subvol="$(findmnt --noheadings -o FSROOT --target / | head -n1)"
+subvol="${subvol#/}"
+
+[ ! -e "/usr/lib/modules/$version/initrd" ] || {
+       echo "a prebuilt initrd is installed, and its size is counted"
+       exit 77
+}
+
+linux="$(bootctl list --json=short | jq -r --arg v "$version" --arg s 
"$subvol" '
+       [.[] | select(.type=="type1") |
+        select((.version // "") | endswith($v)) |
+        select($s == "" or $s == "/" or ((.options // "") | contains("subvol=" 
+ $s)))] |
+       if length == 1 then .[0].linux // empty else empty end')"
+[ -n "$linux" ] && [ -f "$ESP$linux" ] || {
+       echo "expected one entry with a kernel for the running $version"
+       exit 77
+}
+
+tight="$ESP.tight"
+mkdir -p "$tight"
+mount -t tmpfs -o size=1G tmpfs "$tight" || { echo "cannot mount a tmpfs"; 
exit 77; }
+trap 'umount "$tight"; rmdir "$tight"' EXIT
+cp -a "$ESP"/. "$tight"
+
+used="$(findmnt --noheadings --bytes -o USED --target "$tight")"
+kernel="$(stat -c %s "$ESP$linux")"
+size=$(((used + kernel / 2) / 1024))
+mount -o remount,size="${size}k" "$tight" || { echo "cannot shrink the tmpfs 
to ${size}k"; exit 77; }
+echo "ESP on a tmpfs of ${size}k, $(findmnt --noheadings -o AVAIL --target 
"$tight") free, the kernel is $((kernel / 1024))k"
+
+# By id and not by file name: the reinstalled entry can come back
+# with a boot counter, and it is still the same entry
+ids()
+{
+       SYSTEMD_ESP_PATH="$tight" bootctl list --json=short |
+               jq -r '.[] | select(.type=="type1") | .id' | sort
+}
+
+ids > "$tight.before"
+
+SYSTEMD_ESP_PATH="$tight" "$SDBOOTUTIL" --esp-path "$tight" --esp-free-space 0 
\
+               --no-variables --disable-predictions --force -v add-kernel 
"$version" 2>&1 |
+       tee "$tight.log"
+status="${PIPESTATUS[0]}"
+
+ids > "$tight.after"
+changed="$(diff "$tight.before" "$tight.after")"
+rm -f "$tight.before" "$tight.after"
+
+# An initrd that was not found is generated, and then it does need the
+# space: what is evicted for it is the designed behaviour, not this bug.
+# It happens when the entry of the snapshot is under another name
+grep -q "Generating new initrd" "$tight.log" && {
+       rm -f "$tight.log"
+       echo "the initrd was not reused, so there was something to make room 
for"
+       exit 77
+}
+rm -f "$tight.log"
+
+# The result goes back where fde-state looks, so the invariants are
+# checked on what the tight ESP was left with
+rm -rf "${ESP:?}"/*
+cp -a "$tight"/. "$ESP"
+
+[ "$status" = 0 ] || { echo "FAIL: add-kernel exited with $status"; exit 1; }
+[ -z "$changed" ] || {
+       echo "FAIL: reinstalling $version changed the entries:"
+       echo "$changed"
+       exit 1
+}
+echo "every entry is still in place"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/sdbootutil-1+git20260909.7cfa1f0/tests/unit 
new/sdbootutil-1+git20260929.26b6989/tests/unit
--- old/sdbootutil-1+git20260909.7cfa1f0/tests/unit     2026-09-09 
13:40:14.000000000 +0200
+++ new/sdbootutil-1+git20260929.26b6989/tests/unit     2026-09-29 
22:21:06.000000000 +0200
@@ -2,8 +2,11 @@
 # SPDX-License-Identifier: MIT
 # SPDX-FileCopyrightText: Copyright 2026 SUSE LLC
 #
-# Unit tests for the routing of the secrets: which source wins, what is
-# published to the kernel keyring, and which warnings are printed.
+# Unit tests for the library half of sdbootutil: the routing of the
+# secrets -- which source wins, what is published to the keyring, and
+# which warnings are printed -- the entry names the boot loaders spell
+# differently, the versions the status report compares, `grubenv`, and
+# the entries that may be removed to make room in the ESP.
 #
 # These are decisions the tool takes *before* it writes anything to the
 # TPM2, so they do not need one, and they do not need a guest either: a
@@ -41,8 +44,11 @@
 KEYS=(cryptenroll sdbootutil sdbootutil-key sdbootutil-pw sdbootutil-pin
       sdbootutil-recovery-pin sdbootutil-tpm2-pin)
 
+# A boot entry ID as `bootctl` reports it, for the entry_key cases
+ENTRY="opensuse-microos-7.2.3-1-default-10"
+
 [ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ] || {
-       sed -n '4,26p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
+       sed -n '4,27p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
        exit 0
 }
 
@@ -333,6 +339,193 @@
        no_key sdbootutil-recovery-pin
 }
 
+# entry_key: the spellings a boot loader can leave in
+# "LoaderEntrySelected", all reduced to the ID that `bootctl` reports.
+# systemd-boot writes that ID as it is; grub2-bls drops the ".conf"
+# suffix and keeps the boot counter, which it records before
+# "systemd-bless-boot" renames the file
+
+t_ek_systemd_boot()
+{
+       load
+
+       is "the ID itself" "$ENTRY" "$(entry_key "$ENTRY.conf")"
+}
+
+t_ek_no_suffix()
+{
+       load
+
+       is "no .conf suffix" "$ENTRY" "$(entry_key "$ENTRY")"
+}
+
+t_ek_counter()
+{
+       load
+
+       is "counter alone" "$ENTRY" "$(entry_key "$ENTRY+3")"
+       is "counter and suffix" "$ENTRY" "$(entry_key "$ENTRY+3.conf")"
+}
+
+t_ek_counted_boot()
+{
+       load
+
+       is "one attempt counted" "$ENTRY" "$(entry_key "$ENTRY+2-1")"
+}
+
+t_ek_not_a_counter()
+{
+       load
+
+       # Only "+" followed by digits is a counter, so an entry token
+       # that carries one keeps it
+       is "a + in the name" "my+entry-1" "$(entry_key "my+entry-1.conf")"
+       is "nothing to reduce" "" "$(entry_key "")"
+}
+
+# status_version_row: the three answers that the report can give about a
+# binary in the ESP, the same ones that `bootloader_needs_update` gives
+
+t_svr_current()
+{
+       load
+
+       says "row" "$(status_version_row "In the ESP" 261.2 261.2)" "up to date 
(261.2)"
+}
+
+t_svr_outdated()
+{
+       load
+
+       says "row" "$(status_version_row "In the ESP" 258.4 261.2)" \
+            "OUTDATED: 258.4 in the ESP, 261.2 in the system"
+}
+
+t_svr_newer()
+{
+       load
+
+       says "row" "$(status_version_row "In the ESP" 262 261.2)" \
+            "262, newer than the 261.2 of the system"
+}
+
+# The shim reports a major.minor that does not change between builds, so
+# the modification time appended to it is what decides
+t_svr_shim_mtime()
+{
+       load
+
+       says "older" "$(status_version_row Shim 16.1-202506170714 
16.1-202601011200)" "OUTDATED"
+       says "newer" "$(status_version_row Shim 16.1-202601011200 
16.1-202506170714)" "newer than"
+}
+
+t_svr_no_system()
+{
+       load
+
+       says "row" "$(status_version_row Shim 16.1-202506170714 "")" \
+            "the version of the system cannot be read"
+}
+
+# grubenv_set: GRUB2 reads a block of exactly 1024 bytes, so the size is
+# the whole of what the padding has to get right
+
+t_grubenv_size()
+{
+       load
+       # shellcheck disable=SC2034  # both are read by the sourced library
+       esp_root="$workdir" esp_dst=""
+
+       grubenv_set default opensuse-tumbleweed-7.2.3-1-default-10
+       is "size" 1024 "$(stat -c %s "$workdir/grubenv")"
+
+       grubenv_set timeout 5
+       is "size after a second key" 1024 "$(stat -c %s "$workdir/grubenv")"
+
+       is "default" opensuse-tumbleweed-7.2.3-1-default-10 "$(grubenv_get 
default)"
+       is "timeout" 5 "$(grubenv_get timeout)"
+}
+
+# A block whose variables already fill it needs no padding at all, and
+# that is where one byte used to be written anyway
+t_grubenv_full()
+{
+       load
+       # shellcheck disable=SC2034  # both are read by the sourced library
+       esp_root="$workdir" esp_dst=""
+
+       # 25 bytes of header, and a "default=" line that takes the rest
+       printf '%s\n' "# GRUB Environment Block" > "$workdir/grubenv"
+       printf 'default=%s\n' "$(printf '%*s' 990 "" | tr ' ' x)" >> 
"$workdir/grubenv"
+
+       grubenv_set default "$(printf '%*s' 990 "" | tr ' ' y)"
+       is "size" 1024 "$(stat -c %s "$workdir/grubenv")"
+}
+
+# removable_entries: the entries that make_free_space may unlink when
+# the ESP is short of space.  A new snapshot reuses the initrd of an
+# entry of its parent (github issue 449), and unlinking that entry frees
+# the initrd the new entry is about to point at
+
+# The ESP of a Tumbleweed where snapshot 11 is being created from 10,
+# the default, which has two kernels.  Snapshot 5 is a leftover
+removable_setup()
+{
+       load
+       # shellcheck disable=SC2034  # read by the sourced library
+       have_snapshots=1 root_snapshot=10 entryfile="$workdir/entries.json"
+
+       # What reads the system: bootctl, and the snapper database
+       # shellcheck disable=SC2329  # both are called by the sourced library
+       update_entries_for_this_system() { :; }
+       # shellcheck disable=SC2329
+       snapshots_in_use() { printf '%s\n' 10 11; }
+
+       local e=/boot/efi/loader/entries t=opensuse-tumbleweed s=@/.snapshots
+       jq -n --arg e "$e" --arg t "$t" --arg s "$s" '[
+               {id: "\($t)-6.1.0-1-default-5.conf", path: 
"\($e)/\($t)-6.1.0-1-default-5.conf",
+                options: "rootflags=subvol=\($s)/5/snapshot", linux: 
"/\($t)/6.1.0-1-default/linux-a"},
+               {id: "\($t)-6.1.0-1-default-10.conf", path: 
"\($e)/\($t)-6.1.0-1-default-10.conf",
+                options: "rootflags=subvol=\($s)/10/snapshot", linux: 
"/\($t)/6.1.0-1-default/linux-a"},
+               {id: "\($t)-6.2.0-1-default-10.conf", path: 
"\($e)/\($t)-6.2.0-1-default-10.conf",
+                options: "rootflags=subvol=\($s)/10/snapshot", linux: 
"/\($t)/6.2.0-1-default/linux-b",
+                isDefault: true}
+       ]' > "$entryfile"
+}
+
+t_re_order()
+{
+       removable_setup
+
+       # The leftover first, the default snapshot as a last resort, and
+       # never the default entry
+       is "candidates" \
+          "opensuse-tumbleweed-6.1.0-1-default-5.conf 
opensuse-tumbleweed-6.1.0-1-default-10.conf" \
+          "$(removable_entries 11 | paste -sd' ')"
+}
+
+t_re_reused()
+{
+       removable_setup
+       # shellcheck disable=SC2034  # read by the sourced library
+       
reused_entry="/boot/efi/loader/entries/opensuse-tumbleweed-6.1.0-1-default-10.conf"
+
+       is "candidates" "opensuse-tumbleweed-6.1.0-1-default-5.conf" \
+          "$(removable_entries 11 | paste -sd' ')"
+}
+
+# pending_kernel_size: a kernel that is already in the ESP is reused, so
+# it needs no space
+t_pks_reused()
+{
+       load
+
+       is "no kernel to copy" 0 "$(pending_kernel_size "")"
+       head -c 4096 /dev/zero > "$workdir/linux"
+       is "a 4K kernel" 5 "$(pending_kernel_size "$workdir/linux")"
+}
+
 ####### the runner #######
 
 run_case()
@@ -380,6 +573,21 @@
        t_erk_recovery_pin_env      enroll_recovery_key enrolls the presented 
recovery PIN as the key
        t_erk_diverging             enroll_recovery_key reports a key that 
differs from the PIN
        t_erk_unreachable_pin       enroll_recovery_key publishes nothing when 
a PIN it cannot reach exists
+       t_ek_systemd_boot           entry_key takes the ID that systemd-boot 
writes
+       t_ek_no_suffix              entry_key takes the ID without the ".conf" 
suffix
+       t_ek_counter                entry_key drops the boot counter
+       t_ek_counted_boot           entry_key drops a counter with the attempts 
done
+       t_ek_not_a_counter          entry_key keeps a "+" that is not a boot 
counter
+       t_svr_current               status_version_row reports a version that 
is the one of the system
+       t_svr_outdated              status_version_row reports a version older 
than the one of the system
+       t_svr_newer                 status_version_row reports a version newer 
than the one of the system
+       t_svr_shim_mtime            status_version_row compares the shim by its 
modification time
+       t_svr_no_system             status_version_row reports a system version 
that cannot be read
+       t_grubenv_size              grubenv_set writes a block of exactly 1024 
bytes
+       t_grubenv_full              grubenv_set does not pad a block that is 
already full
+       t_re_order                  removable_entries keeps the default and 
removes leftovers first
+       t_re_reused                 removable_entries keeps the entry whose 
initrd is being reused
+       t_pks_reused                pending_kernel_size counts nothing for a 
kernel already in the ESP
 EOF
 
 echo

++++++ sdbootutil.obsinfo ++++++
--- /var/tmp/diff_new_pack.WBaDkZ/_old  2026-09-30 16:22:42.179474334 +0200
+++ /var/tmp/diff_new_pack.WBaDkZ/_new  2026-09-30 16:22:42.182474460 +0200
@@ -1,5 +1,5 @@
 name: sdbootutil
-version: 1+git20260909.7cfa1f0
-mtime: 1788954014
-commit: 7cfa1f0ab1bba2c808ef5ff63aff22b26aa42d08
+version: 1+git20260929.26b6989
+mtime: 1790713266
+commit: 26b6989e346388bfa244b226c809a1e6a3824a0d
 

Reply via email to