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}. 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/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 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 diff --git a/packages/release/release.spec b/packages/release/release.spec index 6baa88d98..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 @@ -165,7 +167,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 @@ -278,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 @@ -493,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 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/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/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..ff47a2575 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,15 @@ 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 + +# 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 @@ -175,6 +193,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 +328,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 +407,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 +500,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 +955,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/ 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, 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))); + } +}