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-29 19:01:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/sdbootutil (Old) and /work/SRC/openSUSE:Factory/.sdbootutil.new.383539 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "sdbootutil" Tue Sep 29 19:01:40 2026 rev:112 rq: version:1+git20260909.7cfa1f0 Changes: -------- --- /work/SRC/openSUSE:Factory/sdbootutil/sdbootutil.changes 2026-09-28 10:37:29.045415164 +0200 +++ /work/SRC/openSUSE:Factory/.sdbootutil.new.383539/sdbootutil.changes 2026-09-29 19:01:42.103659621 +0200 @@ -2,27 +1,0 @@ -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+git20260924.2b7b94e.obscpio New: ---- sdbootutil-1+git20260909.7cfa1f0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ sdbootutil.spec ++++++ --- /var/tmp/diff_new_pack.pgujlh/_old 2026-09-29 19:01:43.447715843 +0200 +++ /var/tmp/diff_new_pack.pgujlh/_new 2026-09-29 19:01:43.448715885 +0200 @@ -55,7 +55,7 @@ %{nil} Name: sdbootutil -Version: 1+git20260924.2b7b94e +Version: 1+git20260909.7cfa1f0 Release: 0 Summary: Bootctl wrapper for BLS boot loaders License: MIT @@ -151,7 +151,6 @@ Requires: %{name} = %{version} Requires: bash Requires: bash-completion -Supplements: (%{name} and bash-completion) BuildArch: noarch %description bash-completion ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.pgujlh/_old 2026-09-29 19:01:43.491717683 +0200 +++ /var/tmp/diff_new_pack.pgujlh/_new 2026-09-29 19:01:43.494717809 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/openSUSE/sdbootutil.git</param> - <param name="changesrevision">2b7b94e0792520e76bfdf84650365aa4d251560c</param></service></servicedata> + <param name="changesrevision">7cfa1f0ab1bba2c808ef5ff63aff22b26aa42d08</param></service></servicedata> (No newline at EOF) ++++++ sdbootutil-1+git20260924.2b7b94e.obscpio -> sdbootutil-1+git20260909.7cfa1f0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20260924.2b7b94e/.github/ISSUE_TEMPLATE/bug_report.yml new/sdbootutil-1+git20260909.7cfa1f0/.github/ISSUE_TEMPLATE/bug_report.yml --- old/sdbootutil-1+git20260924.2b7b94e/.github/ISSUE_TEMPLATE/bug_report.yml 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/.github/ISSUE_TEMPLATE/bug_report.yml 2026-09-09 13:40:14.000000000 +0200 @@ -121,7 +121,7 @@ attributes: label: Debug trace excerpts (sanitized) description: | - 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. + 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. ⚠️ **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+git20260924.2b7b94e/ARCHITECTURE.md new/sdbootutil-1+git20260909.7cfa1f0/ARCHITECTURE.md --- old/sdbootutil-1+git20260924.2b7b94e/ARCHITECTURE.md 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/ARCHITECTURE.md 2026-09-09 13:40:14.000000000 +0200 @@ -83,7 +83,7 @@ An entry file might now look like this: title openSUSE Tumbleweed - version 15_1.2.3-1-default + version [email protected] 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 15_1.2.3-1-default + version [email protected] 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+git20260924.2b7b94e/CLAUDE.md new/sdbootutil-1+git20260909.7cfa1f0/CLAUDE.md --- old/sdbootutil-1+git20260924.2b7b94e/CLAUDE.md 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/CLAUDE.md 2026-09-09 13:40:14.000000000 +0200 @@ -185,7 +185,7 @@ ``` title openSUSE Tumbleweed -version 15_6.2.1-1-default +version [email protected] 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+git20260924.2b7b94e/measure-pcr-validator.sh new/sdbootutil-1+git20260909.7cfa1f0/measure-pcr-validator.sh --- old/sdbootutil-1+git20260924.2b7b94e/measure-pcr-validator.sh 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/measure-pcr-validator.sh 2026-09-09 13:40:14.000000000 +0200 @@ -27,8 +27,7 @@ fi local res=1 - # Strongest bank first - for sha in sha512 sha384 sha256 sha1; do + for sha in sha1 sha256 sha384 sha512; 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+git20260924.2b7b94e/sdbootutil new/sdbootutil-1+git20260909.7cfa1f0/sdbootutil --- old/sdbootutil-1+git20260924.2b7b94e/sdbootutil 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/sdbootutil 2026-09-09 13:40:14.000000000 +0200 @@ -33,10 +33,7 @@ DEBUG_LOG="/var/log/sdbootutil.log" verbose= -# 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 +if [[ "$*" =~ "--start-trace-code" ]] && ! touch "$DEBUG_LOG" 2>/dev/null; then echo "Cannot enable the code trace, $DEBUG_LOG is not writable" >&2 exit 1 fi @@ -46,12 +43,9 @@ 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 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 +# 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 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 @@ -859,7 +853,7 @@ { local device read -r device < <(findmnt / -v -n -o SOURCE 2> /dev/null) - [ -n "$device" ] && lsblk --noheadings --list --inverse -o TYPE "$device" 2> /dev/null | grep -qx crypt + [ -n "$device" ] && [ "$(lsblk --noheadings -o TYPE "$device" 2> /dev/null)" = "crypt" ] } get_rootfs() @@ -871,9 +865,7 @@ 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) ;; - # "/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) ;; + device) read -r rootfs_data < <(findmnt / -v -n -o SOURCE 2> /dev/null) ;; *) info "Can't determine rootfs ($ROOTFS). Using UUID as default" ROOTFS="uuid" @@ -891,18 +883,12 @@ get_all_rootfs() { - local rootfs rootfs_data kname path + local rootfs rootfs_data 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) @@ -952,28 +938,12 @@ 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 - bootctl "$@" 2> >(grep -Ev "$bootctl_noise" >&2) -} entry_filter=("cat") update_entries() { [ -z "$1" ] || entry_filter=("$@") - bootctl_entries list --json=short | "${entry_filter[@]}" > "$entryfile" + bootctl list --json=short | "${entry_filter[@]}" > "$entryfile" dbg "Entry filter: ${entry_filter[*]}" dbg_cat "$entryfile" } @@ -1001,7 +971,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() @@ -1042,24 +1012,6 @@ 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:?}" @@ -1215,7 +1167,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') } @@ -1352,7 +1304,7 @@ local id id="$(entry_conf_file "$kernel_version" "$snapshot")" info "Removing boot entry $id" - bootctl_entries unlink "$id" + bootctl 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 @@ -1387,7 +1339,8 @@ mv "$old" "$old.bak" || return "$?" fi rollback+=("$old") - install -p -m 0644 -o root -g root "$src" "$dst" || return "$?" + install -p -m 0644 "$src" "$dst" || return "$?" + chown root:root "$dst" 2> /dev/null || true info "Installed $dst" } @@ -1506,36 +1459,38 @@ # 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 --json / > "$tmpdir/mounts" + findmnt -o TARGET,FSTYPE,FSROOT -Rv --pairs / > "$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" - 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 + 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 # 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 - [ -z "$(snapshot_from_fsroot "$fsroot")" ] || continue + # shellcheck disable=SC2153 + [ -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 < <(jq -r '..|objects|select(has("target"))|[.target,.fstype,.fsroot]|@tsv' "$tmpdir/mounts") + mountpoint --quiet "$snapshot_dir$TARGET" || mount --bind "$TARGET" "$snapshot_dir$TARGET" + done < "$tmpdir/mounts" rm "$tmpdir/mounts" mount -t tmpfs -o size=10m tmpfs "$snapshot_dir/run" @@ -1708,12 +1663,12 @@ jq -r --arg in_use "${in_use[*]}" --arg last "$root_snapshot" ' 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("^[0-9]+[@_](?<k>.+)$") | .k) + // ((.version // "") | capture("@(?<k>.+)$") | .k) // ""; ($in_use | split(" ") | map(select(. != ""))) as $in_use @@ -2089,18 +2044,10 @@ 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 @@ -2259,7 +2206,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 @@ -2313,12 +2260,13 @@ fi # `bootctl` builds "isSelected" by matching the entry ID against - # "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 + # "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` local selected= - [ -z "$interactive" ] || selected="$(entry_key "$(bli_efi_var_get "LoaderEntrySelected")")" + [ -z "$interactive" ] || selected="$(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 @@ -2335,7 +2283,8 @@ marker= if [ -n "$interactive" ]; then booted= - if [ "$isselected" = "true" ] || [ "${id%.conf}" = "$selected" ]; then + if [ "$isselected" = "true" ] || [ "${id,,}" = "$selected" ] \ + || [ "${id,,}" = "$selected.conf" ]; then booted=1 fi if [ "$isdefault" = "true" ]; then @@ -3106,20 +3055,14 @@ 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 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 + 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" } has_efivars() @@ -3131,8 +3074,7 @@ { # BLI uses this vendor UUID local efi_var="/sys/firmware/efi/efivars/${1:?}-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" - # 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' + [ ! -e "$efi_var" ] || dd "if=$efi_var" bs=2 skip=2 conv=lcase status=none | tr -d '\0' } bli_efi_var_set() @@ -3154,8 +3096,7 @@ [ -e "${esp_root}/loader/loader.conf" ] || touch "${esp_root}/loader/loader.conf" if grep -q "^$key " "${esp_root}/loader/loader.conf"; then - # "|" as the delimiter, as a value can contain a path - sed -i -e "s|^$key .*|$key $value|" "${esp_root}/loader/loader.conf" + sed -i -e "s/^$key .*/$key $value/" "${esp_root}/loader/loader.conf" else echo "$key $value" >> "${esp_root}/loader/loader.conf" fi @@ -3186,15 +3127,9 @@ 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"))) - [ "$filler" -ge 0 ] || err "grubenv has no room left for $key" - printf '%*s' "$filler" "" | tr ' ' '#' >> "$grubenv" + printf '#%.0s' $(seq 1 $filler) >> "$grubenv" mv "$grubenv" "${esp_root}${esp_dst}/grubenv" } @@ -3364,14 +3299,19 @@ # 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 == $id) + | select((.id | ascii_downcase) == ($id | ascii_downcase)) | ((.options // "") | capture("rootflags=subvol=[^ ]*/\\.snapshots/(?<n>[0-9]+)/snapshot") | .n) - // ((.version // "") | capture("^(?<n>[0-9]+)[@_]") | .n) + // ((.version // "") | capture("^(?<n>[0-9]+)@") | .n) // empty' } @@ -3607,22 +3547,6 @@ 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 ] && \ @@ -3763,9 +3687,9 @@ # version has more priority # # Without snapshots - # - 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 + # - 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 # higher one first # local filter @@ -3773,7 +3697,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+)-(\\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+))") | map(. | tonumber)})' else filter='map(([.version // "" | scan("(\\d+)\\.(\\d+)\\.(\\d+)-(\\d+)")] | first) as $kernel | . + {"priority": 0, "kernel": (($kernel // []) | map(tonumber))})' fi @@ -3853,26 +3777,6 @@ 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 @@ -4513,10 +4417,6 @@ 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 } @@ -5026,16 +4926,18 @@ shift_component 250-firmware-code-early shift_component 550-firmware-code-late - pcrlock_lock lock-firmware-code + pcrlock lock-firmware-code shift_component 250-firmware-config-early shift_component 550-firmware-config-late - pcrlock_lock lock-firmware-config + pcrlock 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 lock-secureboot-policy + pcrlock lock-secureboot-policy &> /dev/null || true shift_component 620-secureboot-authority - pcrlock_lock lock-secureboot-authority + pcrlock lock-secureboot-authority &> /dev/null || true # Generates 620-secureboot-authority when the verb cannot pcrlock_secureboot_sbatlevel @@ -5101,8 +5003,6 @@ 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 @@ -5281,7 +5181,7 @@ echo "Recovery PIN: $pin" if [ -x /usr/bin/qrencode ]; then echo "You can also scan it with your mobile phone:" - echo -n "$pin" | qrencode -t utf8i + qrencode -t utf8i "$pin" fi # Add the generated recovery PIN to the kernel @@ -5893,21 +5793,7 @@ in_lockout() { command -v tpm2_getcap &> /dev/null || { warn "tpm2_getcap not found"; return 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' + tpm2_getcap properties-variable | grep -q 'inLockout: *1' } is_same_device() @@ -6292,7 +6178,7 @@ echo "Recovery key: $key" if [ -x /usr/bin/qrencode ]; then echo "You can also scan it with your mobile phone:" - echo -n "$key" | qrencode -t utf8i + qrencode -t utf8i "$key" fi fi @@ -6724,7 +6610,6 @@ 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 @@ -7099,27 +6984,11 @@ 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 - if [ -n "$status_pcr15_halt" ]; then - status_row "Prediction" "UNUSABLE, $status_pcr15_halt" - else - status_row "Prediction" "present, signed" - fi + local present="MISSING" signed=", NOT SIGNED" + [ ! -e "$prediction" ] || present="present" + [ ! -e "$prediction.sha256" ] || signed=", signed" + status_row "Prediction" "$present$signed" if [ -e /var/lib/sdbootutil/measure-pcr-public.pem ]; then status_row "Public key" "yes" @@ -7332,28 +7201,6 @@ 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() { @@ -7373,19 +7220,18 @@ 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 - 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" + # 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 fi # Its own copy of the entry list, and not "update_entries": that @@ -7396,16 +7242,17 @@ [ -n "$entries" ] || entries="[]" # `bootctl` builds "isSelected" by matching the entry ID against - # "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="" + # "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="" selected="$(bli_efi_var_get "LoaderEntrySelected" 2> /dev/null)" || selected="" - key="$(entry_key "$selected")" - booted="$(jq -r --arg k "$key" \ + booted="$(jq -r --arg s "$selected" \ 'first(.[] | select(.isSelected == true - or (.id | sub("\\.conf$"; "")) == $k) + or (.id | ascii_downcase) == $s + or (.id | ascii_downcase) == ($s + ".conf")) | .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 @@ -7569,16 +7416,6 @@ 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+git20260924.2b7b94e/sdbootutil-update-predictions.service new/sdbootutil-1+git20260909.7cfa1f0/sdbootutil-update-predictions.service --- old/sdbootutil-1+git20260924.2b7b94e/sdbootutil-update-predictions.service 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/sdbootutil-update-predictions.service 2026-09-09 13:40:14.000000000 +0200 @@ -3,7 +3,7 @@ ConditionSecurity=tpm2 [Service] -Type=exec +Type=oneshot KeyringMode=shared PrivateTmp=yes # Predictions are only needed when a device is unlocked with the TPM2. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20260924.2b7b94e/sdbootutil.spec new/sdbootutil-1+git20260909.7cfa1f0/sdbootutil.spec --- old/sdbootutil-1+git20260924.2b7b94e/sdbootutil.spec 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/sdbootutil.spec 2026-09-09 13:40:14.000000000 +0200 @@ -151,7 +151,6 @@ 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+git20260924.2b7b94e/tests/README.md new/sdbootutil-1+git20260909.7cfa1f0/tests/README.md --- old/sdbootutil-1+git20260924.2b7b94e/tests/README.md 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/tests/README.md 2026-09-09 13:40:14.000000000 +0200 @@ -61,7 +61,6 @@ | `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 | ### What it reports today diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20260924.2b7b94e/tests/scenarios/set-default new/sdbootutil-1+git20260909.7cfa1f0/tests/scenarios/set-default --- old/sdbootutil-1+git20260924.2b7b94e/tests/scenarios/set-default 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/tests/scenarios/set-default 1970-01-01 01:00:00.000000000 +0100 @@ -1,36 +0,0 @@ -#!/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+git20260924.2b7b94e/tests/unit new/sdbootutil-1+git20260909.7cfa1f0/tests/unit --- old/sdbootutil-1+git20260924.2b7b94e/tests/unit 2026-09-24 22:03:19.000000000 +0200 +++ new/sdbootutil-1+git20260909.7cfa1f0/tests/unit 2026-09-09 13:40:14.000000000 +0200 @@ -2,10 +2,8 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: Copyright 2026 SUSE LLC # -# 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, and `grubenv`. +# Unit tests for the routing of the secrets: which source wins, what is +# published to the kernel keyring, and which warnings are printed. # # 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 @@ -43,9 +41,6 @@ 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/^# \?//' exit 0 @@ -338,130 +333,6 @@ 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")" -} - ####### the runner ####### run_case() @@ -509,18 +380,6 @@ 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 EOF echo ++++++ sdbootutil.obsinfo ++++++ --- /var/tmp/diff_new_pack.pgujlh/_old 2026-09-29 19:01:43.643724042 +0200 +++ /var/tmp/diff_new_pack.pgujlh/_new 2026-09-29 19:01:43.646724168 +0200 @@ -1,5 +1,5 @@ name: sdbootutil -version: 1+git20260924.2b7b94e -mtime: 1790280199 -commit: 2b7b94e0792520e76bfdf84650365aa4d251560c +version: 1+git20260909.7cfa1f0 +mtime: 1788954014 +commit: 7cfa1f0ab1bba2c808ef5ff63aff22b26aa42d08
