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-10-29 18:18:15 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/sdbootutil (Old) and /work/SRC/openSUSE:Factory/.sdbootutil.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "sdbootutil" Thu Oct 29 18:18:15 2026 rev:116 rq:1393446 version:1+git20261028.5233b70 Changes: -------- --- /work/SRC/openSUSE:Factory/sdbootutil/sdbootutil.changes 2026-10-27 16:46:26.899162688 +0100 +++ /work/SRC/openSUSE:Factory/.sdbootutil.new.1255/sdbootutil.changes 2026-10-29 18:18:17.754534384 +0100 @@ -1,0 +2,14 @@ +Wed Oct 28 15:50:21 UTC 2026 - Alberto Planas Dominguez <[email protected]> + +- Update to version 1+git20261028.5233b70: + * Enroll recovery key if no one is provided via combustion + +------------------------------------------------------------------- +Tue Oct 27 19:26:41 UTC 2026 - Alberto Planas Dominguez <[email protected]> + +- Update to version 1+git20261027.f17cbce: + * Support new kernel/initrd order for PCR9 in grub2 2.16 + * Use the preferred boot entry in systemd-boot + * Serialize concurrent sdbootutil runs + +------------------------------------------------------------------- Old: ---- sdbootutil-1+git20261026.4e266f0.obscpio New: ---- sdbootutil-1+git20261028.5233b70.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ sdbootutil.spec ++++++ --- /var/tmp/diff_new_pack.qxLDot/_old 2026-10-29 18:18:19.074589703 +0100 +++ /var/tmp/diff_new_pack.qxLDot/_new 2026-10-29 18:18:19.076589787 +0100 @@ -55,7 +55,7 @@ %{nil} Name: sdbootutil -Version: 1+git20261026.4e266f0 +Version: 1+git20261028.5233b70 Release: 0 Summary: Bootctl wrapper for BLS boot loaders License: MIT ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.qxLDot/_old 2026-10-29 18:18:19.149592846 +0100 +++ /var/tmp/diff_new_pack.qxLDot/_new 2026-10-29 18:18:19.154593056 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/openSUSE/sdbootutil.git</param> - <param name="changesrevision">4e266f09ced4ed9a44f290f2cb34b91bae9ac6ef</param></service></servicedata> + <param name="changesrevision">5233b701840f4b91bbed367e194d238dddaa11f8</param></service></servicedata> (No newline at EOF) ++++++ sdbootutil-1+git20261026.4e266f0.obscpio -> sdbootutil-1+git20261028.5233b70.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20261026.4e266f0/doc/raw-disk-image-architecture.md new/sdbootutil-1+git20261028.5233b70/doc/raw-disk-image-architecture.md --- old/sdbootutil-1+git20261026.4e266f0/doc/raw-disk-image-architecture.md 2026-10-26 12:44:49.000000000 +0100 +++ new/sdbootutil-1+git20261028.5233b70/doc/raw-disk-image-architecture.md 2026-10-28 16:48:49.000000000 +0100 @@ -826,7 +826,7 @@ | `firstboot.timezone` | `jeos-firstboot` | Skips the timezone dialog | | `sdbootutil-enroll.current-pw` | `sdbootutil-enroll` | Existing password/passphrase needed to authorize enrolling a *new* factor on an already-enrolled device | | `sdbootutil-enroll.pw` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a password keyslot | -| `sdbootutil-enroll.rk` | `sdbootutil-enroll` | Enroll a recovery-key keyslot | +| `sdbootutil-enroll.rk` | `sdbootutil-enroll` | Enroll a recovery-key keyslot. Implied when no `sdbootutil-enroll.*` method is given and a device is still opened only by the transient enrollment key | | `sdbootutil-enroll.tpm2` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a TPM2 keyslot (no PIN) | | `sdbootutil-enroll.tpm2+pin` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a TPM2 keyslot; value is the PIN | | `sdbootutil-enroll.fido2` | `sdbootutil-enroll`, `jeos-firstboot-enroll` | Enroll a FIDO2 keyslot | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20261026.4e266f0/sdbootutil new/sdbootutil-1+git20261028.5233b70/sdbootutil --- old/sdbootutil-1+git20261026.4e266f0/sdbootutil 2026-10-26 12:44:49.000000000 +0100 +++ new/sdbootutil-1+git20261028.5233b70/sdbootutil 2026-10-28 16:48:49.000000000 +0100 @@ -3187,6 +3187,14 @@ chattr +i "$efi_var" } +bli_efi_var_unset() +{ + local efi_var="/sys/firmware/efi/efivars/${1:?}-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" + [ -e "$efi_var" ] || return 0 + chattr -i "$efi_var" + rm -f "$efi_var" +} + loader_conf_set() { local key="${1:?}" @@ -3202,6 +3210,12 @@ fi } +loader_conf_unset() +{ + local key="${1:?}" + [ ! -e "${esp_root}/loader/loader.conf" ] || sed -i -e "/^$key /d" "${esp_root}/loader/loader.conf" +} + loader_conf_get() { local key="${1:?}" @@ -3250,16 +3264,39 @@ fi } +# systemd-boot boots the default entry even when boot counting marked +# it as bad, so a snapshot that never boots correctly is tried +# forever. The preferred entry (systemd 260) is skipped when it is +# bad, and the boot loader moves to the first entry that is not marked +# as preferred +sdboot_has_preferred() +{ + local features + features="$(od -An -t u8 -j 4 -N 8 /sys/firmware/efi/efivars/LoaderFeatures-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f 2> /dev/null)" || return 1 + (( (features >> 19) & 1 )) +} + set_default_sdboot() { local id="${1:?}" + # The entry is the preferred or the default one, never both: a + # bad preferred entry is skipped, and then the default one is + # booted, even when it is the same bad entry + local key="default" stale="preferred" + if sdboot_has_preferred; then + key="preferred" + stale="default" + fi + if ! has_efivars || [ -n "$arg_no_variables" ]; then - loader_conf_set "default" "$id" - elif ! bli_efi_var_set "LoaderEntryDefault" "$id"; then - warn "EFI variable LoaderEntryDefault cannot be set. Falling back to configuration file" - loader_conf_set "default" "$id" + loader_conf_set "$key" "$id" + elif ! bli_efi_var_set "LoaderEntry${key^}" "$id"; then + warn "EFI variable LoaderEntry${key^} cannot be set. Falling back to configuration file" + loader_conf_set "$key" "$id" fi + loader_conf_unset "$stale" + [ -n "$arg_no_variables" ] || bli_efi_var_unset "LoaderEntry${stale^}" } set_default_grub2_bls() @@ -3351,8 +3388,10 @@ { local val if has_efivars && [ -z "$arg_no_variables" ]; then - val="$(bli_efi_var_get "LoaderEntryDefault")" + val="$(bli_efi_var_get "LoaderEntryPreferred")" + [ -n "$val" ] || val="$(bli_efi_var_get "LoaderEntryDefault")" fi + [ -n "$val" ] || val="$(loader_conf_get "preferred")" [ -n "$val" ] || val="$(loader_conf_get "default")" [ -n "$val" ] || val="$(get_default_bootctl)" [ -z "$val" ] || echo "$val" @@ -4057,7 +4096,13 @@ local cmdline="$4" local suffix="$5" - local elements=("$linux" "$initrd") + # GRUB2-BLS >= 2.16 measures the initrd when the kernel loads it, + # and the kernel measures the cmdline before that + local initrd_in_place= + systemd-analyze compare-versions "$(bootloader_version)" "<" "2.16" || initrd_in_place=1 + + local elements=("$linux") + [ -n "$initrd_in_place" ] || elements+=("$initrd") [ -z "$devicetree" ] || elements+=("$devicetree") local locks=() local n=0 @@ -4094,6 +4139,16 @@ locks+=("$tmpdir/element-$n.pcrlock") rm "$tmpdir/cmdline" + if [ -n "$initrd_in_place" ]; then + n=$((n+1)) + pcrlock \ + lock-raw \ + --pcr=9 \ + --pcrlock "$tmpdir/element-$n.pcrlock" \ + "$initrd" 2> /dev/null + locks+=("$tmpdir/element-$n.pcrlock") + fi + n=$((n+1)) pcrlock \ lock-kernel-initrd \ @@ -7782,6 +7837,23 @@ opts_long="${opts_long%,}" } +# Two runs at the same time race over the ESP, the boot entries and +# /var/lib/pcrlock.d. It happens at boot, when health-checker calls +# set-default-snapshot while sdbootutil-update-predictions.service is +# running. The lock is released when the process exits. Without +# permissions to take it the command would fail later anyway +# +# Like in the snapper plugin the directory is only for root: any user +# that can open the lock can also hold it, and block every run +take_lock() +{ + { mkdir -p /run/sdbootutil && chmod 700 /run/sdbootutil; } 2> /dev/null || return 0 + exec {lock_fd}> /run/sdbootutil/lock || return 0 + flock -n "$lock_fd" && return 0 + info "Waiting for another sdbootutil to finish" + flock -w 600 "$lock_fd" || err "Another sdbootutil has been running for 10 minutes" +} + ####### main ####### if [ "$1" = "_print_bash_completion_data" ]; then @@ -7872,6 +7944,11 @@ err "unknown command $1" fi +case "$1" in + is-installed|needs-update|list-*|status|show-entry|is-bootable|get-*|print-loader-path|bootloader) ;; + *) take_lock ;; +esac + [ -n "$arg_esp_path" ] && export SYSTEMD_ESP_PATH="$arg_esp_path" eval_bootctl diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20261026.4e266f0/sdbootutil-enroll new/sdbootutil-1+git20261028.5233b70/sdbootutil-enroll --- old/sdbootutil-1+git20261026.4e266f0/sdbootutil-enroll 2026-10-26 12:44:49.000000000 +0100 +++ new/sdbootutil-1+git20261028.5233b70/sdbootutil-enroll 2026-10-28 16:48:49.000000000 +0100 @@ -172,20 +172,21 @@ recovery_pin= get_credential recovery_pin "sdbootutil-enroll.recovery-pin" -# Without a single credential there is nothing to enroll, and the -# keyslot that disk-encryption-tool left behind would be wiped without -# anything to replace it. Not wiping it does not make the machine -# bootable either, as that key is random and is never shown to anyone, -# but it is still the better end: the key is in the keyring while this -# boot lasts, so it can be piped out and enrolled by hand. A device -# with no keyslot at all is lost even with the key in hand. -# -# 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" >&2 - exit 0 -} +# Without a single credential nothing was requested. But a device that +# still has the keyslot of disk-encryption-tool is opened only by a key +# that nobody saw, and its initrd does not know about /etc/crypttab, so +# the next boot waits forever for the root device. Enroll a recovery +# key as if it was requested: it is shown in the issue file, and the +# initrd is regenerated +if [ -z "$rk$pw$tpm2_pin$tpm2$fido2" ]; then + while read -r dev; do + cryptsetup luksDump --dump-json-metadata "$dev" 2> /dev/null \ + | jq -e 'any(.tokens[]?; .type == "enrollment-key")' > /dev/null \ + && rk=1 + done < <(sdbootutil list-devices) + [ -n "$rk" ] || exit 0 + echo "No enrollment method requested. Enrolling a recovery key" >&2 +fi # A method that fails must not be mistaken for one that worked: the # keyslot that disk-encryption-tool left behind is wiped at the end of diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20261026.4e266f0/tests/README.md new/sdbootutil-1+git20261028.5233b70/tests/README.md --- old/sdbootutil-1+git20261026.4e266f0/tests/README.md 2026-10-26 12:44:49.000000000 +0100 +++ new/sdbootutil-1+git20261028.5233b70/tests/README.md 2026-10-28 16:48:49.000000000 +0100 @@ -124,6 +124,22 @@ that does not exist yet, and whose first requirement is a way to put a guest back. +## Enrollment service (`tests/enroll`) + +`sdbootutil-enroll` decides what to enroll from the credentials and from +the LUKS2 headers that disk-encryption-tool left behind. `tests/enroll` +runs the real script against LUKS2 headers in image files, with +`sdbootutil` replaced by a stub that enrolls them with `cryptsetup`, in +a user and mount namespace with a private `/run`. It does not need +root. + +```console +$ tests/enroll +ok enrolls a recovery key when nothing was requested +... +5 cases, all passed +``` + ## Adding a scenario A scenario is a bash script in `scenarios/`, run on the guest with diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sdbootutil-1+git20261026.4e266f0/tests/enroll new/sdbootutil-1+git20261028.5233b70/tests/enroll --- old/sdbootutil-1+git20261026.4e266f0/tests/enroll 1970-01-01 01:00:00.000000000 +0100 +++ new/sdbootutil-1+git20261028.5233b70/tests/enroll 2026-10-28 16:48:49.000000000 +0100 @@ -0,0 +1,295 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: Copyright 2026 SUSE LLC +# +# Tests for sdbootutil-enroll: which methods it asks sdbootutil for, +# given the credentials and the LUKS2 headers that disk-encryption-tool +# left behind, what it shows in the issue file, and which keyslots it +# wipes at the end. +# +# The script runs for real, against LUKS2 headers in image files, with +# `sdbootutil` replaced by a stub that lists those images and enrolls +# them with cryptsetup. It runs in a user and mount namespace with a +# private /run, so nothing reaches the issue files of the host. +# +# Usage: +# tests/enroll [-h] +# +# Does not need root. It uses the real %user:cryptenroll keyring entry, +# so it refuses to start when it is already there. + +set -e +shopt -s nullglob + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +top="$(dirname "$here")" + +PATH="$PATH:/usr/sbin:/sbin" + +# The key that disk-encryption-tool generates, and the recovery key that +# the stub enrolls +BOOTSTRAP="bootstrap-password" +RECOVERY="recovery-key-of-the-stub" + +[ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ] || { + sed -n '4,19p' "${BASH_SOURCE[0]}" | sed 's/^# \?//' + exit 0 +} + +skip() +{ + echo "SKIP: $*" + exit 77 +} + +die() +{ + echo "ERROR: $*" >&2 + exit 1 +} + +for cmd in cryptsetup systemd-cryptenroll keyctl jq unshare; do + command -v "$cmd" > /dev/null || skip "$cmd is not installed" +done +unshare -rm true 2> /dev/null || skip "unprivileged user namespaces are not available" +keyctl id %user:cryptenroll &> /dev/null && \ + die "%user:cryptenroll is in the keyring. Purge it first: keyctl purge user cryptenroll" + +workdir="$(mktemp -d -t sdbootutil-enroll.XXXXXX)" + +cleanup_enroll() +{ + rm -rf "$workdir" + keyctl purge user cryptenroll &> /dev/null || true +} +trap cleanup_enroll EXIT + +####### the stubs ####### + +mkdir "$workdir/bin" + +# have_luks2 asks lsblk, that does not see image files +cat > "$workdir/bin/lsblk" <<-'EOF' + #!/bin/bash + echo crypto_LUKS +EOF + +# Every call is logged. The enrollments add a keyslot to every listed +# device, authorized by the key in %user:cryptenroll, as the real one +# does through systemd-cryptenroll +cat > "$workdir/bin/sdbootutil" <<-EOF + #!/bin/bash + echo "\$*" >> "$workdir/calls" + add_key() + { + local dev + while read -r dev; do + cryptsetup luksAddKey --batch-mode --force-password \\ + --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \\ + --key-file <(keyctl pipe %user:cryptenroll) \\ + "\$dev" <(echo -n "\$1") || exit 1 + done < "$workdir/devices" + } + case "\$1 \$2" in + "list-devices "*) cat "$workdir/devices" ;; + "enroll --method=recovery-key") add_key "$RECOVERY"; echo "Recovery key: $RECOVERY" ;; + "enroll --method=password") add_key "\$PW" ;; + *) exit 1 ;; + esac +EOF +chmod +x "$workdir/bin/lsblk" "$workdir/bin/sdbootutil" + +####### the fixtures ####### + +# A device as disk-encryption-tool leaves it: a single keyslot, with the +# key in %user:cryptenroll, marked with the "enrollment-key" token +new_device() +{ + local img="$workdir/$1.img" + truncate -s 32M "$img" + cryptsetup luksFormat --type luks2 --batch-mode --force-password \ + --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ + "$img" <<<"$BOOTSTRAP" + echo '{"type":"enrollment-key","keyslots":["0"]}' | cryptsetup token import "$img" + echo "$img" >> "$workdir/devices" +} + +credential() +{ + mkdir -p "$workdir/creds" + echo "$2" > "$workdir/creds/sdbootutil-enroll.$1" +} + +# Run sdbootutil-enroll with a private /run, and without the +# reconfig_system flag or the issue-generator of the host +run_enroll() +{ + # shellcheck disable=SC2016 # expanded by the inner bash + unshare -rm bash -c ' + mount -t tmpfs tmpfs /run + [ ! -d /var/lib/YaST2 ] || mount -t tmpfs tmpfs /var/lib/YaST2 + [ ! -e /usr/sbin/issue-generator ] || mount --bind /dev/null /usr/sbin/issue-generator + rc=0 + PATH="$1/bin:$PATH" CREDENTIALS_DIRECTORY="$1/creds" "$2/sdbootutil-enroll" || rc=$? + cat /run/issue.d/* > "$1/issue" 2> /dev/null + exit "$rc" + ' - "$workdir" "$top" > "$workdir/out" 2>&1 +} + +####### the assertions ####### + +fail() +{ + echo " $*" + case_failed=1 +} + +is() +{ + [ "$2" = "$3" ] || fail "$1: expected '$2', got '$3'" +} + +says() +{ + grep -qF -- "$3" "$2" 2> /dev/null || fail "$1: '$3' was not said" +} + +says_not() +{ + ! grep -qF -- "$3" "$2" 2> /dev/null || fail "$1: '$3' was said, and should not" +} + +opens() +{ + cryptsetup luksOpen --test-passphrase --disable-external-tokens \ + "$workdir/$1.img" <<<"$2" &> /dev/null || \ + fail "$1: '$2' does not open the device" +} + +opens_not() +{ + ! cryptsetup luksOpen --test-passphrase --disable-external-tokens \ + "$workdir/$1.img" <<<"$2" &> /dev/null || \ + fail "$1: '$2' still opens the device" +} + +####### the cases ####### + +# The bug: the deployment requested nothing, and the device was left +# with a key that nobody saw, and with an initrd that does not ask for it +t_no_credentials() +{ + new_device root + run_enroll + is "exit status" 0 "$?" + + says "calls" "$workdir/calls" "enroll --method=recovery-key --no-reuse-initrd" + says "issue file" "$workdir/issue" "Recovery key: $RECOVERY" + opens root "$RECOVERY" + opens_not root "$BOOTSTRAP" +} + +# Every disk that disk-encryption-tool encrypted shares its key, and +# sdbootutil enrolls all of them in a single call +t_multiple_disks() +{ + new_device root + new_device home + run_enroll + is "exit status" 0 "$?" + + is "recovery key enrollments" 1 "$(grep -c "enroll --method=recovery-key" "$workdir/calls" 2> /dev/null)" + opens root "$RECOVERY" + opens home "$RECOVERY" + opens_not root "$BOOTSTRAP" + opens_not home "$BOOTSTRAP" +} + +# The enroll module of jeos-firstboot runs before, and leaves the key +# in the keyring after it wiped the keyslot +t_already_enrolled() +{ + new_device root + cryptsetup luksAddKey --batch-mode --force-password \ + --pbkdf pbkdf2 --pbkdf-force-iterations 1000 \ + --key-file <(echo -n "$BOOTSTRAP") \ + "$workdir/root.img" <(echo -n "jeos-password") + systemd-cryptenroll --wipe-slot=0 "$workdir/root.img" &> /dev/null + run_enroll + is "exit status" 0 "$?" + + says_not "calls" "$workdir/calls" "enroll" + [ ! -s "$workdir/issue" ] || fail "issue file: written, and should not" +} + +# A requested method is the only one enrolled +t_password() +{ + new_device root + credential pw "requested-password" + run_enroll + is "exit status" 0 "$?" + + says "calls" "$workdir/calls" "enroll --method=password --no-reuse-initrd" + says_not "calls" "$workdir/calls" "enroll --method=recovery-key" + opens root "requested-password" + opens_not root "$BOOTSTRAP" +} + +# Without the key of disk-encryption-tool there is nothing to do +t_no_keyring() +{ + new_device root + keyctl purge user cryptenroll &> /dev/null + run_enroll + is "exit status" 0 "$?" + + [ ! -e "$workdir/calls" ] || fail "calls: sdbootutil was called" + opens root "$BOOTSTRAP" +} + +####### the runner ####### + +run_case() +{ + local fn="$1"; shift + local desc="$*" + local rc=0 + + rm -rf "$workdir"/{*.img,devices,calls,issue,out,creds} + keyctl purge user cryptenroll &> /dev/null || true + echo -n "$BOOTSTRAP" | keyctl padd user cryptenroll @u > /dev/null + + ( set +e; case_failed=; "$fn"; [ -z "$case_failed" ] ) || rc=$? + + total=$((total + 1)) + if [ "$rc" = 0 ]; then + echo "ok $desc" + else + failed=$((failed + 1)) + echo "FAIL $desc" + sed 's/^/ | /' "$workdir/out" 2> /dev/null || true + fi +} + +total=0 +failed=0 + +while read -r fn desc; do + [ -n "$fn" ] || continue + run_case "$fn" "$desc" +done <<-'EOF' + t_no_credentials enrolls a recovery key when nothing was requested + t_multiple_disks enrolls the same recovery key in every disk + t_already_enrolled enrolls nothing when the enrollment key was already wiped + t_password enrolls only the requested method + t_no_keyring does nothing without the key of disk-encryption-tool +EOF + +echo +if [ "$failed" = 0 ]; then + echo "$total cases, all passed" +else + echo "$total cases, $failed failed" + exit 1 +fi ++++++ sdbootutil.obsinfo ++++++ --- /var/tmp/diff_new_pack.qxLDot/_old 2026-10-29 18:18:19.314599761 +0100 +++ /var/tmp/diff_new_pack.qxLDot/_new 2026-10-29 18:18:19.317599887 +0100 @@ -1,5 +1,5 @@ name: sdbootutil -version: 1+git20261026.4e266f0 -mtime: 1793015089 -commit: 4e266f09ced4ed9a44f290f2cb34b91bae9ac6ef +version: 1+git20261028.5233b70 +mtime: 1793202529 +commit: 5233b701840f4b91bbed367e194d238dddaa11f8
