From 3bb7b28abf5b62a335b992d6fe30903cde8c8874 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Mon, 10 Aug 2026 22:58:25 +0000 Subject: [PATCH 1/9] systemd-257: build systemd-boot and the UKI stub Enable systemd-boot and systemd-stub in the systemd-257 spec to provide the bootloader and EFI stub required for Unified Kernel Images (UKI). Add a patch to skip building 32-bit (ia32) EFI binaries on architectures that do not need them, and ship a loader.conf with sensible default settings. Signed-off-by: Arnaldo Garcia Rincon --- ...-boot-EFI-arch-on-the-Bottlerocket-S.patch | 95 +++++++++ ...OS-Type-11-kernel-cmdline-extra-mech.patch | 185 ++++++++++++++++++ ...ish-removing-SMBIOS-cmdline-extra-bi.patch | 92 +++++++++ packages/systemd-257/loader.conf | 31 +++ packages/systemd-257/systemd-257.spec | 74 ++++++- 5 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 packages/systemd-257/9018-build-correct-sd-boot-EFI-arch-on-the-Bottlerocket-S.patch create mode 100644 packages/systemd-257/9019-boot-remove-SMBIOS-Type-11-kernel-cmdline-extra-mech.patch create mode 100644 packages/systemd-257/9020-boot-vmspawn-finish-removing-SMBIOS-cmdline-extra-bi.patch create mode 100644 packages/systemd-257/loader.conf diff --git a/packages/systemd-257/9018-build-correct-sd-boot-EFI-arch-on-the-Bottlerocket-S.patch b/packages/systemd-257/9018-build-correct-sd-boot-EFI-arch-on-the-Bottlerocket-S.patch new file mode 100644 index 000000000..3ad7e78a9 --- /dev/null +++ b/packages/systemd-257/9018-build-correct-sd-boot-EFI-arch-on-the-Bottlerocket-S.patch @@ -0,0 +1,95 @@ +From 0bab5fb1dfe436727eecf531bcfa24b06ca72561 Mon Sep 17 00:00:00 2001 +From: Arnaldo Garcia Rincon +Date: Tue, 4 Aug 2026 03:50:03 +0000 +Subject: [PATCH] build: correct sd-boot EFI arch on the Bottlerocket SDK + +The SDK cross sysroot mis-reports host_machine.cpu_family(), so systemd's +efi_arch lookup resolves to the wrong EFI target: 'ia32' on x86_64 and 'arm' on +aarch64. Correct both from host_machine.cpu(), and drop the IA-32 mixed-mode +alternate build, which cannot link in this sysroot. + +Signed-off-by: Arnaldo Garcia Rincon +--- + meson.build | 40 ++++++++++++++++++++++++++++++++-------- + src/boot/meson.build | 4 ++-- + 2 files changed, 34 insertions(+), 10 deletions(-) + +diff --git a/meson.build b/meson.build +index b9d8d5266f..a928ed22cb 100644 +--- a/meson.build ++++ b/meson.build +@@ -1942,6 +1942,36 @@ efi_arch = { + 'x86' : 'ia32', + }.get(host_machine.cpu_family(), '') + ++# Bottlerocket SDK fix: the cross sysroot reports cpu_family 'x86' even for ++# x86_64 targets, which makes efi_arch resolve to 'ia32'. Force x64 when the ++# real target cpu is x86_64. ++if efi_arch == 'ia32' and host_machine.cpu() == 'x86_64' ++ efi_arch = 'x64' ++endif ++ ++# The same SDK quirk applies on 64-bit ARM: cpu_family is reported as 'arm', ++# so efi_arch resolves to the 32-bit 'arm' EFI target and sd-boot is emitted as ++# systemd-bootarm.efi with EFI_MACHINE_TYPE_NAME=arm. Force aa64 when the real ++# target cpu is aarch64, so the binary is named and typed per the EFI spec and ++# matches the bootaa64.efi/grubaa64.efi convention used by shim and GRUB. ++# Note: efi_cpu_family is deliberately left resolving to 'arm' below, since the ++# 'arm' and 'aarch64' entries of efi_arch_c_args are identical ++# (-mgeneral-regs-only) and the extra 'arm' link arg ++# (-Wl,--no-wchar-size-warning) is harmless here. ++if efi_arch == 'arm' and host_machine.cpu() == 'aarch64' ++ efi_arch = 'aa64' ++endif ++ ++# Single corrected arch key for all EFI flag lookups, so no flag site can ++# silently regress to the -m32 (ia32) path on this SDK. ++if efi_arch == 'x64' ++ efi_cpu_family = 'x86_64' ++elif efi_arch == 'ia32' ++ efi_cpu_family = 'x86' ++else ++ efi_cpu_family = host_machine.cpu_family() ++endif ++ + pyelftools = pymod.find_installation('python3', + required : get_option('bootloader'), + modules : ['elftools']) +@@ -1954,14 +1984,8 @@ conf.set_quoted('EFI_MACHINE_TYPE_NAME', have ? efi_arch : '') + + efi_arch_alt = '' + efi_cpu_family_alt = '' +-if have and efi_arch == 'x64' and cc.links(''' +- #include +- int main(int argc, char *argv[]) { +- return __builtin_popcount(argc - CHAR_MAX); +- }''', args : ['-m32', '-march=i686'], name : '32bit build possible') +- efi_arch_alt = 'ia32' +- efi_cpu_family_alt = 'x86' +-endif ++# IA-32 mixed-mode alternate build disabled for Bottlerocket: the SDK cross ++# sysroot lacks 32-bit glibc headers (gnu/stubs-32.h), so -m32 cannot build. + + pefile = pymod.find_installation('python3', required: false, modules : ['pefile']) + +diff --git a/src/boot/meson.build b/src/boot/meson.build +index 632771777b..28089efed2 100644 +--- a/src/boot/meson.build ++++ b/src/boot/meson.build +@@ -313,11 +313,11 @@ efi_archspecs = [ + 'c_args' : [ + efi_c_args, + '-DEFI_MACHINE_TYPE_NAME="' + efi_arch + '"', +- efi_arch_c_args.get(host_machine.cpu_family(), []), ++ efi_arch_c_args.get(efi_cpu_family, []), + ], + 'link_args' : [ + efi_c_ld_args, +- efi_arch_c_ld_args.get(host_machine.cpu_family(), []), ++ efi_arch_c_ld_args.get(efi_cpu_family, []), + ], + }, + ] +-- +2.52.0 + diff --git a/packages/systemd-257/9019-boot-remove-SMBIOS-Type-11-kernel-cmdline-extra-mech.patch b/packages/systemd-257/9019-boot-remove-SMBIOS-Type-11-kernel-cmdline-extra-mech.patch new file mode 100644 index 000000000..d27838659 --- /dev/null +++ b/packages/systemd-257/9019-boot-remove-SMBIOS-Type-11-kernel-cmdline-extra-mech.patch @@ -0,0 +1,185 @@ +From 18967f6064e3e9a6d139e9d19e92367e4f9c3c94 Mon Sep 17 00:00:00 2001 +From: Arnaldo Garcia Rincon +Date: Wed, 12 Aug 2026 18:55:46 +0000 +Subject: [PATCH] boot: remove SMBIOS Type 11 kernel-cmdline-extra mechanism + +Remove the ability for sd-boot and the UKI stub to extend the kernel +command line with values read from SMBIOS Type 11 OEM strings +("io.systemd.boot.kernel-cmdline-extra" and +"io.systemd.stub.kernel-cmdline-extra"). + +SMBIOS Type 11 OEM strings are supplied by the firmware or VMM and +were being trusted to inject additional kernel command line +arguments outside of the UKI/boot entry itself. Remove this +mechanism entirely: + +- src/boot/stub.c: drop cmdline_append_and_measure_smbios() and its + call site in run(). +- src/boot/boot.c: drop the block in image_start() that appended the + "io.systemd.boot.kernel-cmdline-extra" OEM string to the boot + entry options. +- src/boot/smbios.c, src/boot/smbios.h: drop + smbios_find_oem_string(), which no longer has any callers. + +systemd-vmspawn's arg_kernel_cmdline_extra is left untouched: aside +from the (now dead) SMBIOS Type 11 OEM string arguments it still +generates for QEMU when booting via a bootloader/UKI, it is also +used to build the kernel command line passed directly via QEMU's +-append when booting a kernel image directly, so it is not solely +tied to this mechanism. + +Signed-off-by: Arnaldo Garcia Rincon +--- + src/boot/boot.c | 12 ------------ + src/boot/smbios.c | 27 --------------------------- + src/boot/smbios.h | 2 -- + src/boot/stub.c | 36 +----------------------------------- + 4 files changed, 1 insertion(+), 76 deletions(-) + +diff --git a/src/boot/boot.c b/src/boot/boot.c +index 21aa00b356..da5e97771e 100644 +--- a/src/boot/boot.c ++++ b/src/boot/boot.c +@@ -23,7 +23,6 @@ + #include "sbat.h" + #include "secure-boot.h" + #include "shim.h" +-#include "smbios.h" + #include "ticks.h" + #include "tpm2-pcr.h" + #include "uki.h" +@@ -2599,17 +2598,6 @@ static EFI_STATUS image_start( + * so). */ + _cleanup_free_ char16_t *options = xstrdup16(options_initrd ?: entry->options_implied ? NULL : entry->options); + +- if (entry->type == LOADER_LINUX && !is_confidential_vm()) { +- const char *extra = smbios_find_oem_string("io.systemd.boot.kernel-cmdline-extra"); +- if (extra) { +- _cleanup_free_ char16_t *tmp = TAKE_PTR(options), *extra16 = xstr8_to_16(extra); +- if (isempty(tmp)) +- options = TAKE_PTR(extra16); +- else +- options = xasprintf("%ls %ls", tmp, extra16); +- } +- } +- + /* Prefix profile if it's non-zero */ + if (entry->profile > 0) { + _cleanup_free_ char16_t *tmp = TAKE_PTR(options); +diff --git a/src/boot/smbios.c b/src/boot/smbios.c +index 329619f85b..844b558cea 100644 +--- a/src/boot/smbios.c ++++ b/src/boot/smbios.c +@@ -182,33 +182,6 @@ bool smbios_in_hypervisor(void) { + return FLAGS_SET(type0->bios_characteristics_ext[1], 1 << 4); + } + +-const char* smbios_find_oem_string(const char *name) { +- uint64_t left; +- +- assert(name); +- +- const SmbiosTableType11 *type11 = (const SmbiosTableType11 *) get_smbios_table(11, sizeof(SmbiosTableType11), &left); +- if (!type11) +- return NULL; +- +- assert(left >= type11->header.length); /* get_smbios_table() already validated this */ +- left -= type11->header.length; +- +- for (const char *p = type11->contents, *limit = type11->contents + left; p < limit; ) { +- const char *e = memchr(p, 0, limit - p); +- if (!e || e == p) /* Double NUL byte means we've reached the end of the OEM strings. */ +- break; +- +- const char *eq = startswith8(p, name); +- if (eq && *eq == '=') +- return eq + 1; +- +- p = e + 1; +- } +- +- return NULL; +-} +- + static const char* smbios_get_string(const SmbiosHeader *header, size_t nr, uint64_t left) { + const char *s = (const char *) ASSERT_PTR(header); + +diff --git a/src/boot/smbios.h b/src/boot/smbios.h +index 34625c8572..674d702841 100644 +--- a/src/boot/smbios.h ++++ b/src/boot/smbios.h +@@ -5,8 +5,6 @@ + + bool smbios_in_hypervisor(void); + +-const char* smbios_find_oem_string(const char *name); +- + typedef struct RawSmbiosInfo { + const char *manufacturer; + const char *product_name; +diff --git a/src/boot/stub.c b/src/boot/stub.c +index 06bf513950..6904d21fd8 100644 +--- a/src/boot/stub.c ++++ b/src/boot/stub.c +@@ -17,7 +17,6 @@ + #include "sbat.h" + #include "secure-boot.h" + #include "shim.h" +-#include "smbios.h" + #include "splash.h" + #include "tpm2-pcr.h" + #include "uki.h" +@@ -768,37 +767,6 @@ static void measure_sections( + } + } + +-static void cmdline_append_and_measure_smbios(char16_t **cmdline, int *parameters_measured) { +- assert(cmdline); +- assert(parameters_measured); +- +- /* SMBIOS OEM Strings data is controlled by the host admin and not covered by the VM attestation, so +- * MUST NOT be trusted when in a confidential VM */ +- if (is_confidential_vm()) +- return; +- +- const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra"); +- if (!extra) +- return; +- +- _cleanup_free_ char16_t *extra16 = mangle_stub_cmdline(xstr8_to_16(extra)); +- if (isempty(extra16)) +- return; +- +- /* SMBIOS strings are measured in PCR1, but we also want to measure them in our specific PCR12, as +- * firmware-owned PCRs are very difficult to use as they'll contain unpredictable measurements that +- * are not under control of the machine owner. */ +- bool m = false; +- (void) tpm_log_load_options(extra16, &m); +- combine_measured_flag(parameters_measured, m); +- +- _cleanup_free_ char16_t *tmp = TAKE_PTR(*cmdline); +- if (isempty(tmp)) +- *cmdline = TAKE_PTR(extra16); +- else +- *cmdline = xasprintf("%ls %ls", tmp, extra16); +-} +- + static void initrds_free(struct iovec (*initrds)[_INITRD_MAX]) { + assert(initrds); + +@@ -1219,11 +1187,9 @@ static EFI_STATUS run(EFI_HANDLE image) { + load_all_addons(image, loaded_image, uname, &cmdline_addons, &dt_addons, &n_dt_addons, &initrd_addons, &n_initrd_addons, &ucode_addons, &n_ucode_addons); + + /* If we have any extra command line to add via PE addons, load them now and append, and measure the +- * additions together, after the embedded options, but before the smbios ones, so that the order is +- * reversed from "most hardcoded" to "most dynamic". The global addons are loaded first, and the ++ * additions together, after the embedded options. The global addons are loaded first, and the + * image-specific ones later, for the same reason. */ + cmdline_append_and_measure_addons(cmdline_addons, &cmdline, ¶meters_measured); +- cmdline_append_and_measure_smbios(&cmdline, ¶meters_measured); + + export_common_variables(loaded_image); + export_stub_variables(loaded_image, profile); +-- +2.52.0 + diff --git a/packages/systemd-257/9020-boot-vmspawn-finish-removing-SMBIOS-cmdline-extra-bi.patch b/packages/systemd-257/9020-boot-vmspawn-finish-removing-SMBIOS-cmdline-extra-bi.patch new file mode 100644 index 000000000..967308b58 --- /dev/null +++ b/packages/systemd-257/9020-boot-vmspawn-finish-removing-SMBIOS-cmdline-extra-bi.patch @@ -0,0 +1,92 @@ +From a21b5d6f051ce76245c8e509b09905a72fd077a6 Mon Sep 17 00:00:00 2001 +From: Arnaldo Garcia Rincon +Date: Wed, 12 Aug 2026 19:01:30 +0000 +Subject: [PATCH] boot,vmspawn: finish removing SMBIOS cmdline-extra bits + +Follow-up to the removal of the SMBIOS Type 11 OEM string mechanism +for kernel command line injection: two leftover references to the +removed feature remained and are cleaned up here. + +- src/boot/stub.c: stop advertising EFI_STUB_FEATURE_CMDLINE_SMBIOS + in the StubFeatures EFI variable. The stub no longer reads the + SMBIOS Type 11 OEM string, so it must not claim to support it. + +- src/vmspawn/vmspawn.c: when booting a guest via a boot loader or + UKI (i.e. not a direct kernel boot), stop emitting the two + "-smbios type=11,value=io.systemd.{stub,boot}.kernel-cmdline-extra=..." + QEMU arguments. Since sd-boot and the UKI stub no longer read these + OEM strings, that code silently produced inert QEMU arguments and + gave no indication that the extra kernel command line was dropped. + A log_warning() is now emitted in that case instead. Direct kernel + boots are unaffected: they still pass the extra command line via + QEMU's -append, which does not rely on SMBIOS at all. + +Signed-off-by: Arnaldo Garcia Rincon +--- + src/boot/stub.c | 1 - + src/vmspawn/vmspawn.c | 30 +++++------------------------- + 2 files changed, 5 insertions(+), 26 deletions(-) + +diff --git a/src/boot/stub.c b/src/boot/stub.c +index 6904d21fd8..98177b1741 100644 +--- a/src/boot/stub.c ++++ b/src/boot/stub.c +@@ -146,7 +146,6 @@ static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, unsig + EFI_STUB_FEATURE_THREE_PCRS | /* We can measure kernel image, parameters and sysext */ + EFI_STUB_FEATURE_RANDOM_SEED | /* We pass a random seed to the kernel */ + EFI_STUB_FEATURE_CMDLINE_ADDONS | /* We pick up .cmdline addons */ +- EFI_STUB_FEATURE_CMDLINE_SMBIOS | /* We support extending kernel cmdline from SMBIOS Type #11 */ + EFI_STUB_FEATURE_DEVICETREE_ADDONS | /* We pick up .dtb addons */ + EFI_STUB_FEATURE_MULTI_PROFILE_UKI | /* We grok the "@1" profile command line argument */ + EFI_STUB_FEATURE_REPORT_STUB_PARTITION | /* We set StubDevicePartUUID + StubImageIdentifier */ +diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c +index faac8775d4..650d8778f5 100644 +--- a/src/vmspawn/vmspawn.c ++++ b/src/vmspawn/vmspawn.c +@@ -1860,7 +1860,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { + } + + if (ARCHITECTURE_SUPPORTS_SMBIOS) { +- _cleanup_free_ char *kcl = strv_join(arg_kernel_cmdline_extra, " "), *escaped_kcl = NULL; ++ _cleanup_free_ char *kcl = strv_join(arg_kernel_cmdline_extra, " "); + if (!kcl) + return log_oom(); + +@@ -1868,30 +1868,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { + r = strv_extend_many(&cmdline, "-append", kcl); + if (r < 0) + return log_oom(); +- } else { +- if (ARCHITECTURE_SUPPORTS_SMBIOS) { +- escaped_kcl = escape_qemu_value(kcl); +- if (!escaped_kcl) +- log_oom(); +- +- r = strv_extend(&cmdline, "-smbios"); +- if (r < 0) +- return log_oom(); +- +- r = strv_extendf(&cmdline, "type=11,value=io.systemd.stub.kernel-cmdline-extra=%s", escaped_kcl); +- if (r < 0) +- return log_oom(); +- +- r = strv_extend(&cmdline, "-smbios"); +- if (r < 0) +- return log_oom(); +- +- r = strv_extendf(&cmdline, "type=11,value=io.systemd.boot.kernel-cmdline-extra=%s", escaped_kcl); +- if (r < 0) +- return log_oom(); +- } else +- log_warning("Cannot append extra args to kernel cmdline, native architecture doesn't support SMBIOS, ignoring"); +- } ++ } else if (!strv_isempty(arg_kernel_cmdline_extra)) ++ log_warning("Cannot append extra args to kernel cmdline when booting via a boot loader or UKI, " ++ "the SMBIOS Type 11 OEM string mechanism previously used for this is no longer " ++ "supported by sd-boot/the UKI stub, ignoring"); + } else + log_warning("Cannot append extra args to kernel cmdline, native architecture doesn't support SMBIOS"); + +-- +2.52.0 + diff --git a/packages/systemd-257/loader.conf b/packages/systemd-257/loader.conf new file mode 100644 index 000000000..50be1716f --- /dev/null +++ b/packages/systemd-257/loader.conf @@ -0,0 +1,31 @@ +# There is no interactive user at the the console, so the defaults below +# aim for "boot straight through, expose nothing extra, keep exactly +# one local recovery path." +# +# Note that this file is measured into TPM PCR 5 before parsing: editing +# it changes PCR 5 and invalidates anything sealed against it. + +# Boot the default entry immediately, with no menu delay. +timeout menu-disabled + +# Leave the firmware-selected console text mode alone. +console-mode keep + +# Disable the interactive kernel command line editor. +editor no + +# Do not synthesize "other OS" menu entries. +auto-entries no + +# Drop the "Reboot Into Firmware Interface" menu entry. +auto-firmware no + +# No "Power Off"/"Reboot The System" menu entries. +auto-poweroff no +auto-reboot no + +# No PC-speaker beeping during the countdown. +beep no + +# Never scan /loader/keys or enroll Secure Boot keys from the ESP. +secure-boot-enroll off diff --git a/packages/systemd-257/systemd-257.spec b/packages/systemd-257/systemd-257.spec index fa88b7cf2..4ce3bac96 100644 --- a/packages/systemd-257/systemd-257.spec +++ b/packages/systemd-257/systemd-257.spec @@ -3,6 +3,14 @@ %global package_priority_epoch 0 +# Install the systemd-boot bootloader at the same location as the GRUB and +# shim EFI binaries, so image builds find all ESP loaders in one place. +%global efidir /boot/efi/EFI/BOOT + +# systemd-boot reads its own configuration from /loader/loader.conf on the +# volume it was loaded from, i.e. the ESP, never from XBOOTLDR. +%global loaderdir /boot/efi/loader + Name: %{_cross_os}systemd-257 Version: 257.13 Release: 1%{?dist} @@ -17,6 +25,7 @@ Source103: org.freedesktop.systemd1.toml Source1: systemd-mount-rate-bootconfig.conf Source2: systemd-cgroup-legacy-force-bootconfig.conf +Source3: loader.conf # Backport of upstream patch to change `Failed to execute No such file # or directory` error logs to debug @@ -81,6 +90,11 @@ Patch9016: 9016-bootctl-disable-secure-boot-autoenroll.patch # Patch meson to set OPENSSL_NO_UI_CONSOLE CFLAGS for the build Patch9017: 9017-meson-set-DOPENSSL_NO_UI_CONSOLE-when-using-openssl.patch +# Patch meson to skip unsupported architecture builds +Patch9018: 9018-build-correct-sd-boot-EFI-arch-on-the-Bottlerocket-S.patch +# Remove undesired SMBIOS functionality +Patch9019: 9019-boot-remove-SMBIOS-Type-11-kernel-cmdline-extra-mech.patch +Patch9020: 9020-boot-vmspawn-finish-removing-SMBIOS-cmdline-extra-bi.patch BuildRequires: gperf BuildRequires: intltool @@ -175,6 +189,17 @@ Conflicts: %{_cross_os}auditd %description journald-audit %{summary}. +%package bootloader +Summary: EFI boot loader and stub binaries +Requires: %{name} +Requires: %{_cross_os}image-feature(uki-image) +Provides: %{_cross_os}bootloader(efi) +Conflicts: %{_cross_os}image-feature(no-uki-image) +Conflicts: %{_cross_os}image-feature(in-place-updates) + +%description bootloader +%{summary}. + %prep %autosetup -n systemd-%{version} -p1 @@ -299,7 +324,13 @@ CONFIGURE_OPTS=( -Dglib=disabled -Ddbus=disabled - -Dbootloader=disabled + -Dbootloader=enabled + + -Dsbat-distro='bottlerocket' + -Dsbat-distro-generation=1 + -Dsbat-distro-summary='Bottlerocket' + -Dsbat-distro-pkgname='systemd-boot' + -Dsbat-distro-url='https://github.com/bottlerocket-os/bottlerocket/' -Dbashcompletiondir=no -Dzshcompletiondir=no @@ -372,6 +403,20 @@ install -d %{buildroot}%{_cross_bootconfigdir} install -p -m 0644 %{S:1} %{buildroot}%{_cross_bootconfigdir}/20-mount-rate-limit-burst.conf install -p -m 0644 %{S:2} %{buildroot}%{_cross_bootconfigdir}/21-cgroup-enable-legacy-force.conf +# Relocate the systemd-boot EFI binary from the meson install location to +# %%{efidir}, matching the GRUB and shim convention. The UKI stub +# (linux%%{_cross_efi_arch}.efi.stub) stays in %%{_cross_libdir}/systemd/boot/efi +# since it is a build-time input for ukify, not an ESP loader. +install -d %{buildroot}%{efidir} +mv %{buildroot}%{_cross_libdir}/systemd/boot/efi/systemd-boot%{_cross_efi_arch}.efi \ + %{buildroot}%{efidir}/systemd-boot%{_cross_efi_arch}.efi + +# Ship sensible systemd-boot defaults. This has to land on the ESP rather than +# the boot partition, since systemd-boot only reads /loader/loader.conf from +# the volume it was loaded from. +install -d %{buildroot}%{loaderdir} +install -p -m 0644 %{S:3} %{buildroot}%{loaderdir}/loader.conf + %files %license LICENSE.GPL2 LICENSE.LGPL2.1 %{_cross_attribution_file} @@ -451,6 +496,25 @@ install -p -m 0644 %{S:2} %{buildroot}%{_cross_bootconfigdir}/21-cgroup-enable-l %exclude %{_cross_systemdgeneratordir}/systemd-ssh-generator %exclude %{_cross_systemdgeneratordir}/systemd-gpt-auto-generator +# Bottlerocket does not ship the systemd-boot bootloader runtime or the +# TPM measured-boot/pcrlock subsystem; we build -Dbootloader=enabled only to +# produce sd-stub/sd-boot EFI binaries for UKI assembly. Exclude the whole +# subsystem (units, wants symlinks, generator, and helper binaries). +%exclude %{_cross_unitdir}/systemd-bless-boot* +%exclude %{_cross_unitdir}/systemd-boot-random-seed* +%exclude %{_cross_unitdir}/systemd-boot-update* +%exclude %{_cross_unitdir}/systemd-bootctl* +%exclude %{_cross_unitdir}/systemd-pcr* +%exclude %{_cross_unitdir}/systemd-tpm2-setup* +%exclude %{_cross_unitdir}/*.target.wants/systemd-boot-random-seed.service +%exclude %{_cross_unitdir}/*.target.wants/systemd-pcr* +%exclude %{_cross_unitdir}/*.target.wants/systemd-tpm2-setup* +%exclude %{_cross_unitdir}/*.target.wants/systemd-bootctl.socket +%exclude %{_cross_systemdgeneratordir}/systemd-bless-boot-generator +%exclude %{_cross_libdir}/systemd/systemd-bless-boot +%exclude %{_cross_libdir}/systemd/systemd-tpm2-setup +%exclude %{_cross_libdir}/systemd/systemd-pcrextend + %dir %{_cross_libdir}/systemd/system-preset %{_cross_libdir}/systemd/system-preset/90-systemd.preset @@ -887,3 +951,11 @@ install -p -m 0644 %{S:2} %{buildroot}%{_cross_bootconfigdir}/21-cgroup-enable-l %files journald-audit %{_cross_unitdir}/systemd-journald-audit.socket + +%files bootloader +%dir %{efidir} +%{efidir}/systemd-boot%{_cross_efi_arch}.efi +%dir %{loaderdir} +%{loaderdir}/loader.conf +%dir %{_cross_libdir}/systemd/boot +%{_cross_libdir}/systemd/boot/efi/ From 901706e4936dc57b77fd275a57ffcee5ce500841 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Mon, 10 Aug 2026 23:23:46 +0000 Subject: [PATCH 2/9] release: load image-format env file in prepare-boot service Add EnvironmentFile=-/usr/share/bottlerocket/image-format.env to prepare-boot.service so prairiedog can read image-format variables at boot preparation time. Signed-off-by: Arnaldo Garcia Rincon --- packages/release/prepare-boot.service | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/release/prepare-boot.service b/packages/release/prepare-boot.service index cffe8dc00..9a8b243f2 100644 --- a/packages/release/prepare-boot.service +++ b/packages/release/prepare-boot.service @@ -6,6 +6,7 @@ DefaultDependencies=no [Service] Type=oneshot +EnvironmentFile=-/usr/share/bottlerocket/image-format.env # We need a helper program to mount the boot partition since it isn't # trivial to figure out which partition set is active ExecStart=/usr/bin/prairiedog prepare-boot From 1dcafe8f020b9bcbec117275c76bbcf5c12127a1 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Tue, 11 Aug 2026 00:40:37 +0000 Subject: [PATCH 3/9] release: skip PCR9 kernel cmdline measurement for UKI On UKI-based images, the kernel command line is measured into PCR9 as part of the UKI PE binary at boot, so the separate user-space measurement in measure-cmdline.service is redundant. Gate the service with an ExecCondition on UKI_IMAGE so it only runs on non-UKI images. Signed-off-by: Arnaldo Garcia Rincon --- packages/release/measure-cmdline.service | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/release/measure-cmdline.service b/packages/release/measure-cmdline.service index 53da91fa6..315c665cd 100644 --- a/packages/release/measure-cmdline.service +++ b/packages/release/measure-cmdline.service @@ -10,6 +10,12 @@ RefuseManualStop=true [Service] Type=oneshot +EnvironmentFile=-/usr/share/bottlerocket/image-format.env + +# Skip on UKI images; the kernel command line is measured as part of the +# UKI PE binary itself, not extended separately here. +ExecCondition=[ "${UKI_IMAGE}" != "true" ] + ExecStart=/usr/bin/rottweiler measure kernel-command-line RemainAfterExit=true From 0f3847e68996e5e7f1e63ea300c5ddac4f0e3984 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Mon, 10 Aug 2026 22:58:49 +0000 Subject: [PATCH 4/9] release: prepare for UKI support Prior to the introduction of systemd-boot, GRUB was the only supported bootloader and was always explicitly required. With systemd-boot now available, the bootloader must be selected based on the format of the built image. Require the (bootloader-efi) capability, which is satisfied by either grub or systemd-boot, whichever is installed based on the variant's enabled feature flags. Default to grub when the (uki-image) bconds are absent, preserving backwards compatibility with older twoliter versions. Signed-off-by: Arnaldo Garcia Rincon --- packages/release/release.spec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/release/release.spec b/packages/release/release.spec index 6baa88d98..44c30273b 100644 --- a/packages/release/release.spec +++ b/packages/release/release.spec @@ -165,7 +165,10 @@ Requires: %{_cross_os}filesystem Requires: %{_cross_os}findutils Requires: %{_cross_os}glibc Requires: %{_cross_os}grep -Requires: %{_cross_os}grub +# For newer versions of twoliter that support UKIs, explicitly request the bootloader(efi) capability +Requires: (%{_cross_os}bootloader(efi) if (%{_cross_os}image-feature(uki-image) or %{_cross_os}image-feature(no-uki-image))) +# Older versions of twoliter that don't support UKIs always get GRUB +Requires: (%{_cross_os}grub or %{_cross_os}image-feature(uki-image) or %{_cross_os}image-feature(no-uki-image)) Requires: %{_cross_os}iproute Requires: %{_cross_os}iptables Requires: %{_cross_os}kexec-tools From e9e418ae209d402ac2f6c785e4c2ed2b03040928 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Mon, 10 Aug 2026 22:59:32 +0000 Subject: [PATCH 5/9] os: skip signpost as a binary for UKIs UKIs do not yet support in-place upgrades. Skip installing signpost on UKI-based variants, since marking partitions with successful boots only applies to the GRUB-based boot format. Signed-off-by: Arnaldo Garcia Rincon --- packages/os/os.spec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/os/os.spec b/packages/os/os.spec index 5fd5994aa..6c10019ef 100644 --- a/packages/os/os.spec +++ b/packages/os/os.spec @@ -105,7 +105,6 @@ Requires: %{_cross_os}metricdog Requires: %{_cross_os}prairiedog Requires: %{_cross_os}schnauzer Requires: %{_cross_os}settings-committer -Requires: %{_cross_os}signpost Requires: %{_cross_os}storewolf Requires: %{_cross_os}sundog Requires: %{_cross_os}xfscli @@ -120,6 +119,8 @@ Requires: (%{_cross_os}migration or %{_cross_os}image-feature(no-in-place-update Requires: (%{_cross_os}thar-be-updates or %{_cross_os}image-feature(no-in-place-updates)) Requires: (%{_cross_os}updog or %{_cross_os}image-feature(no-in-place-updates)) +Requires: (%{_cross_os}signpost or %{_cross_os}image-feature(uki-image)) + Requires: (%{_cross_os}pluto if %{_cross_os}variant-family(aws-k8s)) Requires: (%{_cross_os}shibaken if %{_cross_os}variant-platform(aws)) Requires: (%{_cross_os}cfsignal if %{_cross_os}variant-platform(aws)) @@ -239,6 +240,7 @@ Requires: %{_cross_os}nvme-cli %package -n %{_cross_os}signpost Summary: Bottlerocket GPT priority querier/switcher +Conflicts: %{_cross_os}image-feature(uki-image) %description -n %{_cross_os}signpost %{summary}. From f40cb99c765a3f993c1872e722e868e8b61ba188 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Mon, 10 Aug 2026 23:26:50 +0000 Subject: [PATCH 6/9] signpost: add helper to find the xbootldr partition Add xbootldr_partition(), which scans the OS disk's GPT for a partition of the XBOOTLDR type GUID and returns its path. Returns None when no such partition exists, which is the case on GRUB images, and fails if more than one is found since there would be no way to choose between them. A prior pass introduced generic disk- and partition-type-searching abstractions (disk.rs, partition_types.rs) to support this lookup, but a single-purpose function is simpler and sufficient for the one caller that needs it, so those files are removed in favor of this helper living alongside the rest of the partition-table logic in state.rs. Signed-off-by: Arnaldo Garcia Rincon --- sources/updater/signpost/src/error.rs | 14 + sources/updater/signpost/src/lib.rs | 2 +- sources/updater/signpost/src/state.rs | 418 ++++++++++++++++++++++---- 3 files changed, 382 insertions(+), 52 deletions(-) diff --git a/sources/updater/signpost/src/error.rs b/sources/updater/signpost/src/error.rs index 961f8a691..b9374363f 100644 --- a/sources/updater/signpost/src/error.rs +++ b/sources/updater/signpost/src/error.rs @@ -64,6 +64,20 @@ pub enum Error { successful: bool, }, + #[snafu(display( + "Found {} partitions of type {} on the OS disk, expected at most one: {:?}", + partitions.len(), + partition_type, + partitions + ))] + MultiplePartitionsOfType { + partition_type: String, + partitions: Vec, + }, + + #[snafu(display("No XBOOTLDR partition found on the OS disk"))] + NoXbootldrPartition, + #[snafu(display("Failed to open {} for {}: {}", path.display(), what, source))] Open { path: PathBuf, diff --git a/sources/updater/signpost/src/lib.rs b/sources/updater/signpost/src/lib.rs index 68df302f3..934da56b9 100644 --- a/sources/updater/signpost/src/lib.rs +++ b/sources/updater/signpost/src/lib.rs @@ -7,4 +7,4 @@ mod state; pub use error::{Error, GPTError}; pub use guid::uuid_to_guid; pub use set::PartitionSet; -pub use state::State; +pub use state::{DiskLayout, State}; diff --git a/sources/updater/signpost/src/state.rs b/sources/updater/signpost/src/state.rs index 002f582b8..27c32b8ab 100644 --- a/sources/updater/signpost/src/state.rs +++ b/sources/updater/signpost/src/state.rs @@ -15,6 +15,12 @@ const BOTTLEROCKET_ROOT: [u8; 16] = uuid_to_guid(hex!("5526016a 1a97 4ea4 b39a b const BOTTLEROCKET_HASH: [u8; 16] = uuid_to_guid(hex!("598f10af c955 4456 6a99 7720068a6cea")); const BOTTLEROCKET_PRIVATE: [u8; 16] = uuid_to_guid(hex!("440408bb eb0b 4328 a6e5 a29038fad706")); +/// The extended boot loader partition, as defined by the Discoverable Partitions Specification. +/// +/// UKI images tag their FAT boot partition with this type so systemd-boot can find it; GRUB +/// images have no partition of this type, so its presence identifies a UKI layout at runtime. +const XBOOTLDR: [u8; 16] = uuid_to_guid(hex!("bc13c2ff 59e6 4262 a352 b275fd6f7172")); + #[derive(Debug, Clone)] pub struct State { os_disk: PathBuf, @@ -29,36 +35,20 @@ pub struct State { table: GPT, } -#[derive(Debug, Clone)] -enum PartitionSets { - Single(SinglePartitionSet), - Dual(DualPartitionSet), -} - -#[derive(Debug, Clone)] -struct SinglePartitionSet { - set: PartitionSet, -} - -#[derive(Debug, Clone)] -struct DualPartitionSet { - sets: [PartitionSet; 2], - active: SetSelect, +/// The OS disk backing the running root filesystem, and its parsed partition table. +/// +/// [`DiskLayout::scan`] does the disk open and GPT parse once, shared by both +/// [`DiskLayout::into_state`] (A/B partition state) and [`DiskLayout::xbootldr`] (XBOOTLDR +/// discovery). +pub struct DiskLayout { + os_disk: BlockDevice, + active_partition: PathBuf, + table: GPT, } -impl State { - /// Finds the partition sets available on disk, and determines which is active under the root - /// filesystem. - /// - /// * Finds the device corresponding to the root filesystem mount (`/`), which is assumed to be - /// a dm-verity device. - /// * Gets the first lower device, which will either be the root or hash partition of the - /// active partition set. - /// * Find the first and second partitions matching each of the boot, root, and hash partition - /// type GUIDs. The first partitions are set A and the second partitions are set B. - /// * Determine which partition set is active by finding which one contains the partition we - /// found from our root filesystem earlier. - pub fn load() -> Result { +impl DiskLayout { + /// Opens the OS disk backing the running root filesystem and parses its partition table. + pub fn scan() -> Result { // The root filesystem is a dm-verity device. We want to determine what disk and partition // the backing data is part of. Look up the device major and minor via stat(2): let root_fs = BlockDevice::from_device_path("/") @@ -93,37 +83,55 @@ impl State { device: os_disk.path(), })?; - // Finds the nth partition on `table` matching the partition type GUID `guid`. + Ok(Self { + os_disk, + active_partition, + table, + }) + } + + /// Returns the device path of the XBOOTLDR partition on the OS disk. + /// + /// Fails if the disk has no partition of that type (the case on GRUB images) or more than + /// one. + pub fn xbootldr(&self) -> Result { + resolve_xbootldr_partition(&self.os_disk, &self.table) + } + + /// Interprets the scanned table as a GRUB-era A/B partition layout, using the partition + /// found under the running root filesystem to determine which set (A or B) is active. + pub fn into_state(self) -> Result { + let Self { + os_disk, + active_partition, + table, + } = self; + + // Finds the nth partition on `table` matching the partition type GUID `guid`, ignoring + // unused entries. let nth_guid = |guid, n| -> Option { table .iter() - .filter(|(_, p)| p.partition_type_guid == guid) + .filter(|(_, p)| p.is_used() && p.partition_type_guid == guid) .nth(n) .map(|(n, _)| n) }; - let required_guid = |guid, n| -> Result { + // `part_type` is the human-readable name for the error message; it can't be derived + // from `guid` at runtime, so callers must pass it explicitly. + let required_guid = |guid, n, part_type| -> Result { nth_guid(guid, n).context(error::PartitionMissingFromSetSnafu { - part_type: stringify!(guid), + part_type, set: if n == 0 { "A" } else { "B" }, }) }; // Loads the path to partition number `num` on the OS disk. - let device_from_part_num = |num| -> Result { - Ok(os_disk - .partition(num) - .context(error::PartitionFromDiskSnafu { - device: os_disk.path(), - })? - .context(error::PartitionNotFoundOnDeviceSnafu { - num, - device: os_disk.path(), - })? - .path()) - }; + let device_from_part_num = + |num| -> Result { self::device_from_part_num(&os_disk, num) }; - let mut boot_partition_nums = vec![required_guid(BOTTLEROCKET_BOOT, 0)?]; + let mut boot_partition_nums = + vec![required_guid(BOTTLEROCKET_BOOT, 0, "BOTTLEROCKET_BOOT")?]; if let Some(b) = nth_guid(BOTTLEROCKET_BOOT, 1) { boot_partition_nums.push(b); } @@ -131,15 +139,23 @@ impl State { let mut sets = Vec::new(); sets.push(PartitionSet { boot: device_from_part_num(boot_partition_nums[0])?, - root: device_from_part_num(required_guid(BOTTLEROCKET_ROOT, 0)?)?, - hash: device_from_part_num(required_guid(BOTTLEROCKET_HASH, 0)?)?, + root: device_from_part_num(required_guid(BOTTLEROCKET_ROOT, 0, "BOTTLEROCKET_ROOT")?)?, + hash: device_from_part_num(required_guid(BOTTLEROCKET_HASH, 0, "BOTTLEROCKET_HASH")?)?, }); if boot_partition_nums.len() == 2 { sets.push(PartitionSet { boot: device_from_part_num(boot_partition_nums[1])?, - root: device_from_part_num(required_guid(BOTTLEROCKET_ROOT, 1)?)?, - hash: device_from_part_num(required_guid(BOTTLEROCKET_HASH, 1)?)?, + root: device_from_part_num(required_guid( + BOTTLEROCKET_ROOT, + 1, + "BOTTLEROCKET_ROOT", + )?)?, + hash: device_from_part_num(required_guid( + BOTTLEROCKET_HASH, + 1, + "BOTTLEROCKET_HASH", + )?)?, }); } @@ -165,14 +181,114 @@ impl State { }) }; - Ok(Self { + Ok(State { os_disk: os_disk.path(), - private_partition_num: required_guid(BOTTLEROCKET_PRIVATE, 0)?, + private_partition_num: required_guid(BOTTLEROCKET_PRIVATE, 0, "BOTTLEROCKET_PRIVATE")?, sets, boot_partition_nums, table, }) } +} + +/// Finds the partition numbers of every XBOOTLDR-typed partition on `table`, in ascending +/// order. UKI images tag their boot partition with this type GUID; GRUB images have none. +/// +/// Takes an already-parsed `&GPT` so this filter is unit-testable without a real block device; +/// returns bare numbers since resolving them to paths needs the caller's `BlockDevice` handle. +fn xbootldr_partition_nums(table: &GPT) -> Vec { + table + .iter() + .filter(|(_, p)| p.is_used() && p.partition_type_guid == XBOOTLDR) + .map(|(num, _)| num) + .collect() +} + +/// Loads the path to partition number `num` on `os_disk`. +fn device_from_part_num(os_disk: &BlockDevice, num: u32) -> Result { + Ok(os_disk + .partition(num) + .context(error::PartitionFromDiskSnafu { + device: os_disk.path(), + })? + .context(error::PartitionNotFoundOnDeviceSnafu { + num, + device: os_disk.path(), + })? + .path()) +} + +/// Resolves the XBOOTLDR partition on `table` to a device path on `os_disk`. Fails if no +/// XBOOTLDR partition is found, or if more than one is found, since there would be no way to +/// choose between them. +fn resolve_xbootldr_partition(os_disk: &BlockDevice, table: &GPT) -> Result { + let xbootldr_nums = xbootldr_partition_nums(table); + ensure!( + xbootldr_nums.len() <= 1, + error::MultiplePartitionsOfTypeSnafu { + partition_type: guid_to_string(XBOOTLDR), + partitions: xbootldr_nums + .iter() + .copied() + .map(|num| device_from_part_num(os_disk, num)) + .collect::, Error>>()?, + } + ); + let num = xbootldr_nums + .first() + .copied() + .context(error::NoXbootldrPartitionSnafu)?; + device_from_part_num(os_disk, num) +} + +/// Formats a GPT partition type GUID as its canonical mixed-endian string form (first three +/// fields little-endian, last two big-endian on disk). +fn guid_to_string(g: [u8; 16]) -> String { + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-\ + {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + g[3], + g[2], + g[1], + g[0], + g[5], + g[4], + g[7], + g[6], + g[8], + g[9], + g[10], + g[11], + g[12], + g[13], + g[14], + g[15], + ) +} + +#[derive(Debug, Clone)] +enum PartitionSets { + Single(SinglePartitionSet), + Dual(DualPartitionSet), +} + +#[derive(Debug, Clone)] +struct SinglePartitionSet { + set: PartitionSet, +} + +#[derive(Debug, Clone)] +struct DualPartitionSet { + sets: [PartitionSet; 2], + active: SetSelect, +} + +impl State { + /// Finds the partition sets available on disk, and determines which is active under the root + /// filesystem. + pub fn load() -> Result { + DiskLayout::scan()?.into_state() + } pub(crate) fn os_disk(&self) -> &Path { &self.os_disk @@ -424,3 +540,203 @@ impl fmt::Display for State { } } } + +#[cfg(test)] +mod tests { + use super::{ + guid_to_string, xbootldr_partition_nums, BOTTLEROCKET_BOOT, BOTTLEROCKET_HASH, + BOTTLEROCKET_PRIVATE, BOTTLEROCKET_ROOT, XBOOTLDR, + }; + use crate::error::{self, Error}; + use gptman::{GPTPartitionEntry, GPT}; + use snafu::OptionExt; + use std::io::Cursor; + + const SECTOR_SIZE: u64 = 512; + + /// Builds an in-memory partition table whose partitions have the given type GUIDs, in order + /// starting at partition number 1. + fn table_with_types(types: &[[u8; 16]]) -> GPT { + let mut disk = Cursor::new(vec![0u8; 1024 * SECTOR_SIZE as usize]); + let mut table = + GPT::new_from(&mut disk, SECTOR_SIZE, [0xff; 16]).expect("could not build a GPT"); + + for (i, partition_type) in types.iter().enumerate() { + let num = u32::try_from(i).unwrap() + 1; + let starting_lba = 2048 + u64::from(num) * 64; + table[num] = GPTPartitionEntry { + partition_type_guid: *partition_type, + unique_partition_guid: [num as u8; 16], + starting_lba, + ending_lba: starting_lba + 63, + attribute_bits: 0, + partition_name: "test".into(), + }; + } + + table + } + + /// Finds the numbers of the partitions in `table` matching `partition_type`, in ascending + /// order. Mirrors the private `nth_guid` filter predicate in `DiskLayout::into_state`, since + /// that closure can't be called directly from a unit test. + fn partition_nums_with_type(table: &GPT, partition_type: [u8; 16]) -> Vec { + table + .iter() + .filter(|(_, p)| p.is_used() && p.partition_type_guid == partition_type) + .map(|(num, _)| num) + .collect() + } + + #[test] + fn finds_partitions_of_a_type_in_order() { + // A GRUB layout: two boot partitions and the private partition. + let table = table_with_types(&[BOTTLEROCKET_BOOT, BOTTLEROCKET_PRIVATE, BOTTLEROCKET_BOOT]); + + assert_eq!( + partition_nums_with_type(&table, BOTTLEROCKET_BOOT), + vec![1, 3] + ); + assert_eq!( + partition_nums_with_type(&table, BOTTLEROCKET_PRIVATE), + vec![2] + ); + } + + #[test] + fn ignores_unused_and_other_types() { + // A UKI layout: one XBOOTLDR boot partition and the private partition. The rest of the + // partition array is unused, i.e. has an all-zero type GUID. + let table = table_with_types(&[XBOOTLDR, BOTTLEROCKET_PRIVATE]); + + assert_eq!(partition_nums_with_type(&table, XBOOTLDR), vec![1]); + // The GRUB boot type must not match the XBOOTLDR partition, which is what tells the two + // layouts apart. + assert!(partition_nums_with_type(&table, BOTTLEROCKET_BOOT).is_empty()); + assert!(partition_nums_with_type(&table, [0u8; 16]).is_empty()); + } + + #[test] + fn is_used_matches_nonzero_type_guid() { + // gptman's `is_used()` is defined purely in terms of the type GUID being non-zero + // (`is_used() == (partition_type_guid != [0; 16])`). Confirm that equivalence holds, so + // a future gptman upgrade that changes `is_used()`'s definition is caught here instead + // of silently changing which partitions `DiskLayout::into_state` and the XBOOTLDR scan + // consider. + let mut disk = Cursor::new(vec![0u8; 1024 * SECTOR_SIZE as usize]); + let mut table = + GPT::new_from(&mut disk, SECTOR_SIZE, [0xff; 16]).expect("could not build a GPT"); + table[1] = GPTPartitionEntry { + partition_type_guid: XBOOTLDR, + unique_partition_guid: [1u8; 16], + starting_lba: 2048, + ending_lba: 2048 + 63, + attribute_bits: 0, + partition_name: "test".into(), + }; + + assert!(table[1].is_used()); + // partition 2 was never assigned, so its type GUID is still all-zero. + assert!(!table[2].is_used()); + } + + #[test] + fn xbootldr_guid_byte_order() { + // The canonical form, from `partyplanner`, is "bc13c2ff-59e6-4262-a352-b275fd6f7172". On + // disk the first three fields are little-endian and the last two are big-endian, so the + // raw bytes are the first three fields reversed followed by the rest unchanged. + assert_eq!( + XBOOTLDR, + [ + 0xff, 0xc2, 0x13, 0xbc, // bc13c2ff, little-endian + 0xe6, 0x59, // 59e6, little-endian + 0x62, 0x42, // 4262, little-endian + 0xa3, 0x52, // a352, big-endian + 0xb2, 0x75, 0xfd, 0x6f, 0x71, 0x72, // b275fd6f7172, big-endian + ] + ); + } + + #[test] + fn guid_to_string_matches_canonical_xbootldr_form() { + // Checked against the same canonical form documented in `xbootldr_guid_byte_order` above. + assert_eq!( + guid_to_string(XBOOTLDR), + "bc13c2ff-59e6-4262-a352-b275fd6f7172" + ); + } + + #[test] + fn bottlerocket_boot_guid_byte_order() { + // Bottlerocket's boot partition typecode spells out a message on disk. + assert_eq!(BOTTLEROCKET_BOOT, *b"hack the planet!"); + } + + #[test] + fn xbootldr_partition_nums_finds_uki_partition_without_bottlerocket_boot() { + // A UKI layout has no BOTTLEROCKET_BOOT partition at all; `xbootldr_partition_nums` must + // find the XBOOTLDR partition without requiring BOTTLEROCKET_BOOT to be present. + let table = table_with_types(&[ + XBOOTLDR, + BOTTLEROCKET_ROOT, + BOTTLEROCKET_HASH, + BOTTLEROCKET_PRIVATE, + ]); + + assert_eq!(xbootldr_partition_nums(&table), vec![1]); + assert!(partition_nums_with_type(&table, BOTTLEROCKET_BOOT).is_empty()); + } + + #[test] + fn xbootldr_partition_nums_is_empty_on_a_grub_layout() { + // A GRUB layout has no XBOOTLDR partition; `xbootldr_partition_nums` must return no + // matches, while the GRUB-era boot/root/hash/private partitions are still found normally. + let table = table_with_types(&[ + BOTTLEROCKET_BOOT, + BOTTLEROCKET_ROOT, + BOTTLEROCKET_HASH, + BOTTLEROCKET_BOOT, + BOTTLEROCKET_ROOT, + BOTTLEROCKET_HASH, + BOTTLEROCKET_PRIVATE, + ]); + + assert!(xbootldr_partition_nums(&table).is_empty()); + assert_eq!( + partition_nums_with_type(&table, BOTTLEROCKET_BOOT), + vec![1, 4] + ); + assert_eq!( + partition_nums_with_type(&table, BOTTLEROCKET_ROOT), + vec![2, 5] + ); + assert_eq!( + partition_nums_with_type(&table, BOTTLEROCKET_HASH), + vec![3, 6] + ); + assert_eq!( + partition_nums_with_type(&table, BOTTLEROCKET_PRIVATE), + vec![7] + ); + } + + #[test] + fn missing_xbootldr_partition_is_an_error() { + // Exercises the same `.first().copied().context(...)` lookup that + // `resolve_xbootldr_partition` uses, since that function takes a `&BlockDevice` and + // can't be constructed from a synthetic table in a unit test. + let table = table_with_types(&[ + BOTTLEROCKET_BOOT, + BOTTLEROCKET_ROOT, + BOTTLEROCKET_HASH, + BOTTLEROCKET_PRIVATE, + ]); + + let result: Result = xbootldr_partition_nums(&table) + .first() + .copied() + .context(error::NoXbootldrPartitionSnafu); + + assert!(matches!(result, Err(Error::NoXbootldrPartition))); + } +} From b7117d047811b90a0f6ecf12fff2b306c2ed8d92 Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Thu, 13 Aug 2026 08:00:47 +0000 Subject: [PATCH 7/9] prairiedog: mount xbootldr partition on uki images prepare_boot previously always mounted the active partition set's ext4 boot partition. On UKI images, the boot partition is XBOOTLDR, a FAT filesystem that systemd-boot's firmware loader must be able to read. Scan the disk layout and check the UKI_IMAGE environment variable, which prepare-boot.service sets from image-format.env. If it is a UKI image, look up the XBOOTLDR partition and mount it as vfat with the mount options systemd-boot expects (umask, shortname, iocharset, and an SELinux context, since FAT has no extended attributes to carry a label). Otherwise, fall back to the existing ext4 mount of the active set's boot partition. Signed-off-by: Arnaldo Garcia Rincon --- sources/api/prairiedog/src/error.rs | 6 ++ sources/api/prairiedog/src/main.rs | 92 +++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/sources/api/prairiedog/src/error.rs b/sources/api/prairiedog/src/error.rs index d2609b5f0..4a6f3dbfa 100644 --- a/sources/api/prairiedog/src/error.rs +++ b/sources/api/prairiedog/src/error.rs @@ -41,6 +41,12 @@ pub(super) enum Error { path: PathBuf, }, + #[snafu(display("Failed to scan the OS disk's partition table: {}", source))] + ScanDiskLayout { source: SignpostError }, + + #[snafu(display("Failed to scan the OS disk for the XBOOTLDR partition: {}", source))] + ScanForXbootldrPartition { source: SignpostError }, + #[snafu(display("Failed to setup mount '{}': '{}'", path, source))] SetupMount { path: String, source: nix::Error }, diff --git a/sources/api/prairiedog/src/main.rs b/sources/api/prairiedog/src/main.rs index 6b7a5a3f6..2edc93e3f 100644 --- a/sources/api/prairiedog/src/main.rs +++ b/sources/api/prairiedog/src/main.rs @@ -18,6 +18,7 @@ use crate::error::Result; use argh::FromArgs; use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger, WriteLogger}; use snafu::{ensure, ResultExt}; +use std::env; use std::ffi::OsStr; use std::fs::{self, File}; use std::path::{Path, PathBuf}; @@ -38,6 +39,26 @@ const KEXEC_PATH: &str = "/sbin/kexec"; // Mount points created prairiedog const BOOT_MOUNT_PATH: &str = "/boot"; +// Filesystems used for the boot partition. GRUB images use ext4; UKI images use FAT, since +// systemd-boot has to be able to read the partition from the firmware. +const BOOT_FS_EXT4: &str = "ext4"; +const BOOT_FS_VFAT: &str = "vfat"; + +// Mount options for the FAT boot partition on UKI images. +// +// FAT stores no ownership or permission bits, so they are set for the whole filesystem: only root +// may read it. `shortname=winnt` and `iocharset=iso8859-1` keep name handling predictable. +// +// FAT also stores no extended attributes, so SELinux labels can't live on the filesystem, and +// there is no fcontext rule for /boot to fall back on because it is a separate filesystem. The +// label therefore has to be applied at mount time. +const VFAT_MOUNT_DATA: &str = + "umask=0077,shortname=winnt,iocharset=iso8859-1,context=system_u:object_r:os_t:s0"; + +// Set by prepare-boot.service from /usr/share/bottlerocket/image-format.env. Only consulted when +// the partition table doesn't tell us which boot layout the image uses. +const UKI_IMAGE_ENV: &str = "UKI_IMAGE"; + // Files generated by prairiedog const KDUMP_LOGS_PATH: &str = "/var/log/kdump"; const LOG_FILE: &str = "prairiedog.log"; @@ -186,11 +207,59 @@ fn capture_dump() -> Result<()> { Ok(()) } -// Mounts the active boot partition +// Mounts the boot partition of the active partition set, or the XBOOTLDR partition on UKI images fn prepare_boot() -> Result<()> { - // Get the current partitions state - let state = signpost::State::load().context(error::LoadStateSnafu)?; - let boot_partition_path = &state.active_set().boot; + let layout = signpost::DiskLayout::scan().context(error::ScanDiskLayoutSnafu)?; + + if uki_image_from_env() { + let boot_partition_path = layout + .xbootldr() + .context(error::ScanForXbootldrPartitionSnafu)?; + info!( + "UKI image detected (XBOOTLDR partition '{}')", + boot_partition_path.display() + ); + mount_boot_partition(&boot_partition_path, BOOT_FS_VFAT, Some(VFAT_MOUNT_DATA)) + } else { + info!("GRUB image detected; using the boot partition of the active partition set"); + let state = layout.into_state().context(error::LoadStateSnafu)?; + mount_boot_partition(&state.active_set().boot, BOOT_FS_EXT4, None) + } +} + +/// Reports whether the image format environment says this is a UKI image. +fn uki_image_from_env() -> bool { + match env::var(UKI_IMAGE_ENV) { + Ok(value) if value.trim().eq_ignore_ascii_case("true") => { + info!("{UKI_IMAGE_ENV} is set to '{value}' in the environment"); + true + } + Ok(value) => { + warn!( + "{UKI_IMAGE_ENV} is set to '{value}' in the environment, which is not a \ + recognized true value; assuming this is not a UKI image" + ); + false + } + Err(env::VarError::NotUnicode(value)) => { + warn!( + "{UKI_IMAGE_ENV} is set in the environment to a value that is not valid UTF-8 \ + ({}); assuming this is not a UKI image", + value.to_string_lossy() + ); + false + } + Err(env::VarError::NotPresent) => { + info!( + "{UKI_IMAGE_ENV} is not set in the environment; assuming this is not a UKI image" + ); + false + } + } +} + +/// Mounts `partition_path` read-only on `BOOT_MOUNT_PATH`, then makes the mount private. +fn mount_boot_partition(partition_path: &Path, fstype: &str, data: Option<&str>) -> Result<()> { let flags = nix::mount::MsFlags::MS_NOSUID | nix::mount::MsFlags::MS_NODEV | nix::mount::MsFlags::MS_NOEXEC @@ -198,17 +267,18 @@ fn prepare_boot() -> Result<()> { | nix::mount::MsFlags::MS_RDONLY; info!( - "Mounting {} in {}", - boot_partition_path.display(), - BOOT_MOUNT_PATH + "Mounting {} in {} as {}", + partition_path.display(), + BOOT_MOUNT_PATH, + fstype ); - // Mount the active boot partition in /boot + // Mount the boot partition in /boot nix::mount::mount( - Some(boot_partition_path), + Some(partition_path), BOOT_MOUNT_PATH, - Some("ext4"), + Some(fstype), flags, - NONE, + data.map(str::as_bytes), ) .context(error::MountSnafu { path: BOOT_MOUNT_PATH, From a02dda4c59f16f3c1a2c007296e6e8d3f5924706 Mon Sep 17 00:00:00 2001 From: Maher Homsi Date: Tue, 11 Aug 2026 21:28:01 +0000 Subject: [PATCH 8/9] systemd: figure success of measurement correctly Backport of upstream bb19b6104978b5ede792fa3f0cfc74272f20bf9c. Signed-off-by: Maher Homsi --- ...ure-success-of-measurement-correctly.patch | 32 +++++++++++++++++++ packages/systemd-257/systemd-257.spec | 4 +++ 2 files changed, 36 insertions(+) create mode 100644 packages/systemd-257/9021-measure-figure-success-of-measurement-correctly.patch diff --git a/packages/systemd-257/9021-measure-figure-success-of-measurement-correctly.patch b/packages/systemd-257/9021-measure-figure-success-of-measurement-correctly.patch new file mode 100644 index 000000000..25dccd413 --- /dev/null +++ b/packages/systemd-257/9021-measure-figure-success-of-measurement-correctly.patch @@ -0,0 +1,32 @@ +From bb19b6104978b5ede792fa3f0cfc74272f20bf9c Mon Sep 17 00:00:00 2001 +From: Maher Homsi +Date: Tue, 11 Aug 2026 13:30:00 -0700 +Subject: [PATCH] measure: figure success of measurement correctly + +Compare the EFI_STATUS return of tpm_log_tagged_event's measurement call +against EFI_SUCCESS instead of "if (!err)", which masked failures as +success (EFI_SUCCESS is 0). + +Backported from upstream bb19b6104978b5ede792fa3f0cfc74272f20bf9c. + +Signed-off-by: Maher Homsi +--- + src/boot/measure.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/boot/measure.c b/src/boot/measure.c +index 22129cb87d61d..3c51998d1b298 100644 +--- a/src/boot/measure.c ++++ b/src/boot/measure.c +@@ -285,7 +285,7 @@ EFI_STATUS tpm_log_tagged_event( + } + + err = tpm2_measure_to_pcr_and_tagged_event_log(tpm2, pcrindex, buffer, buffer_size, event_id, description); +- if (!err) ++ if (err != EFI_SUCCESS) + return err; + + if (ret_measured) +-- +2.51.0 + diff --git a/packages/systemd-257/systemd-257.spec b/packages/systemd-257/systemd-257.spec index 4ce3bac96..ff47a2575 100644 --- a/packages/systemd-257/systemd-257.spec +++ b/packages/systemd-257/systemd-257.spec @@ -96,6 +96,10 @@ Patch9018: 9018-build-correct-sd-boot-EFI-arch-on-the-Bottlerocket-S.patch Patch9019: 9019-boot-remove-SMBIOS-Type-11-kernel-cmdline-extra-mech.patch Patch9020: 9020-boot-vmspawn-finish-removing-SMBIOS-cmdline-extra-bi.patch +# Correctly check the EFI_STATUS return of the measurement call in +# tpm_log_tagged_event so early return only happens on failure +Patch9021: 9021-measure-figure-success-of-measurement-correctly.patch + BuildRequires: gperf BuildRequires: intltool BuildRequires: meson From 72a14e4be78d5c0f6b0d505c081144ba4981f5fb Mon Sep 17 00:00:00 2001 From: Arnaldo Garcia Rincon Date: Fri, 14 Aug 2026 17:59:22 +0000 Subject: [PATCH 9/9] release: select FIPS HMAC path based on image format The FIPS kernel integrity check hardcoded /boot/.vmlinuz.hmac, which does not exist on UKI images where the kernel is embedded in the EFI binary. Add two oneshot units, generate-fips-hmac-path-uki.service and generate-fips-hmac-path-vmlinuz.service, that inspect UKI_IMAGE in /usr/share/bottlerocket/image-format.env and write the matching HMAC path to /etc/fips-hmac-path.env. Both are gated on fips=1 and ordered before fipscheck.target and check-kernel-integrity.service, which now reads ${HMAC_PATH} from that file instead of a fixed path. Signed-off-by: Arnaldo Garcia Rincon --- .../release/check-kernel-integrity.service | 3 ++- .../generate-fips-hmac-path-uki.service | 20 +++++++++++++++++++ .../generate-fips-hmac-path-vmlinuz.service | 19 ++++++++++++++++++ packages/release/release.spec | 5 +++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/release/generate-fips-hmac-path-uki.service create mode 100644 packages/release/generate-fips-hmac-path-vmlinuz.service diff --git a/packages/release/check-kernel-integrity.service b/packages/release/check-kernel-integrity.service index e239b42e4..2b1ddf81a 100644 --- a/packages/release/check-kernel-integrity.service +++ b/packages/release/check-kernel-integrity.service @@ -12,7 +12,8 @@ ConditionPathExists=!/etc/.fips-kernel-check-passed [Service] Type=oneshot WorkingDirectory=/boot -ExecStart=/usr/bin/sha512hmac -q -c /boot/.vmlinuz.hmac +EnvironmentFile=/etc/fips-hmac-path.env +ExecStart=/usr/bin/sha512hmac -q -c ${HMAC_PATH} ExecStart=/usr/bin/touch /etc/.fips-kernel-check-passed RemainAfterExit=true StandardOutput=tty diff --git a/packages/release/generate-fips-hmac-path-uki.service b/packages/release/generate-fips-hmac-path-uki.service new file mode 100644 index 000000000..d003c48d0 --- /dev/null +++ b/packages/release/generate-fips-hmac-path-uki.service @@ -0,0 +1,20 @@ +[Unit] +Description=Determine FIPS HMAC in UKI image +RefuseManualStart=true +RefuseManualStop=true +DefaultDependencies=no +Before=fipscheck.target check-kernel-integrity.service +ConditionKernelCommandLine=fips=1 + +[Service] +Type=oneshot +EnvironmentFile=-/usr/share/bottlerocket/image-format.env +# Only run on UKI images; the vmlinuz producer handles the other case. +ExecCondition=[ "${UKI_IMAGE}" = "true" ] +ExecStart=/usr/bin/echo "HMAC_PATH=/boot/EFI/Linux/.bottlerocket.efi.hmac" +RemainAfterExit=true +StandardOutput=file:/etc/fips-hmac-path.env +StandardError=journal+console + +[Install] +RequiredBy=fipscheck.target diff --git a/packages/release/generate-fips-hmac-path-vmlinuz.service b/packages/release/generate-fips-hmac-path-vmlinuz.service new file mode 100644 index 000000000..5554cf73a --- /dev/null +++ b/packages/release/generate-fips-hmac-path-vmlinuz.service @@ -0,0 +1,19 @@ +[Unit] +Description=Determine FIPS HMAC in vmlinuz image +RefuseManualStart=true +RefuseManualStop=true +DefaultDependencies=no +Before=fipscheck.target check-kernel-integrity.service +ConditionKernelCommandLine=fips=1 + +[Service] +Type=oneshot +EnvironmentFile=-/usr/share/bottlerocket/image-format.env +ExecCondition=[ "${UKI_IMAGE}" != "true" ] +ExecStart=/usr/bin/echo "HMAC_PATH=/boot/.vmlinuz.hmac" +RemainAfterExit=true +StandardOutput=file:/etc/fips-hmac-path.env +StandardError=journal+console + +[Install] +RequiredBy=fipscheck.target diff --git a/packages/release/release.spec b/packages/release/release.spec index 44c30273b..d7cff0884 100644 --- a/packages/release/release.spec +++ b/packages/release/release.spec @@ -88,6 +88,8 @@ Source1065: check-kernel-integrity.service Source1066: check-fips-modules.service Source1067: fips-modprobe@.service Source1068: configure-snapshotter.service +Source1069: generate-fips-hmac-path-uki.service +Source1070: generate-fips-hmac-path-vmlinuz.service # Mounts that require build-time edits. Source1080: var-lib-kernel-devel-lower.mount.in @@ -281,6 +283,7 @@ install -p -m 0644 \ %{S:1065} %{S:1066} %{S:1067} %{S:1068} \ %{S:1600} %{S:1601} %{S:1602} %{S:1603} %{S:1604} \ %{S:1605} %{S:1606} %{S:1607} %{S:1608} %{S:1609} \ + %{S:1069} %{S:1070} \ %{buildroot}%{_cross_unitdir} install -d %{buildroot}%{_cross_unitdir}/systemd-tmpfiles-setup.service.d @@ -496,6 +499,8 @@ ln -s preconfigured.target %{buildroot}%{_cross_unitdir}/default.target %{_cross_unitdir}/fipscheck.target %{_cross_unitdir}/activate-preconfigured.service %{_cross_unitdir}/check-kernel-integrity.service +%{_cross_unitdir}/generate-fips-hmac-path-uki.service +%{_cross_unitdir}/generate-fips-hmac-path-vmlinuz.service %{_cross_unitdir}/check-fips-modules.service %dir %{_cross_unitdir}/check-fips-modules.service.d %{_cross_unitdir}/fips-modprobe@.service