From 3a4e31b1c4f4313b09a67cb0ab46465b2cdb32b0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 17:54:36 +0100 Subject: [PATCH 01/30] mkosi: include networkd + import-generator in initrd --- mkosi.images/initrd/mkosi.conf.d/centos-fedora.conf | 4 ++++ mkosi.images/initrd/mkosi.conf.d/debian-ubuntu.conf | 2 ++ mkosi.images/initrd/mkosi.conf.d/opensuse.conf | 3 +++ 3 files changed, 9 insertions(+) diff --git a/mkosi.images/initrd/mkosi.conf.d/centos-fedora.conf b/mkosi.images/initrd/mkosi.conf.d/centos-fedora.conf index 6607dab089..5c84cc4cd4 100644 --- a/mkosi.images/initrd/mkosi.conf.d/centos-fedora.conf +++ b/mkosi.images/initrd/mkosi.conf.d/centos-fedora.conf @@ -12,3 +12,7 @@ VolatilePackages= systemd systemd-libs systemd-udev + # Pull in systemd-container so that the import-generator is available + systemd-container + # Pull in systemd-networkd so that we can do network boot + systemd-networkd diff --git a/mkosi.images/initrd/mkosi.conf.d/debian-ubuntu.conf b/mkosi.images/initrd/mkosi.conf.d/debian-ubuntu.conf index 093c1bd886..577bf8f59f 100644 --- a/mkosi.images/initrd/mkosi.conf.d/debian-ubuntu.conf +++ b/mkosi.images/initrd/mkosi.conf.d/debian-ubuntu.conf @@ -14,6 +14,8 @@ VolatilePackages= libsystemd0 libudev1 systemd + # Pull in systemd-container so that the import-generator is available + systemd-container systemd-cryptsetup systemd-repart udev diff --git a/mkosi.images/initrd/mkosi.conf.d/opensuse.conf b/mkosi.images/initrd/mkosi.conf.d/opensuse.conf index 8b84e239f2..deaa273b4d 100644 --- a/mkosi.images/initrd/mkosi.conf.d/opensuse.conf +++ b/mkosi.images/initrd/mkosi.conf.d/opensuse.conf @@ -14,5 +14,8 @@ VolatilePackages= libudev1 systemd systemd-boot # TODO: Drop once opensuse moves the systemd-stub tmpfiles snippet back to the systemd package + # Pull in systemd-container so that the import-generator is available + systemd-container systemd-experimental + systemd-network udev From 9290abb611d9904f5faad47ce6c4b666a6557a7a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 9 Feb 2025 07:28:44 +0100 Subject: [PATCH 02/30] mkosi: add kernel-bootcfg to all images kernel-bootcfg --add-uri= is just too useful when playing with network boot. Let's just add it to our images. --- mkosi.conf.d/10-arch/mkosi.conf | 2 ++ mkosi.conf.d/10-centos-fedora/mkosi.conf | 2 ++ mkosi.conf.d/10-debian-ubuntu/mkosi.conf | 2 ++ mkosi.conf.d/10-opensuse/mkosi.conf | 2 ++ 4 files changed, 8 insertions(+) diff --git a/mkosi.conf.d/10-arch/mkosi.conf b/mkosi.conf.d/10-arch/mkosi.conf index 9ceb6ea6f8..f897d7d31c 100644 --- a/mkosi.conf.d/10-arch/mkosi.conf +++ b/mkosi.conf.d/10-arch/mkosi.conf @@ -53,3 +53,5 @@ Packages= tgt tpm2-tools vim + # kernel-bootcfg --add-uri= is just too useful + virt-firmware diff --git a/mkosi.conf.d/10-centos-fedora/mkosi.conf b/mkosi.conf.d/10-centos-fedora/mkosi.conf index 8004c089ad..e0ceca402a 100644 --- a/mkosi.conf.d/10-centos-fedora/mkosi.conf +++ b/mkosi.conf.d/10-centos-fedora/mkosi.conf @@ -59,3 +59,5 @@ Packages= tpm2-tools veritysetup vim-common + # kernel-bootcfg --add-uri= is just too useful + virt-firmware diff --git a/mkosi.conf.d/10-debian-ubuntu/mkosi.conf b/mkosi.conf.d/10-debian-ubuntu/mkosi.conf index c898664f83..b34b8c6e19 100644 --- a/mkosi.conf.d/10-debian-ubuntu/mkosi.conf +++ b/mkosi.conf.d/10-debian-ubuntu/mkosi.conf @@ -68,6 +68,8 @@ Packages= psmisc python3-pexpect python3-psutil + # kernel-bootcfg --add-uri= is just too useful + python3-virt-firmware qrencode quota softhsm2 diff --git a/mkosi.conf.d/10-opensuse/mkosi.conf b/mkosi.conf.d/10-opensuse/mkosi.conf index 4ee3894c00..3d53a1c13a 100644 --- a/mkosi.conf.d/10-opensuse/mkosi.conf +++ b/mkosi.conf.d/10-opensuse/mkosi.conf @@ -79,5 +79,7 @@ Packages= tpm2.0-tools veritysetup vim + # kernel-bootcfg --add-uri= is just too useful + virt-firmware xz zypper From 6b3fd03eacf146d63815ebe5c08e6737954660ad Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 17:55:08 +0100 Subject: [PATCH 03/30] dissect: optionally derive loop-ref from image filename --- man/systemd-dissect.xml | 9 +++++++++ src/dissect/dissect.c | 22 ++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index 7c9369e387..84f4a4ab10 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -517,6 +517,15 @@ + + + + Similar to , but automatically derive the reference + string from the specified backing filename, truncating it if necessary. + + + + diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 956cac6708..cc538c4b92 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -94,6 +94,7 @@ static bool arg_rmdir = false; static bool arg_in_memory = false; static char **arg_argv = NULL; static char *arg_loop_ref = NULL; +static bool arg_loop_ref_auto = false; static ImagePolicy *arg_image_policy = NULL; static bool arg_mtree_hash = true; static bool arg_via_service = false; @@ -156,6 +157,7 @@ static int help(void) { " --json=pretty|short|off\n" " Generate JSON output\n" " --loop-ref=NAME Set reference string for loopback device\n" + " --loop-ref-auto Derive reference string from image file name\n" " --mtree-hash=BOOL Whether to include SHA256 hash in the mtree output\n" " --user Discover user images\n" " --system Discover system images\n" @@ -280,6 +282,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_ATTACH, ARG_DETACH, ARG_LOOP_REF, + ARG_LOOP_REF_AUTO, ARG_IMAGE_POLICY, ARG_VALIDATE, ARG_MTREE_HASH, @@ -317,6 +320,7 @@ static int parse_argv(int argc, char *argv[]) { { "json", required_argument, NULL, ARG_JSON }, { "discover", no_argument, NULL, ARG_DISCOVER }, { "loop-ref", required_argument, NULL, ARG_LOOP_REF }, + { "loop-ref-auto", no_argument, NULL, ARG_LOOP_REF_AUTO }, { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, { "validate", no_argument, NULL, ARG_VALIDATE }, { "mtree-hash", required_argument, NULL, ARG_MTREE_HASH }, @@ -522,6 +526,7 @@ static int parse_argv(int argc, char *argv[]) { case ARG_LOOP_REF: if (isempty(optarg)) { arg_loop_ref = mfree(arg_loop_ref); + arg_loop_ref_auto = false; break; } @@ -531,6 +536,13 @@ static int parse_argv(int argc, char *argv[]) { r = free_and_strdup_warn(&arg_loop_ref, optarg); if (r < 0) return r; + + arg_loop_ref_auto = false; + break; + + case ARG_LOOP_REF_AUTO: + arg_loop_ref = mfree(arg_loop_ref); + arg_loop_ref_auto = true; break; case ARG_IMAGE_POLICY: @@ -2149,6 +2161,12 @@ static int run(int argc, char *argv[]) { * support */ } + if (!arg_loop_ref && arg_loop_ref_auto) { + r = path_extract_filename(arg_image, &arg_loop_ref); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from image path '%s': %m", arg_image); + } + if (arg_action == ACTION_VALIDATE) return action_validate(); @@ -2214,8 +2232,8 @@ static int run(int argc, char *argv[]) { if (arg_in_memory) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--in-memory= not supported when operating via systemd-mountfsd."); - if (arg_loop_ref) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--loop-ref= not supported when operating via systemd-mountfsd."); + if (arg_loop_ref || arg_loop_ref_auto) /* yes, the 2nd check is strictly speaking redundant, given the normalization we did above, but let's be explicit here */ + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--loop-ref=/--loop-ref-auto not supported when operating via systemd-mountfsd."); if (verity_settings_set(&arg_verity_settings)) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Externally configured verity settings not supported when operating via systemd-mountfsd."); From 172fadda65a788c6128e5c93e0c530244c6a42e2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 19:44:13 +0100 Subject: [PATCH 04/30] dissect: add --quiet mode --- man/systemd-dissect.xml | 19 ++++++++++++++++--- src/dissect/dissect.c | 13 +++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index 84f4a4ab10..b0a67da594 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -206,9 +206,12 @@ Attach the specified disk image to an automatically allocated loopback block device, and print the path to the loopback block device to standard output. This is similar to an invocation - of losetup --find --show, but will validate the image as DDI before attaching, and - derive the correct sector size to use automatically. Moreover, it ensures the per-partition block - devices are created before returning. Takes a path to a disk image file. + of losetup --find --show --partscan, but will validate the image as DDI before + attaching, and derive the correct sector size to use automatically. Moreover, it ensures the + per-partition block devices are created before returning and attempts to enable direct IO mode, if + possible. Takes a path to a disk image file. + + If combined with output of the block device name is suppressed. @@ -556,6 +559,16 @@ + + + + + If combined with , suppresses output of the used loopback + block device path. + + + + diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index cc538c4b92..6d238ecddf 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -101,6 +101,7 @@ static bool arg_via_service = false; static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID; static bool arg_all = false; static uid_t arg_uid_base = UID_INVALID; +static bool arg_quiet = false; STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); @@ -181,6 +182,7 @@ static int help(void) { " --discover Discover DDIs in well known directories\n" " --validate Validate image and image policy\n" " --shift Shift UID range to selected base\n" + " -q --quiet Suppress output of chosen loopback block device\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, @@ -329,6 +331,7 @@ static int parse_argv(int argc, char *argv[]) { { "system", no_argument, NULL, ARG_SYSTEM }, { "user", no_argument, NULL, ARG_USER }, { "all", no_argument, NULL, ARG_ALL }, + { "quiet", no_argument, NULL, 'q' }, {} }; @@ -343,7 +346,7 @@ static int parse_argv(int argc, char *argv[]) { if (r < 0) return r; - while ((c = getopt_long(argc, argv, "hmurMUlxa", options, NULL)) >= 0) { + while ((c = getopt_long(argc, argv, "hmurMUlxaq", options, NULL)) >= 0) { switch (c) { @@ -585,6 +588,10 @@ static int parse_argv(int argc, char *argv[]) { arg_all = true; break; + case 'q': + arg_quiet = true; + break; + case '?': return -EINVAL; @@ -1991,7 +1998,9 @@ static int action_attach(DissectedImage *m, LoopDevice *d) { if (r < 0) return log_error_errno(r, "Failed to relinquish DM and loopback block devices: %m"); - puts(d->node); + if (!arg_quiet) + puts(d->node); + return 0; } From 50063d496d69c25f2088a193a769b976bea05f56 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 16:33:17 +0100 Subject: [PATCH 05/30] units: add generic service for attaching a file to a loopback device This is mostly just a friendly unit wrapper around "systemd-dissect --attach". This is useful so that we can automatically attach disk images as block device at boot. --- man/rules/meson.build | 1 + man/systemd-loop@.service.xml | 51 +++++++++++++++++++++++++++++++++++ units/meson.build | 1 + units/systemd-loop@.service | 24 +++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 man/systemd-loop@.service.xml create mode 100644 units/systemd-loop@.service diff --git a/man/rules/meson.build b/man/rules/meson.build index 9fd648392c..7f147d2dd7 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -997,6 +997,7 @@ manpages = [ ['systemd-keyutil', '1', [], ''], ['systemd-localed.service', '8', ['systemd-localed'], 'ENABLE_LOCALED'], ['systemd-logind.service', '8', ['systemd-logind'], 'ENABLE_LOGIND'], + ['systemd-loop@.service', '8', [], ''], ['systemd-machine-id-commit.service', '8', [], ''], ['systemd-machine-id-setup', '1', [], ''], ['systemd-machined.service', '8', ['systemd-machined'], 'ENABLE_MACHINED'], diff --git a/man/systemd-loop@.service.xml b/man/systemd-loop@.service.xml new file mode 100644 index 0000000000..57ed181263 --- /dev/null +++ b/man/systemd-loop@.service.xml @@ -0,0 +1,51 @@ + + + + + + + + systemd-loop@.service + systemd + + + + systemd-loop@.service + 8 + + + + systemd-loop@.service + Attach a loopback block device + + + + systemd-loop@path.service + + + + Description + + systemd-loop@.service is a template service that may be used to automatically + attach a loopback block device at boot (or later). Its instance string may reference an (escaped) file + system path pointing to a disk image to attach as loopback block device. Use + systemd-escape1's + to properly escape a file system path. + + This unit invokes systemd-dissect --attach --quiet --loop-ref-auto on the + specified files, see + systemd-dissect1 for + details. + + + + See Also + + systemd1 + systemd-dissect1 + systemd-import-generator8 + + + + diff --git a/units/meson.build b/units/meson.build index 2f2a1d5fe9..3ae3ef8311 100644 --- a/units/meson.build +++ b/units/meson.build @@ -447,6 +447,7 @@ units = [ 'conditions' : ['ENABLE_LOGIND'], 'symlinks' : ['multi-user.target.wants/', 'dbus-org.freedesktop.login1.service'], }, + { 'file' : 'systemd-loop@.service' }, { 'file' : 'systemd-machine-id-commit.service', 'symlinks' : ['sysinit.target.wants/'], diff --git a/units/systemd-loop@.service b/units/systemd-loop@.service new file mode 100644 index 0000000000..a063dcec86 --- /dev/null +++ b/units/systemd-loop@.service @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Attach File %f to Loopback Block Device +Documentation=man:systemd-loop@.service(8) +DefaultDependencies=no +RequiresMountsFor=%f +IgnoreOnIsolate=yes +Wants=modprobe@loop.service +After=modprobe@loop.service + +[Service] +Type=oneshot +RemainAfterExit=yes +TimeoutSec=infinity +ExecStart=systemd-dissect --attach --loop-ref-auto --quiet %f +ExecStop=systemd-dissect --detach %f From 3e6a3341ac4137b74ceb9df7987f31cfdb9fa328 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Feb 2025 10:37:11 +0100 Subject: [PATCH 06/30] man: mention 'rd.' prefix for import-generator kernel cmdline options --- man/systemd-import-generator.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index 1ccccb2689..1faa2f78a2 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -136,6 +136,10 @@ + + These kernel command line options are interpreted by the host system only. If these options are + prefixed with rd. they are interpreted by the initial RAM disk (initrd) + instead. From c88fdb1e560f4377424e2f687236b8af2f83de99 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 16:29:00 +0100 Subject: [PATCH 07/30] import-generator: optionally create loopback devices after download This is useful for booting from a freshly downloaded disk image: just specify rd.systemd.pull=verify=no,machine,blockdev,raw:image:https://192.168.100.1:8081/image.raw root=/dev/disk/by-loop-ref/image.raw-part2 on the kernel command line, and we'll download that in the initrd and boot from it. (note the above disables download-time verification, putting trust in verity and image policy that this won#t do harm) Here's a more complete example. From a git checkout do: ninja -C build && mkosi -f -T serve and then from another terminal do within the same checkout: ./build/systemd-vmspawn \ --ram=16G \ --register=no \ -n \ -i ./build/mkosi.output/image.raw \ rd.systemd.pull=verify=no,machine,blockdev,raw:image:http://192.168.100.1:8081/image.raw \ root=/dev/disk/by-loop-ref/image.raw-part2 \ rootflags=x-systemd.device-timeout=infinity \ ip=any This will then boot via the ESP of the specified image, then download the image via HTTP from the mkosi instance running in the first terminal, attach it to a loopback block device, and then use its second partition as root fs, and boot into it. (this assumes your host is 192.168.100.1, of course) Note that downloading the full image takes a bit of time (this downloads it uncompressed after all), hence we turn off the timeout to wait for the device. This also introduces a new "imports.target" unit (and associated "imports-pre.target") between imports are grouped, and which ensure the imports actually are ordered correctly both on the host and in the initrd. --- man/systemd-import-generator.xml | 21 ++++++ man/systemd.special.xml | 20 +++++ src/import/import-generator.c | 122 ++++++++++++++++++++++++++----- units/imports-pre.target | 14 ++++ units/imports.target | 12 +++ units/meson.build | 9 +++ 6 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 units/imports-pre.target create mode 100644 units/imports.target diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index 1faa2f78a2..f140873552 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -117,6 +117,16 @@ + + + blockdev + + If this option is specified the downloaded image is attached to a loopback block + device (via systemd-loop@.service) after completion. This permits booting + from downloaded disk images. This is only supported for raw disk images. + + + @@ -183,6 +193,16 @@ validation. This is useful for development purposes in virtual machines and containers. Warning: do not deploy a system with validation disabled like this! + + + Download root disk image (raw) into memory, for booting into it + + rd.systemd.pull=raw,machine,verify=no,blockdev:image:https://example.com/image.raw.xz root=/dev/disk/by-loop-ref/image.raw-part2 + + This downloads the specified disk image, saving it locally under the name + image, and attaches it to a loopback block device on completion. It then boots from + the 2nd partition in the image. + @@ -193,6 +213,7 @@ kernel-command-line7 systemd.system-credentials7 importctl1 + systemd-loop@.service8 diff --git a/man/systemd.special.xml b/man/systemd.special.xml index 7bf9b63f0a..ca3bf463ae 100644 --- a/man/systemd.special.xml +++ b/man/systemd.special.xml @@ -386,6 +386,16 @@ directly. + + imports.target + + A target unit that pulls in all disk image download jobs to execute on system boot. This is + used by + systemd-import-generator8. + + + + init.scope @@ -1075,6 +1085,16 @@ + + imports-pre.target + + A passive unit that is ordered before all disk image download jobs to execute on system + boot. This is used by + systemd-import-generator8. + + + + local-fs-pre.target diff --git a/src/import/import-generator.c b/src/import/import-generator.c index 24da2f1a26..a778d064a9 100644 --- a/src/import/import-generator.c +++ b/src/import/import-generator.c @@ -8,20 +8,42 @@ #include "fileio.h" #include "generator.h" #include "import-util.h" +#include "initrd-util.h" #include "json-util.h" #include "proc-cmdline.h" +#include "special.h" #include "specifier.h" +#include "unit-name.h" #include "web-util.h" +typedef struct Transfer { + ImageClass class; + ImportType type; + char *local; + char *remote; + bool blockdev; + sd_json_variant *json; +} Transfer; + static const char *arg_dest = NULL; static char *arg_success_action = NULL; static char *arg_failure_action = NULL; -static sd_json_variant **arg_transfers = NULL; +static Transfer *arg_transfers = NULL; static size_t arg_n_transfers = 0; +static void transfer_destroy_many(Transfer *transfers, size_t n) { + FOREACH_ARRAY(t, transfers, n) { + free(t->local); + free(t->remote); + sd_json_variant_unref(t->json); + } + + free(transfers); +} + STATIC_DESTRUCTOR_REGISTER(arg_success_action, freep); STATIC_DESTRUCTOR_REGISTER(arg_failure_action, freep); -STATIC_ARRAY_DESTRUCTOR_REGISTER(arg_transfers, arg_n_transfers, sd_json_variant_unref_many); +STATIC_ARRAY_DESTRUCTOR_REGISTER(arg_transfers, arg_n_transfers, transfer_destroy_many); static int parse_pull_expression(const char *v) { const char *p = v; @@ -57,7 +79,7 @@ static int parse_pull_expression(const char *v) { ImportType type = _IMPORT_TYPE_INVALID; ImageClass class = _IMAGE_CLASS_INVALID; ImportVerify verify = IMPORT_VERIFY_SIGNATURE; - bool ro = false; + bool ro = false, blockdev = false; const char *o = options; for (;;) { @@ -75,6 +97,8 @@ static int parse_pull_expression(const char *v) { ro = true; else if (streq(opt, "rw")) ro = false; + else if (streq(opt, "blockdev")) + blockdev = true; else if ((suffix = startswith(opt, "verify="))) { ImportVerify w = import_verify_from_string(suffix); @@ -105,6 +129,35 @@ static int parse_pull_expression(const char *v) { if (class < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No image class (machine, portable, sysext, confext) specified in pull expression, refusing: %s", v); + if (!local) { + _cleanup_free_ char *c = NULL; + r = import_url_last_component(remote, &c); + if (r < 0) + return log_error_errno(r, "Failed to generate local name from URL '%s': %m", remote); + + switch (type) { + + case IMPORT_RAW: + r = raw_strip_suffixes(c, &local); + break; + + case IMPORT_TAR: + r = tar_strip_suffixes(c, &local); + break; + + default: + assert_not_reached(); + break; + } + if (r < 0) + return log_error_errno(r, "Failed to strip suffix from URL '%s': %m", remote); + + log_info("Saving downloaded file under local name '%s'.", local); + } + + if (blockdev && type != IMPORT_RAW) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option 'blockdev' only available for raw images, refusing: %s", v); + if (!GREEDY_REALLOC(arg_transfers, arg_n_transfers + 1)) return log_oom(); @@ -113,7 +166,7 @@ static int parse_pull_expression(const char *v) { r = sd_json_buildo( &j, SD_JSON_BUILD_PAIR("remote", SD_JSON_BUILD_STRING(remote)), - SD_JSON_BUILD_PAIR_CONDITION(!!local, "local", SD_JSON_BUILD_STRING(local)), + SD_JSON_BUILD_PAIR("local", SD_JSON_BUILD_STRING(local)), SD_JSON_BUILD_PAIR("class", JSON_BUILD_STRING_UNDERSCORIFY(image_class_to_string(class))), SD_JSON_BUILD_PAIR("type", JSON_BUILD_STRING_UNDERSCORIFY(import_type_to_string(type))), SD_JSON_BUILD_PAIR("readOnly", SD_JSON_BUILD_BOOLEAN(ro)), @@ -121,7 +174,15 @@ static int parse_pull_expression(const char *v) { if (r < 0) return log_error_errno(r, "Failed to build import JSON object: %m"); - arg_transfers[arg_n_transfers++] = TAKE_PTR(j); + arg_transfers[arg_n_transfers++] = (Transfer) { + .class = class, + .type = type, + .local = TAKE_PTR(local), + .remote = TAKE_PTR(remote), + .json = TAKE_PTR(j), + .blockdev = blockdev, + }; + return 0; } @@ -191,10 +252,10 @@ static int parse_credentials(void) { return 0; } -static int transfer_generate(sd_json_variant *v, size_t c) { +static int transfer_generate(const Transfer *t, size_t c) { int r; - assert(v); + assert(t); _cleanup_free_ char *service = NULL; if (asprintf(&service, "import%zu.service", c) < 0) @@ -205,19 +266,17 @@ static int transfer_generate(sd_json_variant *v, size_t c) { if (r < 0) return r; - const char *remote = sd_json_variant_string(sd_json_variant_by_key(v, "remote")); - fprintf(f, "[Unit]\n" "Description=Download of %s\n" "Documentation=man:systemd-import-generator(8)\n" "SourcePath=/proc/cmdline\n" "Requires=systemd-importd.socket\n" - "After=systemd-importd.socket\n" + "After=imports-pre.target systemd-importd.socket\n" "Conflicts=shutdown.target\n" - "Before=shutdown.target\n" + "Before=imports.target shutdown.target\n" "DefaultDependencies=no\n", - remote); + t->remote); if (arg_success_action) fprintf(f, "SuccessAction=%s\n", @@ -227,24 +286,39 @@ static int transfer_generate(sd_json_variant *v, size_t c) { fprintf(f, "FailureAction=%s\n", arg_failure_action); - const char *class = sd_json_variant_string(sd_json_variant_by_key(v, "class")); - if (streq_ptr(class, "sysext")) + if (t->class == IMAGE_SYSEXT) fputs("Before=systemd-sysext.service\n", f); - else if (streq_ptr(class, "confext")) + else if (t->class == IMAGE_CONFEXT) fputs("Before=systemd-confext.service\n", f); /* Assume network resource unless URL is file:// */ - if (!file_url_is_valid(remote)) + if (!file_url_is_valid(t->remote)) fputs("Wants=network-online.target\n" "After=network-online.target\n", f); + _cleanup_free_ char *local_path = NULL, *loop_service = NULL; + if (t->blockdev) { + assert(t->type == IMPORT_RAW); + + local_path = strjoin(image_root_to_string(t->class), "/", t->local, ".raw"); + if (!local_path) + return log_oom(); + + r = unit_name_from_path_instance("systemd-loop", local_path, ".service", &loop_service); + if (r < 0) + return log_error_errno(r, "Failed to build systemd-loop@.service instance name from path '%s': %m", local_path); + + /* Make sure download completes before the loopback service is activated */ + fprintf(f, "Before=%s\n", loop_service); + } + fputs("\n" "[Service]\n" "Type=oneshot\n" "NotifyAccess=main\n", f); _cleanup_free_ char *formatted = NULL; - r = sd_json_variant_format(v, /* flags= */ 0, &formatted); + r = sd_json_variant_format(t->json, /* flags= */ 0, &formatted); if (r < 0) return log_error_errno(r, "Failed to format import JSON data: %m"); @@ -259,7 +333,17 @@ static int transfer_generate(sd_json_variant *v, size_t c) { if (r < 0) return log_error_errno(r, "Failed to write unit %s: %m", service); - return generator_add_symlink(arg_dest, "multi-user.target", "wants", service); + r = generator_add_symlink(arg_dest, "imports.target", "wants", service); + if (r < 0) + return r; + + if (loop_service) { + r = generator_add_symlink(arg_dest, "imports.target", "wants", loop_service); + if (r < 0) + return r; + } + + return 0; } static int generate(void) { @@ -267,7 +351,7 @@ static int generate(void) { int r = 0; FOREACH_ARRAY(i, arg_transfers, arg_n_transfers) - RET_GATHER(r, transfer_generate(*i, c++)); + RET_GATHER(r, transfer_generate(i, c++)); return r; } diff --git a/units/imports-pre.target b/units/imports-pre.target new file mode 100644 index 0000000000..1bd3f0d778 --- /dev/null +++ b/units/imports-pre.target @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Image Downloads (Pre) +Documentation=man:systemd.special(7) +Before=imports.target +RefuseManualStart=yes diff --git a/units/imports.target b/units/imports.target new file mode 100644 index 0000000000..e07a8a3832 --- /dev/null +++ b/units/imports.target @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Image Downloads +Documentation=man:systemd.special(7) diff --git a/units/meson.build b/units/meson.build index 3ae3ef8311..edf09b7989 100644 --- a/units/meson.build +++ b/units/meson.build @@ -382,6 +382,15 @@ units = [ 'conditions' : ['ENABLE_IMPORTD'], 'symlinks' : ['sockets.target.wants/'], }, + { + 'file' : 'imports-pre.target', + 'conditions' : ['ENABLE_IMPORTD'], + }, + { + 'file' : 'imports.target', + 'conditions' : ['ENABLE_IMPORTD'], + 'symlinks' : ['sysinit.target.wants/'], + }, { 'file' : 'systemd-initctl.service.in', 'conditions' : ['HAVE_SYSV_COMPAT'], From e15d18b4c64e13c37a4c676bf61a5ecf8bba04a3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 23:30:13 +0100 Subject: [PATCH 08/30] sd-stub: if we are http booted, query source URL and write to EFI variable This way userspace can read the field, and use it to retrieve more resources from the same place. --- docs/BOOT_LOADER_INTERFACE.md | 3 +++ man/systemd-boot.xml | 10 +++++++++ man/systemd-stub.xml | 10 +++++++++ src/boot/boot.c | 1 + src/boot/export-vars.c | 18 +++++++++++----- src/boot/meson.build | 1 + src/boot/proto/device-path.h | 7 +++++++ src/boot/stub.c | 6 ++++++ src/boot/url-discovery.c | 30 +++++++++++++++++++++++++++ src/boot/url-discovery.h | 4 ++++ src/bootctl/bootctl-status.c | 21 +++++++++++++++++-- src/fundamental/efivars-fundamental.h | 2 ++ 12 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 src/boot/url-discovery.c create mode 100644 src/boot/url-discovery.h diff --git a/docs/BOOT_LOADER_INTERFACE.md b/docs/BOOT_LOADER_INTERFACE.md index 4bf074df5e..e264c2cc3c 100644 --- a/docs/BOOT_LOADER_INTERFACE.md +++ b/docs/BOOT_LOADER_INTERFACE.md @@ -97,6 +97,9 @@ variables. All EFI variables use the vendor UUID generally only written once, by the OS installer, and is usually not touched after that. +* The EFI variable `LoaderDeviceURL` contains the URL the boot loader was + downloaded from, in UTF-16 format. Only set in case of network boots. + If `LoaderTimeInitUSec` and `LoaderTimeExecUSec` are set, `systemd-analyze` will include them in its boot-time analysis. If `LoaderDevicePartUUID` is set, systemd will mount the ESP that was used for the boot to `/boot`, but only if diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml index 18b0708035..22f3cdb4ca 100644 --- a/man/systemd-boot.xml +++ b/man/systemd-boot.xml @@ -440,6 +440,16 @@ + + LoaderDeviceURL + + If the boot loader has been invoked via network booting this variable contains the + originating URL. This may be used to automatically acquire additional resources from the same + source. + + + + LoaderEntries diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index 730d27cf6c..273513c14b 100644 --- a/man/systemd-stub.xml +++ b/man/systemd-stub.xml @@ -550,6 +550,16 @@ + + StubDeviceURL + + If the kernel image has been invoked via network booting this variable contains the + originating URL. This may be used to automatically acquire additional resources from the same + source. + + + + StubInfo diff --git a/src/boot/boot.c b/src/boot/boot.c index 53bd22c42d..7c91e5d389 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -2764,6 +2764,7 @@ static void export_loader_variables( EFI_LOADER_FEATURE_RETAIN_SHIM | EFI_LOADER_FEATURE_MENU_DISABLE | EFI_LOADER_FEATURE_MULTI_PROFILE_UKI | + EFI_LOADER_FEATURE_REPORT_URL | 0; assert(loaded_image); diff --git a/src/boot/export-vars.c b/src/boot/export-vars.c index 12fbd10b05..fb281ad394 100644 --- a/src/boot/export-vars.c +++ b/src/boot/export-vars.c @@ -4,17 +4,25 @@ #include "efivars.h" #include "export-vars.h" #include "part-discovery.h" +#include "url-discovery.h" #include "util.h" void export_common_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) { assert(loaded_image); /* Export the device path this image is started from, if it's not set yet */ - if (loaded_image->DeviceHandle && - efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", NULL, NULL) != EFI_SUCCESS) { - _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); - if (uuid) - efivar_set_str16(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", uuid, 0); + if (loaded_image->DeviceHandle) { + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", /* ret_data= */ NULL, /* ret_size= */ NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); + if (uuid) + efivar_set_str16(MAKE_GUID_PTR(LOADER), u"LoaderDevicePartUUID", uuid, 0); + } + + if (efivar_get_raw(MAKE_GUID_PTR(LOADER), u"LoaderDeviceURL", /* ret_data= */ NULL, /* ret_size= */ NULL) != EFI_SUCCESS) { + _cleanup_free_ char16_t *url = disk_get_url(loaded_image->DeviceHandle); + if (url) + efivar_set_str16(MAKE_GUID_PTR(LOADER), u"LoaderDeviceURL", url, 0); + } } /* If LoaderImageIdentifier is not set, assume the image with this stub was loaded directly from the diff --git a/src/boot/meson.build b/src/boot/meson.build index 3af5006d59..69c8e40ad3 100644 --- a/src/boot/meson.build +++ b/src/boot/meson.build @@ -300,6 +300,7 @@ libefi_sources = files( 'shim.c', 'smbios.c', 'ticks.c', + 'url-discovery.c', 'util.c', 'vmm.c', ) diff --git a/src/boot/proto/device-path.h b/src/boot/proto/device-path.h index 0fabae1125..a394b0aa6d 100644 --- a/src/boot/proto/device-path.h +++ b/src/boot/proto/device-path.h @@ -30,6 +30,8 @@ enum { MEDIA_FILEPATH_DP = 0x04, MEDIA_PIWG_FW_FILE_DP = 0x06, MEDIA_PIWG_FW_VOL_DP = 0x07, + + MSG_URI_DP = 24, }; struct _packed_ EFI_DEVICE_PATH_PROTOCOL { @@ -67,6 +69,11 @@ typedef struct { char16_t PathName[]; } _packed_ FILEPATH_DEVICE_PATH; +typedef struct { + EFI_DEVICE_PATH Header; + char Uri[]; +} _packed_ URI_DEVICE_PATH; + typedef struct { char16_t* (EFIAPI *ConvertDeviceNodeToText)( const EFI_DEVICE_PATH *DeviceNode, diff --git a/src/boot/stub.c b/src/boot/stub.c index e74b7db95a..3b3a5f6a42 100644 --- a/src/boot/stub.c +++ b/src/boot/stub.c @@ -21,6 +21,7 @@ #include "splash.h" #include "tpm2-pcr.h" #include "uki.h" +#include "url-discovery.h" #include "util.h" #include "version.h" #include "vmm.h" @@ -151,6 +152,7 @@ static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, unsig 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 */ + EFI_STUB_FEATURE_REPORT_URL | /* We set StubDeviceURL + LoaderDeviceURL */ 0; assert(loaded_image); @@ -167,6 +169,10 @@ static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, unsig _cleanup_free_ char16_t *uuid = disk_get_part_uuid(loaded_image->DeviceHandle); if (uuid) efivar_set_str16(MAKE_GUID_PTR(LOADER), u"StubDevicePartUUID", uuid, 0); + + _cleanup_free_ char16_t *url = disk_get_url(loaded_image->DeviceHandle); + if (url) + efivar_set_str16(MAKE_GUID_PTR(LOADER), u"StubDeviceURL", url, 0); } if (loaded_image->FilePath) { diff --git a/src/boot/url-discovery.c b/src/boot/url-discovery.c new file mode 100644 index 0000000000..7c7cfb6a62 --- /dev/null +++ b/src/boot/url-discovery.c @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "device-path-util.h" +#include "efi-string.h" +#include "proto/device-path.h" +#include "url-discovery.h" + +char16_t *disk_get_url(EFI_HANDLE *handle) { + EFI_STATUS err; + EFI_DEVICE_PATH *dp; + + /* export the device path this image is started from */ + + if (!handle) + return NULL; + + err = BS->HandleProtocol(handle, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &dp); + if (err != EFI_SUCCESS) + return NULL; + + for (; !device_path_is_end(dp); dp = device_path_next_node(dp)) { + if (dp->Type != MESSAGING_DEVICE_PATH || dp->SubType != MSG_URI_DP) + continue; + + URI_DEVICE_PATH *udp = (URI_DEVICE_PATH*) dp; + return xstrn8_to_16(udp->Uri, dp->Length); + } + + return NULL; +} diff --git a/src/boot/url-discovery.h b/src/boot/url-discovery.h new file mode 100644 index 0000000000..e4e50c387c --- /dev/null +++ b/src/boot/url-discovery.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +char16_t *disk_get_url(EFI_HANDLE *handle); diff --git a/src/bootctl/bootctl-status.c b/src/bootctl/bootctl-status.c index e9f3a6f5b5..03ebea38df 100644 --- a/src/bootctl/bootctl-status.c +++ b/src/bootctl/bootctl-status.c @@ -390,12 +390,15 @@ int verb_status(int argc, char *argv[], void *userdata) { { EFI_LOADER_FEATURE_RETAIN_SHIM, "Retain SHIM protocols" }, { EFI_LOADER_FEATURE_MENU_DISABLE, "Menu can be disabled" }, { EFI_LOADER_FEATURE_MULTI_PROFILE_UKI, "Multi-Profile UKIs are supported" }, + { EFI_LOADER_FEATURE_REPORT_URL, "Loader reports network boot URL" }, }; static const struct { uint64_t flag; const char *name; } stub_flags[] = { - { EFI_STUB_FEATURE_REPORT_BOOT_PARTITION, "Stub sets loader partition information" }, + { EFI_STUB_FEATURE_REPORT_BOOT_PARTITION, "Stub reports loader partition information" }, + { EFI_STUB_FEATURE_REPORT_STUB_PARTITION, "Stub reports stub partition information" }, + { EFI_STUB_FEATURE_REPORT_URL, "Stub reports network boot URL" }, { EFI_STUB_FEATURE_PICK_UP_CREDENTIALS, "Picks up credentials from boot partition" }, { EFI_STUB_FEATURE_PICK_UP_SYSEXTS, "Picks up system extension images from boot partition" }, { EFI_STUB_FEATURE_PICK_UP_CONFEXTS, "Picks up configuration extension images from boot partition" }, @@ -405,7 +408,6 @@ int verb_status(int argc, char *argv[], void *userdata) { { EFI_STUB_FEATURE_CMDLINE_SMBIOS, "Pick up .cmdline from SMBIOS Type 11" }, { EFI_STUB_FEATURE_DEVICETREE_ADDONS, "Pick up .dtb from addons" }, { EFI_STUB_FEATURE_MULTI_PROFILE_UKI, "Stub understands profile selector" }, - { EFI_STUB_FEATURE_REPORT_STUB_PARTITION, "Stub sets stub partition information" }, }; _cleanup_free_ char *fw_type = NULL, *fw_info = NULL, *loader = NULL, *loader_path = NULL, *stub = NULL, *stub_path = NULL, *current_entry = NULL, *oneshot_entry = NULL, *default_entry = NULL; @@ -480,6 +482,10 @@ int verb_status(int argc, char *argv[], void *userdata) { (void) efi_loader_get_device_part_uuid(&loader_partition_uuid); print_yes_no_line(/* first= */ false, !sd_id128_is_null(loader_partition_uuid), "Boot loader set partition information"); + _cleanup_free_ char *loader_url = NULL; + (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderDeviceURL"), &loader_url); + print_yes_no_line(/* first= */ false, !!loader_url, "Boot loader set network boot URL information"); + if (!sd_id128_is_null(loader_partition_uuid)) { if (!sd_id128_is_null(esp_uuid) && !sd_id128_equal(esp_uuid, loader_partition_uuid)) printf("WARNING: The boot loader reports a different partition UUID than the detected ESP ("SD_ID128_UUID_FORMAT_STR" vs. "SD_ID128_UUID_FORMAT_STR")!\n", @@ -493,6 +499,9 @@ int verb_status(int argc, char *argv[], void *userdata) { if (loader_path) printf(" Loader: %s%s\n", special_glyph(SPECIAL_GLYPH_TREE_RIGHT), strna(loader_path)); + if (loader_url) + printf(" Net Boot URL: %s\n", loader_url); + if (current_entry) printf("Current Entry: %s\n", current_entry); if (default_entry) @@ -513,6 +522,10 @@ int verb_status(int argc, char *argv[], void *userdata) { (void) efi_stub_get_device_part_uuid(&stub_partition_uuid); print_yes_no_line(/* first= */ false, !sd_id128_is_null(stub_partition_uuid), "Stub loader set partition information"); + _cleanup_free_ char *stub_url = NULL; + (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("StubDeviceURL"), &stub_url); + print_yes_no_line(/* first= */ false, !!stub_url, "Stub set network boot URL information"); + if (!sd_id128_is_null(stub_partition_uuid)) { if (!(!sd_id128_is_null(esp_uuid) && sd_id128_equal(esp_uuid, stub_partition_uuid)) && !(!sd_id128_is_null(xbootldr_uuid) && sd_id128_equal(xbootldr_uuid, stub_partition_uuid))) @@ -526,6 +539,10 @@ int verb_status(int argc, char *argv[], void *userdata) { if (stub_path) printf(" Stub: %s%s\n", special_glyph(SPECIAL_GLYPH_TREE_RIGHT), strna(stub_path)); + + if (stub_url) + printf(" Net Boot URL: %s\n", stub_url); + printf("\n"); } diff --git a/src/fundamental/efivars-fundamental.h b/src/fundamental/efivars-fundamental.h index f002e81b53..678cdeeff8 100644 --- a/src/fundamental/efivars-fundamental.h +++ b/src/fundamental/efivars-fundamental.h @@ -24,6 +24,7 @@ #define EFI_LOADER_FEATURE_RETAIN_SHIM (UINT64_C(1) << 12) #define EFI_LOADER_FEATURE_MENU_DISABLE (UINT64_C(1) << 13) #define EFI_LOADER_FEATURE_MULTI_PROFILE_UKI (UINT64_C(1) << 14) +#define EFI_LOADER_FEATURE_REPORT_URL (UINT64_C(1) << 15) /* Features of the stub, i.e. systemd-stub */ #define EFI_STUB_FEATURE_REPORT_BOOT_PARTITION (UINT64_C(1) << 0) @@ -37,6 +38,7 @@ #define EFI_STUB_FEATURE_PICK_UP_CONFEXTS (UINT64_C(1) << 8) #define EFI_STUB_FEATURE_MULTI_PROFILE_UKI (UINT64_C(1) << 9) #define EFI_STUB_FEATURE_REPORT_STUB_PARTITION (UINT64_C(1) << 10) +#define EFI_STUB_FEATURE_REPORT_URL (UINT64_C(1) << 11) typedef enum SecureBootMode { SECURE_BOOT_UNSUPPORTED, From 0c892214f73584e55bfa3fd8f0d54f631cb527b0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 00:23:21 +0100 Subject: [PATCH 09/30] import-generator: add new option 'bootorigin' to derive URL from efi boot url --- man/systemd-import-generator.xml | 28 ++++++++++++++++++++++++++ src/import/import-generator.c | 34 ++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index f140873552..d60c9c96fe 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -127,6 +127,23 @@ + + + bootorigin + + If this option is specified, in place of the URL a simple filename may be + specified. If the system is booted via UEFI HTTP network booting the last component of the + network boot origin URL is replaced by this filename and used as download source. This hence + allows to automatically derive the URLs for disk images from the original boot URL used to invoke + the kernel or boot loader. + + If this option is used and the system is not actually booted via UEFI HTTP network booting, + the download is gracefully skipped. Or in other words without other modifications it is possible + to put together an initrd image that will boot from a local disk if available, or from downloaded + disk image if used via network booting. + + + @@ -203,6 +220,17 @@ image, and attaches it to a loopback block device on completion. It then boots from the 2nd partition in the image. + + + Boot into disk image (raw), with URL derived from UEFI HTTP network booting + + rd.systemd.pull=raw,machine,verify=no,blockdev,bootorigin:rootdisk:image.raw.xz root=/dev/disk/by-loop-ref/rootdisk.raw-part2 + + This is similar to the previous example, but this time the source URL is automatically derived + from the UEFI HTTP network boot URL. For example, if an UKI is booted from an URL + http://example.com/image.efi this would result in a root disk being downloaded from + http://example.com/image.raw.xz. + diff --git a/src/import/import-generator.c b/src/import/import-generator.c index a778d064a9..5d2f37107e 100644 --- a/src/import/import-generator.c +++ b/src/import/import-generator.c @@ -4,6 +4,7 @@ #include "creds-util.h" #include "discover-image.h" +#include "efivars.h" #include "fd-util.h" #include "fileio.h" #include "generator.h" @@ -65,8 +66,6 @@ static int parse_pull_expression(const char *v) { if (r == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No local string in pull expression '%s': %m", v); - if (!http_url_is_valid(p) && !file_url_is_valid(p)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid URL, refusing: %s", p); _cleanup_free_ char *remote = strdup(p); if (!remote) return log_oom(); @@ -79,7 +78,7 @@ static int parse_pull_expression(const char *v) { ImportType type = _IMPORT_TYPE_INVALID; ImageClass class = _IMAGE_CLASS_INVALID; ImportVerify verify = IMPORT_VERIFY_SIGNATURE; - bool ro = false, blockdev = false; + bool ro = false, blockdev = false, bootorigin = false; const char *o = options; for (;;) { @@ -99,8 +98,9 @@ static int parse_pull_expression(const char *v) { ro = false; else if (streq(opt, "blockdev")) blockdev = true; + else if (streq(opt, "bootorigin")) + bootorigin = true; else if ((suffix = startswith(opt, "verify="))) { - ImportVerify w = import_verify_from_string(suffix); if (w < 0) @@ -124,6 +124,32 @@ static int parse_pull_expression(const char *v) { } } + if (bootorigin) { + _cleanup_free_ char *stub_url = NULL; + + r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("StubDeviceURL"), &stub_url); + if (r == -ENOENT) { + log_debug("Option 'bootorigin' specified, but StubDeviceURL EFI variable not set, not scheduling import job for '%s'.", remote); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to read 'StubDeviceURL' EFI variable: %m"); + + if (!http_url_is_valid(stub_url)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Value of 'StubDeviceURL' is not a valid URL, refusing: %s", stub_url); + + _cleanup_free_ char *result = NULL; + r = import_url_change_last_component(stub_url, remote, &result); + if (r < 0) + return log_error_errno(r, "Failed to replace last component of URL '%s': %m", stub_url); + + log_info("URL reported by StubDeviceURL is '%s', derived download URL '%s' from it.", stub_url, result); + free_and_replace(remote, result); + } + + if (!http_url_is_valid(remote) && !file_url_is_valid(remote)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid URL, refusing: %s", remote); + if (type < 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No image type (raw, tar) specified in pull expression, refusing: %s", v); if (class < 0) From 576c97e16576facb0a37239272b65f6d4f198081 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 11:52:21 +0100 Subject: [PATCH 10/30] importd: optionally allow clients to specify alternative image root --- src/import/importd.c | 17 +++++++++++++++++ src/shared/varlink-io.systemd.Import.c | 2 ++ 2 files changed, 19 insertions(+) diff --git a/src/import/importd.c b/src/import/importd.c index 8e1fe51bf8..324bb618a4 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -73,6 +73,7 @@ struct Transfer { char *remote; char *local; + char *image_root; ImageClass class; ImportFlags flags; char *format; @@ -146,6 +147,7 @@ static Transfer *transfer_unref(Transfer *t) { free(t->remote); free(t->local); free(t->format); + free(t->image_root); free(t->object_path); pidref_done_sigkill_wait(&t->pidref); @@ -455,6 +457,8 @@ static int transfer_start(Transfer *t) { NULL, /* if so: the actual URL */ NULL, /* maybe --format= */ NULL, /* if so: the actual format */ + NULL, /* maybe --image-root= */ + NULL, /* if so: the image root path */ NULL, /* remote */ NULL, /* local */ NULL @@ -548,6 +552,11 @@ static int transfer_start(Transfer *t) { cmd[k++] = t->format; } + if (t->image_root) { + cmd[k++] = "--image-root"; + cmd[k++] = t->image_root; + } + if (!IN_SET(t->type, TRANSFER_EXPORT_TAR, TRANSFER_EXPORT_RAW)) { if (t->remote) cmd[k++] = t->remote; @@ -1838,6 +1847,7 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl bool force; bool read_only; bool keep_download; + const char *image_root; } p = { .class = _IMAGE_CLASS_INVALID, .verify = IMPORT_VERIFY_SIGNATURE, @@ -1852,6 +1862,7 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl { "force", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct p, force), 0 }, { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct p, read_only), 0 }, { "keepDownload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct p, keep_download), 0 }, + { "imageRoot", SD_JSON_VARIANT_BOOLEAN, json_dispatch_const_path, offsetof(struct p, image_root), SD_JSON_STRICT }, VARLINK_DISPATCH_POLKIT_FIELD, {}, }; @@ -1918,6 +1929,12 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl return -ENOMEM; } + if (p.image_root) { + t->image_root = strdup(p.image_root); + if (!t->image_root) + return -ENOMEM; + } + r = transfer_start(t); if (r < 0) return r; diff --git a/src/shared/varlink-io.systemd.Import.c b/src/shared/varlink-io.systemd.Import.c index 66030e63fc..400a4429d6 100644 --- a/src/shared/varlink-io.systemd.Import.c +++ b/src/shared/varlink-io.systemd.Import.c @@ -91,6 +91,8 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_DEFINE_INPUT(readOnly, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Whether to keep a pristine copy of the download separate from the locally installed image. Defaults to false."), SD_VARLINK_DEFINE_INPUT(keepDownload, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Root directory for images. If not specified derived from the image class, and located below /var/lib/."), + SD_VARLINK_DEFINE_INPUT(imageRoot, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), VARLINK_DEFINE_POLKIT_INPUT, SD_VARLINK_FIELD_COMMENT("A progress update, as percent value"), SD_VARLINK_DEFINE_OUTPUT(progress, SD_VARLINK_FLOAT, SD_VARLINK_NULLABLE), From 8b918a3a38c3ed5cf3acbc8c41e4cd0ed97f5b38 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 11:57:19 +0100 Subject: [PATCH 11/30] import-generator: optionally import images into /run/ hierarchy rather than /var/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is useful in particular in the initrd, as this ensures any downloaded images are not deleted during the initrd→host transition (where /var/ does not survive, but /run/ does). Might be useful in other cases too, for example for transiently deployed confexts and such. --- man/systemd-import-generator.xml | 11 +++++++++++ src/import/import-generator.c | 23 +++++++++++++++++------ src/import/importd.c | 2 +- src/shared/discover-image.c | 9 +++++++++ src/shared/discover-image.h | 1 + 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index d60c9c96fe..cea015e6f8 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -144,6 +144,17 @@ + + + runtime= + + Takes a boolean argument. If set to true, the image is downloaded below the + /run/ hierarchy, if set to false below the /var/lib/ + hierarchy. If not specified defaults to true in the initial RAM disk (initrd) and to false on the + host system. + + + diff --git a/src/import/import-generator.c b/src/import/import-generator.c index 5d2f37107e..d63ed23a7b 100644 --- a/src/import/import-generator.c +++ b/src/import/import-generator.c @@ -11,6 +11,7 @@ #include "import-util.h" #include "initrd-util.h" #include "json-util.h" +#include "parse-util.h" #include "proc-cmdline.h" #include "special.h" #include "specifier.h" @@ -22,6 +23,7 @@ typedef struct Transfer { ImportType type; char *local; char *remote; + const char *image_root; bool blockdev; sd_json_variant *json; } Transfer; @@ -78,7 +80,7 @@ static int parse_pull_expression(const char *v) { ImportType type = _IMPORT_TYPE_INVALID; ImageClass class = _IMAGE_CLASS_INVALID; ImportVerify verify = IMPORT_VERIFY_SIGNATURE; - bool ro = false, blockdev = false, bootorigin = false; + bool ro = false, blockdev = false, bootorigin = false, runtime = in_initrd(); const char *o = options; for (;;) { @@ -100,7 +102,13 @@ static int parse_pull_expression(const char *v) { blockdev = true; else if (streq(opt, "bootorigin")) bootorigin = true; - else if ((suffix = startswith(opt, "verify="))) { + else if ((suffix = startswith(opt, "runtime="))) { + r = parse_boolean(suffix); + if (r < 0) + log_warning_errno(r, "Unknown runtime= parameter, ignoring: %s", suffix); + else + runtime = r; + } else if ((suffix = startswith(opt, "verify="))) { ImportVerify w = import_verify_from_string(suffix); if (w < 0) @@ -187,8 +195,9 @@ static int parse_pull_expression(const char *v) { if (!GREEDY_REALLOC(arg_transfers, arg_n_transfers + 1)) return log_oom(); - _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL; + const char *image_root = runtime ? image_root_runtime_to_string(class) : image_root_to_string(class); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL; r = sd_json_buildo( &j, SD_JSON_BUILD_PAIR("remote", SD_JSON_BUILD_STRING(remote)), @@ -196,7 +205,8 @@ static int parse_pull_expression(const char *v) { SD_JSON_BUILD_PAIR("class", JSON_BUILD_STRING_UNDERSCORIFY(image_class_to_string(class))), SD_JSON_BUILD_PAIR("type", JSON_BUILD_STRING_UNDERSCORIFY(import_type_to_string(type))), SD_JSON_BUILD_PAIR("readOnly", SD_JSON_BUILD_BOOLEAN(ro)), - SD_JSON_BUILD_PAIR("verify", JSON_BUILD_STRING_UNDERSCORIFY(import_verify_to_string(verify)))); + SD_JSON_BUILD_PAIR("verify", JSON_BUILD_STRING_UNDERSCORIFY(import_verify_to_string(verify))), + SD_JSON_BUILD_PAIR("imageRoot", SD_JSON_BUILD_STRING(image_root))); if (r < 0) return log_error_errno(r, "Failed to build import JSON object: %m"); @@ -205,6 +215,7 @@ static int parse_pull_expression(const char *v) { .type = type, .local = TAKE_PTR(local), .remote = TAKE_PTR(remote), + .image_root = image_root, .json = TAKE_PTR(j), .blockdev = blockdev, }; @@ -322,11 +333,11 @@ static int transfer_generate(const Transfer *t, size_t c) { fputs("Wants=network-online.target\n" "After=network-online.target\n", f); - _cleanup_free_ char *local_path = NULL, *loop_service = NULL; + _cleanup_free_ char *loop_service = NULL; if (t->blockdev) { assert(t->type == IMPORT_RAW); - local_path = strjoin(image_root_to_string(t->class), "/", t->local, ".raw"); + _cleanup_free_ char *local_path = strjoin(t->image_root, "/", t->local, ".raw"); if (!local_path) return log_oom(); diff --git a/src/import/importd.c b/src/import/importd.c index 324bb618a4..5a1dcb92a5 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -1862,7 +1862,7 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl { "force", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct p, force), 0 }, { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct p, read_only), 0 }, { "keepDownload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(struct p, keep_download), 0 }, - { "imageRoot", SD_JSON_VARIANT_BOOLEAN, json_dispatch_const_path, offsetof(struct p, image_root), SD_JSON_STRICT }, + { "imageRoot", SD_JSON_VARIANT_STRING, json_dispatch_const_path, offsetof(struct p, image_root), SD_JSON_STRICT }, VARLINK_DISPATCH_POLKIT_FIELD, {}, }; diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index e2f05dca93..d6afa53a6b 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -110,6 +110,15 @@ static const char *const image_root_table[_IMAGE_CLASS_MAX] = { DEFINE_STRING_TABLE_LOOKUP_TO_STRING(image_root, ImageClass); +static const char *const image_root_runtime_table[_IMAGE_CLASS_MAX] = { + [IMAGE_MACHINE] = "/run/machines", + [IMAGE_PORTABLE] = "/run/portables", + [IMAGE_SYSEXT] = "/run/extensions", + [IMAGE_CONFEXT] = "/run/confexts", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(image_root_runtime, ImageClass); + static Image *image_free(Image *i) { assert(i); diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index c64d0baefb..66dc7ef3d2 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -122,6 +122,7 @@ static inline bool IMAGE_IS_HOST(const struct Image *i) { int image_to_json(const struct Image *i, sd_json_variant **ret); const char* image_root_to_string(ImageClass c) _const_; +const char* image_root_runtime_to_string(ImageClass c) _const_; extern const struct hash_ops image_hash_ops; From b4bee5684db5f252e7480d66fd81625e91951498 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 12:56:08 +0100 Subject: [PATCH 12/30] fstab-generator: support creating bind mounts via root= kernel cmdline switches This is useful for bind mounting a freshly downloaded and unpacked tar disk images to /sysroot to mount into. Specifically, with a kernel command line like this one: rd.systemd.pull=verify=no,machine,tar:root:http://_gateway:8081/image.tar root=bind:/run/machines/root ip=any The first parameter downloads the root image, the second one then binds it to /sysroot so that we can boot into it. --- man/systemd-fstab-generator.xml | 4 + man/systemd-import-generator.xml | 10 ++ src/fstab-generator/fstab-generator.c | 144 +++++++++++------- .../test-12-dev-sdx.expected/sysroot.mount | 1 + .../test-13-label.expected/sysroot.mount | 1 + .../test-14-uuid.expected/sysroot.mount | 1 + .../test-15-partuuid.expected/sysroot.mount | 1 + .../test-16-tmpfs.expected/sysroot.mount | 1 + 8 files changed, 112 insertions(+), 51 deletions(-) diff --git a/man/systemd-fstab-generator.xml b/man/systemd-fstab-generator.xml index 42c6d27bfc..f04334b985 100644 --- a/man/systemd-fstab-generator.xml +++ b/man/systemd-fstab-generator.xml @@ -99,6 +99,10 @@ separate, immutable /usr/ file system. Also see systemd.volatile= below. + Use bind:… to bind mount another directory as operating system root + filesystems (added in v258). Expects an absolute path name referencing an existing directory within the initrd's file + hierarchy to boot into. + diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index cea015e6f8..dfadf5db5f 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -242,6 +242,16 @@ http://example.com/image.efi this would result in a root disk being downloaded from http://example.com/image.raw.xz. + + + Boot into disk image (tar), with URL derived from UEFI HTTP network booting + + systemd.pull=tar,machine,verify=no,bootorigin:root:image.tar.xz root=bind:/run/machines/root + + This is similar to the previous example, but instead of a raw (i.e. block device based) disk + image the system boots into a tarball that is downloaded from the originating UEFI network + server. + diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index 4dfe9fea89..475981f36d 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -494,7 +494,8 @@ static int add_mount( const char *opts, int passno, MountPointFlags flags, - const char *target_unit) { + const char *target_unit, + const char *extra_after) { _cleanup_free_ char *name = NULL, *automount_name = NULL, *filtered = NULL, *where_escaped = NULL, *opts_root_filtered = NULL; @@ -596,6 +597,9 @@ static int add_mount( if (target_unit && !FLAGS_SET(flags, MOUNT_NOFAIL)) fprintf(f, "Before=%s\n", target_unit); + if (extra_after) + fprintf(f, "After=%s\n", extra_after); + if (passno != 0) { r = generator_write_fsck_deps(f, dest, what, where, fstype); if (r < 0) @@ -814,15 +818,16 @@ static bool sysfs_check(void) { static int add_sysusr_sysroot_usr_bind_mount(const char *source) { return add_mount(source, - arg_dest, - "/sysusr/usr", - "/sysroot/usr", - NULL, - NULL, - "bind", - 0, - 0, - SPECIAL_INITRD_FS_TARGET); + arg_dest, + "/sysusr/usr", + "/sysroot/usr", + /* original_where= */ NULL, + /* fstype= */ NULL, + "bind", + /* passno= */ 0, + /* flags= */ 0, + SPECIAL_INITRD_FS_TARGET, + /* extra_after= */ NULL); } static MountPointFlags fstab_options_to_flags(const char *options, bool is_swap) { @@ -1000,7 +1005,8 @@ static int parse_fstab_one( options, passno, flags, - target_unit); + target_unit, + /* extra_after= */ NULL); if (r <= 0) return r; @@ -1105,8 +1111,8 @@ static int sysroot_is_nfsroot(void) { static int add_sysroot_mount(void) { _cleanup_free_ char *what = NULL; - const char *opts, *fstype; - bool default_rw, makefs; + const char *extra_opts = NULL, *fstype = NULL; + bool default_rw = true, makefs = false; MountPointFlags flags; int r; @@ -1149,7 +1155,20 @@ static int add_sysroot_mount(void) { return 0; } - if (streq(arg_root_what, "tmpfs")) { + const char *bind = startswith(arg_root_what, "bind:"); + if (bind) { + if (!path_is_valid(bind) || !path_is_absolute(bind)) { + log_debug("Invalid root=bind: source path, ignoring: %s", bind); + return 0; + } + + what = strdup(bind); + if (!what) + return log_oom(); + + extra_opts = "bind"; + + } else if (streq(arg_root_what, "tmpfs")) { /* If root=tmpfs is specified, then take this as shortcut for a writable tmpfs mount as root */ what = strdup("rootfs"); /* just a pretty name, to show up in /proc/self/mountinfo */ @@ -1157,8 +1176,6 @@ static int add_sysroot_mount(void) { return log_oom(); fstype = arg_root_fstype ?: "tmpfs"; /* tmpfs, unless overridden */ - - default_rw = true; /* writable, unless overridden */; } else { what = fstab_node_to_udev_node(arg_root_what); @@ -1166,40 +1183,44 @@ static int add_sysroot_mount(void) { return log_oom(); fstype = arg_root_fstype; /* if not specified explicitly, don't default to anything here */ - default_rw = false; /* read-only, unless overridden */ } - if (!arg_root_options) - opts = arg_root_rw > 0 || (arg_root_rw < 0 && default_rw) ? "rw" : "ro"; - else if (arg_root_rw >= 0 || - !fstab_test_option(arg_root_options, "ro\0" "rw\0")) - opts = strjoina(arg_root_options, ",", arg_root_rw > 0 ? "rw" : "ro"); - else - opts = arg_root_options; + _cleanup_free_ char *combined_options = NULL; + if (strdup_to(&combined_options, arg_root_options) < 0) + return log_oom(); - log_debug("Found entry what=%s where=/sysroot type=%s opts=%s", what, strna(arg_root_fstype), strempty(opts)); + if (arg_root_rw >= 0 || !fstab_test_option(combined_options, "ro\0" "rw\0")) + if (!strextend_with_separator(&combined_options, ",", arg_root_rw > 0 || (arg_root_rw < 0 && default_rw) ? "rw" : "ro")) + return log_oom(); - makefs = fstab_test_option(opts, "x-systemd.makefs\0"); + if (extra_opts) + if (!strextend_with_separator(&combined_options, ",", extra_opts)) + return log_oom(); + + log_debug("Found entry what=%s where=/sysroot type=%s opts=%s", what, strna(arg_root_fstype), strempty(combined_options)); + + makefs = fstab_test_option(combined_options, "x-systemd.makefs\0"); flags = makefs * MOUNT_MAKEFS; return add_mount("/proc/cmdline", arg_dest, what, "/sysroot", - NULL, + /* original_where= */ NULL, fstype, - opts, - is_device_path(what) ? 1 : 0, /* passno */ - flags, /* makefs off, pcrfs off, quota off, noauto off, nofail off, automount off */ - SPECIAL_INITRD_ROOT_FS_TARGET); + combined_options, + /* passno= */ is_device_path(what) ? 1 : 0, + flags, + SPECIAL_INITRD_ROOT_FS_TARGET, + "imports.target"); } static int add_sysroot_usr_mount(void) { _cleanup_free_ char *what = NULL; - const char *opts; - bool makefs; + const char *extra_opts = NULL; MountPointFlags flags; + bool makefs; int r; /* Returns 0 if we didn't do anything, > 0 if we either generated a unit for the /usr/ mount, or we @@ -1250,16 +1271,35 @@ static int add_sysroot_usr_mount(void) { return 1; /* As above, report that NFS code will create the unit */ } - what = fstab_node_to_udev_node(arg_usr_what); - if (!what) + const char *bind = startswith(arg_usr_what, "bind:"); + if (bind) { + if (!path_is_valid(bind) || !path_is_absolute(bind)) { + log_debug("Invalid mount.usr=bind: source path, ignoring: %s", bind); + return 0; + } + + what = strdup(bind); + if (!what) + return log_oom(); + + extra_opts = "bind"; + } else { + what = fstab_node_to_udev_node(arg_usr_what); + if (!what) + return log_oom(); + } + + _cleanup_free_ char *combined_options = NULL; + if (strdup_to(&combined_options, arg_usr_options) < 0) return log_oom(); - if (!arg_usr_options) - opts = arg_root_rw > 0 ? "rw" : "ro"; - else if (!fstab_test_option(arg_usr_options, "ro\0" "rw\0")) - opts = strjoina(arg_usr_options, ",", arg_root_rw > 0 ? "rw" : "ro"); - else - opts = arg_usr_options; + if (!fstab_test_option(combined_options, "ro\0" "rw\0")) + if (!strextend_with_separator(&combined_options, ",", arg_root_rw > 0 ? "rw" : "ro")) + return log_oom(); + + if (extra_opts) + if (!strextend_with_separator(&combined_options, ",", extra_opts)) + return log_oom(); /* When mounting /usr from the initrd, we add an extra level of indirection: we first mount the /usr/ * partition to /sysusr/usr/, and then afterwards bind mount that to /sysroot/usr/. We do this so @@ -1268,21 +1308,22 @@ static int add_sysroot_usr_mount(void) { * this should order itself after initrd-usr-fs.target and before initrd-fs.target; and it should * look into both /sysusr/ and /sysroot/ for the configuration data to apply. */ - log_debug("Found entry what=%s where=/sysusr/usr type=%s opts=%s", what, strna(arg_usr_fstype), strempty(opts)); + log_debug("Found entry what=%s where=/sysusr/usr type=%s opts=%s", what, strna(arg_usr_fstype), strempty(combined_options)); - makefs = fstab_test_option(opts, "x-systemd.makefs\0"); + makefs = fstab_test_option(combined_options, "x-systemd.makefs\0"); flags = makefs * MOUNT_MAKEFS; r = add_mount("/proc/cmdline", arg_dest, what, "/sysusr/usr", - NULL, + /* original_where= */ NULL, arg_usr_fstype, - opts, - is_device_path(what) ? 1 : 0, /* passno */ + combined_options, + /* passno= */ is_device_path(what) ? 1 : 0, flags, - SPECIAL_INITRD_USR_FS_TARGET); + SPECIAL_INITRD_USR_FS_TARGET, + "imports.target"); if (r < 0) return r; @@ -1336,12 +1377,13 @@ static int add_volatile_var(void) { arg_dest_late, "tmpfs", "/var", - NULL, + /* original_where= */ NULL, "tmpfs", "mode=0755" TMPFS_LIMITS_VAR, - 0, - 0, - SPECIAL_LOCAL_FS_TARGET); + /* passno= */ 0, + /* flags= */ 0, + SPECIAL_LOCAL_FS_TARGET, + /* extra_after= */ NULL); } static int add_mounts_from_cmdline(void) { diff --git a/test/test-fstab-generator/test-12-dev-sdx.expected/sysroot.mount b/test/test-fstab-generator/test-12-dev-sdx.expected/sysroot.mount index 8f8ef48617..92eed56423 100644 --- a/test/test-fstab-generator/test-12-dev-sdx.expected/sysroot.mount +++ b/test/test-fstab-generator/test-12-dev-sdx.expected/sysroot.mount @@ -4,6 +4,7 @@ Documentation=man:fstab(5) man:systemd-fstab-generator(8) SourcePath=/proc/cmdline Before=initrd-root-fs.target +After=imports.target Requires=systemd-fsck-root.service After=systemd-fsck-root.service After=blockdev@dev-sdx1.target diff --git a/test/test-fstab-generator/test-13-label.expected/sysroot.mount b/test/test-fstab-generator/test-13-label.expected/sysroot.mount index 98698d5968..8b0f6cce25 100644 --- a/test/test-fstab-generator/test-13-label.expected/sysroot.mount +++ b/test/test-fstab-generator/test-13-label.expected/sysroot.mount @@ -4,6 +4,7 @@ Documentation=man:fstab(5) man:systemd-fstab-generator(8) SourcePath=/proc/cmdline Before=initrd-root-fs.target +After=imports.target Requires=systemd-fsck-root.service After=systemd-fsck-root.service After=blockdev@dev-disk-by\x2dlabel-Root.target diff --git a/test/test-fstab-generator/test-14-uuid.expected/sysroot.mount b/test/test-fstab-generator/test-14-uuid.expected/sysroot.mount index 999acb0b23..096da67f1e 100644 --- a/test/test-fstab-generator/test-14-uuid.expected/sysroot.mount +++ b/test/test-fstab-generator/test-14-uuid.expected/sysroot.mount @@ -4,6 +4,7 @@ Documentation=man:fstab(5) man:systemd-fstab-generator(8) SourcePath=/proc/cmdline Before=initrd-root-fs.target +After=imports.target Requires=systemd-fsck-root.service After=systemd-fsck-root.service After=blockdev@dev-disk-by\x2duuid-3f5ad593\x2d4546\x2d4a94\x2da374\x2dbcfb68aa11f7.target diff --git a/test/test-fstab-generator/test-15-partuuid.expected/sysroot.mount b/test/test-fstab-generator/test-15-partuuid.expected/sysroot.mount index d10fb6ef76..6d948b91c2 100644 --- a/test/test-fstab-generator/test-15-partuuid.expected/sysroot.mount +++ b/test/test-fstab-generator/test-15-partuuid.expected/sysroot.mount @@ -4,6 +4,7 @@ Documentation=man:fstab(5) man:systemd-fstab-generator(8) SourcePath=/proc/cmdline Before=initrd-root-fs.target +After=imports.target Requires=systemd-fsck-root.service After=systemd-fsck-root.service After=blockdev@dev-disk-by\x2dpartuuid-3f5ad593\x2d4546\x2d4a94\x2da374\x2dbcfb68aa11f7.target diff --git a/test/test-fstab-generator/test-16-tmpfs.expected/sysroot.mount b/test/test-fstab-generator/test-16-tmpfs.expected/sysroot.mount index 6bd9a07f2c..99728fd0ca 100644 --- a/test/test-fstab-generator/test-16-tmpfs.expected/sysroot.mount +++ b/test/test-fstab-generator/test-16-tmpfs.expected/sysroot.mount @@ -4,6 +4,7 @@ Documentation=man:fstab(5) man:systemd-fstab-generator(8) SourcePath=/proc/cmdline Before=initrd-root-fs.target +After=imports.target [Mount] What=rootfs From fe72166979326e0c6d45c8397a9136e6a322a161 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 13:12:46 +0100 Subject: [PATCH 13/30] fstab-generator: validate root= and mount.usr= the same way In both cases, let's check for the same special mount sources. We already covered some of the same, but let's just make it the same codepaths. --- man/systemd-import-generator.xml | 2 +- src/fstab-generator/fstab-generator.c | 132 ++++++++++++-------------- 2 files changed, 63 insertions(+), 71 deletions(-) diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index dfadf5db5f..ba95ebe2c1 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -246,7 +246,7 @@ Boot into disk image (tar), with URL derived from UEFI HTTP network booting - systemd.pull=tar,machine,verify=no,bootorigin:root:image.tar.xz root=bind:/run/machines/root + rd.systemd.pull=tar,machine,verify=no,bootorigin:root:image.tar.xz root=bind:/run/machines/root This is similar to the previous example, but instead of a raw (i.e. block device based) disk image the system boots into a tarball that is downloaded from the originating UEFI network diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index 475981f36d..6693333c4b 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -1058,12 +1058,12 @@ static int parse_fstab(bool prefix_sysroot) { return ret; } -static int sysroot_is_nfsroot(void) { +static int mount_source_is_nfsroot(const char *what) { union in_addr_union u; const char *sep, *a; int r; - assert(arg_root_what); + assert(what); /* From dracut.cmdline(7). * @@ -1076,18 +1076,18 @@ static int sysroot_is_nfsroot(void) { * If server-ip is an IPv6 address it has to be put in brackets, e.g. [2001:DB8::1]. NFS options * can be appended with the prefix ":" or "," and are separated by ",". */ - if (path_equal(arg_root_what, "/dev/nfs") || - STR_IN_SET(arg_root_what, "dhcp", "dhcp6") || - STARTSWITH_SET(arg_root_what, "nfs:", "nfs4:")) + if (path_equal(what, "/dev/nfs") || + STR_IN_SET(what, "dhcp", "dhcp6") || + STARTSWITH_SET(what, "nfs:", "nfs4:")) return true; /* IPv6 address */ - if (arg_root_what[0] == '[') { - sep = strchr(arg_root_what + 1, ']'); + if (what[0] == '[') { + sep = strchr(what + 1, ']'); if (!sep) return -EINVAL; - a = strndupa_safe(arg_root_what + 1, sep - arg_root_what - 1); + a = strndupa_safe(what + 1, sep - what - 1); r = in_addr_from_string(AF_INET6, a, &u); if (r < 0) @@ -1097,16 +1097,65 @@ static int sysroot_is_nfsroot(void) { } /* IPv4 address */ - sep = strchr(arg_root_what, ':'); + sep = strchr(what, ':'); if (sep) { - a = strndupa_safe(arg_root_what, sep - arg_root_what); + a = strndupa_safe(what, sep - what); if (in_addr_from_string(AF_INET, a, &u) >= 0) return true; } /* root directory without address */ - return path_is_absolute(arg_root_what) && !path_startswith(arg_root_what, "/dev"); + return path_is_absolute(what) && !path_startswith(what, "/dev"); +} + +static bool validate_root_or_usr_mount_source(const char *what, const char *switch_name) { + int r; + + assert(switch_name); + + if (isempty(what)) { + log_debug("No %s switch specified on the kernel command line.", switch_name); + return false; + } + + if (streq(what, "gpt-auto")) { + /* This is handled by gpt-auto-generator */ + log_debug("Skipping %s directory handling, as gpt-auto was requested.", switch_name); + return false; + } + + if (streq(what, "fstab")) { + /* This is handled by parse_fstab() */ + log_debug("Using initrd's fstab for %s configuration.", switch_name); + return false; + } + + r = mount_source_is_nfsroot(what); + if (r < 0) + log_debug_errno(r, "Failed to determine if the %s directory is on NFS, assuming not: %m", switch_name); + else if (r > 0) { + /* This is handled by the kernel or the initrd */ + log_debug("Skipping %s directory handling, as root on NFS was requested.", switch_name); + return false; + } + + if (startswith(what, "cifs://")) { + log_debug("Skipping %s directory handling, as root on CIFS was requested.", switch_name); + return false; + } + + if (startswith(what, "iscsi:")) { + log_debug("Skipping %s directory handling, as root on iSCSI was requested.", switch_name); + return false; + } + + if (startswith(what, "live:")) { + log_debug("Skipping %s directory handling, as root on live image was requested.", switch_name); + return false; + } + + return true; } static int add_sysroot_mount(void) { @@ -1114,46 +1163,9 @@ static int add_sysroot_mount(void) { const char *extra_opts = NULL, *fstype = NULL; bool default_rw = true, makefs = false; MountPointFlags flags; - int r; - if (isempty(arg_root_what)) { - log_debug("Could not find a root= entry on the kernel command line."); + if (!validate_root_or_usr_mount_source(arg_root_what, "root=")) return 0; - } - - if (streq(arg_root_what, "gpt-auto")) { - /* This is handled by gpt-auto-generator */ - log_debug("Skipping root directory handling, as gpt-auto was requested."); - return 0; - } else if (streq(arg_root_what, "fstab")) { - /* This is handled by parse_fstab */ - log_debug("Using initrd's fstab for /sysroot/ configuration."); - return 0; - } - - r = sysroot_is_nfsroot(); - if (r < 0) - log_debug_errno(r, "Failed to determine if the root directory is on NFS, assuming not: %m"); - else if (r > 0) { - /* This is handled by the kernel or the initrd */ - log_debug("Skipping root directory handling, as root on NFS was requested."); - return 0; - } - - if (startswith(arg_root_what, "cifs://")) { - log_debug("Skipping root directory handling, as root on CIFS was requested."); - return 0; - } - - if (startswith(arg_root_what, "iscsi:")) { - log_debug("Skipping root directory handling, as root on iSCSI was requested."); - return 0; - } - - if (startswith(arg_root_what, "live:")) { - log_debug("Skipping root directory handling, as root on live image was requested."); - return 0; - } const char *bind = startswith(arg_root_what, "bind:"); if (bind) { @@ -1248,28 +1260,8 @@ static int add_sysroot_usr_mount(void) { return log_oom(); } - if (isempty(arg_usr_what)) { - log_debug("Could not find a mount.usr= entry on the kernel command line."); + if (!validate_root_or_usr_mount_source(arg_usr_what, "mount.usr=")) return 0; - } - - if (streq(arg_usr_what, "gpt-auto")) { - /* This is handled by the gpt-auto generator */ - log_debug("Skipping /usr/ directory handling, as gpt-auto was requested."); - return 1; /* systemd-gpt-auto-generator will generate a unit for this, hence report that a - * unit file is being created for the host /usr/ mount. */ - } else if (streq(arg_usr_what, "fstab")) { - /* This is handled by parse_fstab */ - log_debug("Using initrd's fstab for /sysroot/usr/ configuration."); - return 1; /* parse_fstab will generate a unit for this, hence report that a - * unit file is being created for the host /usr/ mount. */ - } - - if (path_equal(arg_usr_what, "/dev/nfs")) { - /* This is handled by the initrd (if at all supported, that is) */ - log_debug("Skipping /usr/ directory handling, as /dev/nfs was requested."); - return 1; /* As above, report that NFS code will create the unit */ - } const char *bind = startswith(arg_usr_what, "bind:"); if (bind) { From 3d49850096228f5923d2ff9370313d405cac1cb5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 7 Feb 2025 18:22:13 +0100 Subject: [PATCH 14/30] import-generator: give import services better names Previously, we'd name the import services numerically. Let's instead use the local target file name, i.e. the object we are creating with these services locally. That's useful so that we can robustely order against these service instances, should we need to one day. --- src/import/import-generator.c | 37 +++++++++++++++++++------- test/units/TEST-13-NSPAWN.importctl.sh | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/import/import-generator.c b/src/import/import-generator.c index d63ed23a7b..3e052e54ec 100644 --- a/src/import/import-generator.c +++ b/src/import/import-generator.c @@ -289,15 +289,39 @@ static int parse_credentials(void) { return 0; } -static int transfer_generate(const Transfer *t, size_t c) { +static char *transfer_get_local_path(const Transfer *t) { + assert(t); + assert(t->image_root); + assert(t->local); + + switch (t->type) { + case IMPORT_RAW: + return strjoin(t->image_root, "/", t->local, ".raw"); + + case IMPORT_TAR: + return path_join(t->image_root, t->local); + + default: + assert_not_reached(); + } +} + +static int transfer_generate(const Transfer *t) { int r; assert(t); - _cleanup_free_ char *service = NULL; - if (asprintf(&service, "import%zu.service", c) < 0) + _cleanup_free_ char *local_path = transfer_get_local_path(t); + if (!local_path) return log_oom(); + /* Give this unit a clear name derived from the file system object we are installed into the OS, so + * that other components can nicely have dependencies on this. */ + _cleanup_free_ char *service = NULL; + r = unit_name_from_path_instance("systemd-import", local_path, ".service", &service); + if (r < 0) + return log_error_errno(r, "Failed to build import unit name from '%s': %m", local_path); + _cleanup_fclose_ FILE *f = NULL; r = generator_open_unit_file(arg_dest, /* source = */ NULL, service, &f); if (r < 0) @@ -337,10 +361,6 @@ static int transfer_generate(const Transfer *t, size_t c) { if (t->blockdev) { assert(t->type == IMPORT_RAW); - _cleanup_free_ char *local_path = strjoin(t->image_root, "/", t->local, ".raw"); - if (!local_path) - return log_oom(); - r = unit_name_from_path_instance("systemd-loop", local_path, ".service", &loop_service); if (r < 0) return log_error_errno(r, "Failed to build systemd-loop@.service instance name from path '%s': %m", local_path); @@ -384,11 +404,10 @@ static int transfer_generate(const Transfer *t, size_t c) { } static int generate(void) { - size_t c = 0; int r = 0; FOREACH_ARRAY(i, arg_transfers, arg_n_transfers) - RET_GATHER(r, transfer_generate(i, c++)); + RET_GATHER(r, transfer_generate(i)); return r; } diff --git a/test/units/TEST-13-NSPAWN.importctl.sh b/test/units/TEST-13-NSPAWN.importctl.sh index ac4a70e9b0..e4931a9bfd 100755 --- a/test/units/TEST-13-NSPAWN.importctl.sh +++ b/test/units/TEST-13-NSPAWN.importctl.sh @@ -81,7 +81,7 @@ cat /proc/cmdline systemctl daemon-reload -systemctl start import0.service +systemctl start systemd-import@var-lib-confexts-importtest9.service cmp /var/tmp/importtest /var/lib/confexts/importtest9/importtest # Verify generic service calls, too From f8825c1364fb1c3f3f5d96654e779fd51500f476 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 22:24:52 +0100 Subject: [PATCH 15/30] udev-builtin-blkid: use loopback block device 'ref' field fo determining gpt-auto whole block device So far the gpt-auto logic only looked for the partition table of devices that the ESP/XBOOTLDR partition used to boot was on. This works great for local boots, but is more problematic if we boot a UKI via UEFI HTTP boot, because there is no ESP in play in that case. Let's introduce an alternative to communicate the intended default root disk to cover for this situation: any loopback block device whose backing file field (i.e. the userspace controlled freeform field we use for /dev/disk/by-loop-ref/ naming) is set to "rootdisk" will be consider for gpt-auto will be consider for gpt-auto. With this in place we should have nice automatic behaviour: 1. If we are booted locally we'll get the ESP/XBOOTLDR data, and derive the root disk from that. 2. If we are booted via UEFI HTTP boot we expect that the caller makes the loopback device appear with the right loop-ref identifier, and then will use that. --- man/systemd-gpt-auto-generator.xml | 61 ++++++++++++++++------- man/systemd-import-generator.xml | 18 ++++++- src/udev/udev-builtin-blkid.c | 77 ++++++++++++++++++++---------- 3 files changed, 113 insertions(+), 43 deletions(-) diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index 2c37c8af03..69788baab4 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -50,19 +50,49 @@ If the units this generator creates are overridden, for example by units in directories with higher precedence, drop-ins and additional dependencies created by this generator might still be used. - This generator will only look for the root partition on the same physical disk where the EFI System - Partition (ESP) is located. Note that support from the boot loader is required: the EFI variable - LoaderDevicePartUUID of the 4a67b082-0a4c-41cf-b6c7-440b29bb8c4f - vendor UUID is used to determine from which partition, and hence the disk, from which the system was - booted. If the boot loader does not set this variable, this generator will not be able to detect the root - partition. See the Boot Loader Interface - for details. + When run in the initial RAM disk (initrd) this generator can automatically search for the root file + system. Specifically: - Similarly, this generator will only look for the other partitions on the same physical disk as the - root partition. In this case, boot loader support is not required. These partitions will not be searched - for on systems where the root file system is distributed on multiple disks, for example via btrfs RAID. + + It will look for the root partition on the same physical disk where the EFI System + Partition (ESP) is located. Note that support from the boot loader is required for this to work: the + EFI variable LoaderDevicePartUUID of the + 4a67b082-0a4c-41cf-b6c7-440b29bb8c4f vendor UUID is used to determine from which + partition (and hence disk) the system was booted. If the boot loader does not set this variable, this + generator will not be able to detect the root partition. See the Boot Loader Interface for + details. + + Alternatively, it will look for the root file system on a loopback block device whose + .lo_name field is set to one of the literal strings rootdisk or + rootdisk.raw. This field can be set via losetup's + string. For images downloaded via + systemd-import-generator8 + make sure to set the blockdev option and set the local name string to + rootdisk to achieve this effect. Note that discovery of the root file system on + loopback block devices like this is only done if root=gpt-auto is specified + explicitly on the kernel command line, unlike the discovery based on the boot loader reported ESP which + is also enabled if no root= parameter is specified at all. (The latter relies on + systemd-udevd.service's /dev/gpt-auto-root block device symlink + generation). + + When run on the host system (i.e. after successfully transitioning out of the initrd into the root + filesystem) this generator will look for all other partitions on the same physical disk as the root + partition. For this discovery, boot loader support is not required. Moreover, it is not required that the + root partition was automatically discovered by the initrd (as described above) for the discovery of the + non-root file partitions to take place. Or in other words: automatic discovery of the root file system + and of the non-root file systems are independent operations, that do not rely on each other, and are done + during two distinct phases of the boot process (one in the initrd, the other after). These partitions will + not be searched for on systems where the root file system is distributed on multiple disks, for example + via btrfs RAID. + + The root partition can be configured explicitly by symlinking + /run/systemd/volatile-root to /dev/block/$major:$minor. This is + especially useful if the root mount has been replaced by some form of volatile file system + (overlayfs). + systemd-gpt-auto-generator is useful for centralizing file system configuration in the partition table and making configuration in /etc/fstab or on the kernel command line unnecessary. @@ -225,11 +255,6 @@ bootup7. - The root partition can be specified by symlinking /run/systemd/volatile-root - to /dev/block/$major:$minor. This is especially useful if the root mount has been - replaced by some form of volatile file system (overlayfs). - - Mount and automount units for the EFI System Partition (ESP) and Extended Boot Loader Partition (XBOOTLDR) are generated on EFI systems. If the disk contains an XBOOTLDR partition, as defined in the Boot Loader @@ -297,10 +322,14 @@ rootflags= When root= is used with the special value - gpt-auto (or if the parameter is not used at all), automatic discovery of the root + gpt-auto, full automatic discovery of the root partition based on the GPT partition type is enabled. Any other value disables this logic. + If root= is not specified at all on the kernel command line automatic + discovery of the root partition via the boot loader reported ESP is also enabled, however in this + case discovery based on the loopback block device .lo_name field is not enabled. + The rootfstype= and rootflags= are used to select the file system type and options when the root file system is automatically discovered. diff --git a/man/systemd-import-generator.xml b/man/systemd-import-generator.xml index ba95ebe2c1..767f346bf2 100644 --- a/man/systemd-import-generator.xml +++ b/man/systemd-import-generator.xml @@ -125,6 +125,17 @@ device (via systemd-loop@.service) after completion. This permits booting from downloaded disk images. This is only supported for raw disk images. + Note when this option is used with the purpose of mounting a disk image conforming to the + Discoverable + Disk Image Specification as root file system, and the automatic GPT partition discovery + logic as implemented by + systemd-gpt-auto-generator8 + shall process it, it's essential to specify rootdisk as the local name for the + import. Moreover, root=gpt-auto must be specified on the kernel command line + explicitly. Also, prefix the systemd.pull= commmand line option with + rd. to ensure it is executed in the initial RAM disk (initrd) already, also + see below. + @@ -235,12 +246,14 @@ Boot into disk image (raw), with URL derived from UEFI HTTP network booting - rd.systemd.pull=raw,machine,verify=no,blockdev,bootorigin:rootdisk:image.raw.xz root=/dev/disk/by-loop-ref/rootdisk.raw-part2 + rd.systemd.pull=raw,machine,verify=no,blockdev,bootorigin:rootdisk:image.raw.xz root=gpt-auto This is similar to the previous example, but this time the source URL is automatically derived from the UEFI HTTP network boot URL. For example, if an UKI is booted from an URL http://example.com/image.efi this would result in a root disk being downloaded from - http://example.com/image.raw.xz. + http://example.com/image.raw.xz. Moreover this uses the + systemd-gpt-auto-generator8 + logic to mount the root file system from the disk image. @@ -263,6 +276,7 @@ systemd.system-credentials7 importctl1 systemd-loop@.service8 + systemd-gpt-auto-generator8 diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c index 73417338f5..6fdd6c1044 100644 --- a/src/udev/udev-builtin-blkid.c +++ b/src/udev/udev-builtin-blkid.c @@ -125,12 +125,13 @@ static void print_property(UdevEvent *event, const char *name, const char *value } } -static int find_gpt_root(UdevEvent *event, blkid_probe pr) { +static int find_gpt_root(UdevEvent *event, blkid_probe pr, const char *loop_backing_fname) { #if defined(SD_GPT_ROOT_NATIVE) && ENABLE_EFI - + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + sd_id128_t esp_or_xbootldr = SD_ID128_NULL; _cleanup_free_ char *root_label = NULL; - bool found_esp_or_xbootldr = false; + bool found_esp_or_xbootldr = false, need_esp_or_xbootldr; sd_id128_t root_id = SD_ID128_NULL; int r; @@ -138,13 +139,42 @@ static int find_gpt_root(UdevEvent *event, blkid_probe pr) { assert(pr); /* Iterate through the partitions on this disk, and see if the UEFI ESP or XBOOTLDR partition we - * booted from is on it. If so, find the first root disk, and add a property indicating its partition - * UUID. */ + * booted from is on it. If so, find the newest root partition, and add a property indicating its + * partition UUID. We also do this if we are dealing with a loopback block device whose "backing + * filename" field is set to the string "root". In the latter case we do not search for ESP or + * XBOOTLDR. */ + + if (!device_is_devtype(dev, "disk")) { + log_device_debug(dev, "Skipping GPT root logic on partition block device."); + return 0; + } + + r = efi_loader_get_device_part_uuid(&esp_or_xbootldr); + if (r < 0) { + if (r != -ENOENT && !ERRNO_IS_NOT_SUPPORTED(r)) + return log_debug_errno(r, "Unable to determine loader partition UUID: %m"); + + log_device_debug(dev, "No loader partition UUID EFI variable set, not using partition data for search for default root block device."); + + /* NB: if an ESP/xbootldr field is set, we always use that. We do this in order to guarantee + * systematic behaviour. */ + if (!STRPTR_IN_SET(loop_backing_fname, "rootdisk", "rootdisk.raw")) { + log_device_debug(dev, "Device is not a loopback block device with reference string 'root', not considering block device as default root block device."); + return 0; + } + + /* OK, we have now sufficiently identified this device as the right root "whole" device, + * hence no need to bother with searching for ESP/XBOOTLDR */ + need_esp_or_xbootldr = false; + } else + /* We now know the the ESP/xbootldr UUID, but we cannot be sure yet it's on this block + * device, hence look for it among partitions now */ + need_esp_or_xbootldr = true; errno = 0; blkid_partlist pl = blkid_probe_get_partitions(pr); if (!pl) - return errno_or_else(ENOMEM); + return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to probe partitions: %m"); int nvals = blkid_partlist_numof_partitions(pl); for (int i = 0; i < nvals; i++) { @@ -158,27 +188,21 @@ static int find_gpt_root(UdevEvent *event, blkid_probe pr) { r = blkid_partition_get_uuid_id128(pp, &id); if (r < 0) { - log_debug_errno(r, "Failed to get partition UUID, ignoring: %m"); + log_device_debug_errno(dev, r, "Failed to get partition UUID, ignoring: %m"); continue; } r = blkid_partition_get_type_id128(pp, &type); if (r < 0) { - log_debug_errno(r, "Failed to get partition type UUID, ignoring: %m"); + log_device_debug_errno(dev, r, "Failed to get partition type UUID, ignoring: %m"); continue; } label = blkid_partition_get_name(pp); /* returns NULL if empty */ - if (sd_id128_in_set(type, SD_GPT_ESP, SD_GPT_XBOOTLDR)) { - sd_id128_t esp_or_xbootldr; + if (need_esp_or_xbootldr && sd_id128_in_set(type, SD_GPT_ESP, SD_GPT_XBOOTLDR)) { /* We found an ESP or XBOOTLDR, let's see if it matches the ESP/XBOOTLDR we booted from. */ - - r = efi_loader_get_device_part_uuid(&esp_or_xbootldr); - if (r < 0) - return r; - if (sd_id128_equal(id, esp_or_xbootldr)) found_esp_or_xbootldr = true; @@ -195,17 +219,20 @@ static int find_gpt_root(UdevEvent *event, blkid_probe pr) { if (sd_id128_is_null(root_id) || strverscmp_improved(label, root_label) > 0) { root_id = id; - r = free_and_strdup(&root_label, label); - if (r < 0) - return r; + if (free_and_strdup(&root_label, label) < 0) + return log_oom_debug(); } } } - /* We found the ESP/XBOOTLDR on this disk, and also found a root partition, nice! Let's export its - * UUID */ - if (found_esp_or_xbootldr && !sd_id128_is_null(root_id)) - udev_builtin_add_property(event, "ID_PART_GPT_AUTO_ROOT_UUID", SD_ID128_TO_UUID_STRING(root_id)); + if (!need_esp_or_xbootldr || found_esp_or_xbootldr) { + /* We found the ESP/XBOOTLDR on this disk (or we didn't need it) */ + udev_builtin_add_property(event, "ID_PART_GPT_AUTO_ROOT_DISK", "1"); + + /* We found a root partition, nice! Let's export its UUID. */ + if (!sd_id128_is_null(root_id)) + udev_builtin_add_property(event, "ID_PART_GPT_AUTO_ROOT_UUID", SD_ID128_TO_UUID_STRING(root_id)); + } #endif return 0; @@ -437,9 +464,6 @@ static int builtin_blkid(UdevEvent *event, int argc, char *argv[]) { udev_builtin_add_property(event, "ID_PART_GPT_AUTO_ROOT", "1"); } - if (is_gpt) - find_gpt_root(event, pr); - r = read_loopback_backing_inode( dev, fd, @@ -466,6 +490,9 @@ static int builtin_blkid(UdevEvent *event, int argc, char *argv[]) { } } + if (is_gpt) + find_gpt_root(event, pr, backing_fname); + return 0; } From 1a95fc870a5a02e358a7067f67fa4309844dc6f3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 23:15:15 +0100 Subject: [PATCH 16/30] gpt-auto-generator: if root=gpt-auto is specified on kernel cmdline, always wait for /dev/gpt-auto-root symlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So far our login in gpt-auto-generator when run in the initrd has been to generate the units that wait for /dev/gpt-auto-root to show up and mount them only if we have the loader partition EFI variables set. This is of course not the case for network boots with a UKI kernel, which means auto-gpt would not work for mounting the rootfs. What's nasty is that we don't know for sure whether the "rootdisk" loopback device will shown up eventually, as it needs explicit configuration by the user via the kernel cmdline, or could be configured entirely indepdenently. Hence, let's tweak the logic when we wat for /dev/gpt-auto-root as device to mount: make the gpt auto root logic a tristate: if root=gpt-auto is specified on the cmdline *definitely* enable the logic. If root= is specified and set to anyting else, *definitely* disable the logic. And if root= is not specified check for the EFI partition vars – as before – to conditionalized things. Or in other words, you can now boot the same image either via ESP/local boot or via netboot with a kernelcmdline image like this: rd.systemd.pull=verify=no,machine,blockdev,bootorigin,raw:rootdisk:image.raw root=gpt-auto rootflags=x-systemd.device-timeout=infinity ip=any --- src/gpt-auto-generator/gpt-auto-generator.c | 47 ++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index dbcef3b0a9..1b59fecf9e 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -39,7 +39,7 @@ static const char *arg_dest = NULL; static bool arg_enabled = true; -static bool arg_root_enabled = true; +static int arg_root_enabled = -1; /* tristate */ static bool arg_swap_enabled = true; static char *arg_root_fstype = NULL; static char *arg_root_options = NULL; @@ -673,21 +673,29 @@ static int add_root_mount(void) { _cleanup_free_ char *options = NULL; int r; - if (!is_efi_boot()) { - log_debug("Not an EFI boot, not creating root mount."); + /* Explicitly disabled? Then exit immediately */ + if (arg_root_enabled == 0) return 0; + + /* Neither explicitly enabled nor disabled? Then decide based on the EFI partition variables to be set */ + if (arg_root_enabled < 0) { + if (!is_efi_boot()) { + log_debug("Not an EFI boot, not creating root mount."); + return 0; + } + + r = efi_loader_get_device_part_uuid(/* ret_uuid= */ NULL); + if (r == -ENOENT) { + log_notice("EFI loader partition unknown, exiting.\n" + "(The boot loader did not set EFI variable LoaderDevicePartUUID.)"); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to read loader partition UUID: %m"); } - r = efi_loader_get_device_part_uuid(NULL); - if (r == -ENOENT) { - log_notice("EFI loader partition unknown, exiting.\n" - "(The boot loader did not set EFI variable LoaderDevicePartUUID.)"); - return 0; - } else if (r < 0) - return log_error_errno(r, "Failed to read loader partition UUID: %m"); - - /* OK, we have an ESP/XBOOTLDR partition, this is fantastic, so let's wait for a root device to show up. - * A udev rule will create the link for us under the right name. */ + /* OK, we shall look for a root device, so let's wait for a root device to show up. A udev rule will + * create the link for us under the right name. */ if (in_initrd()) { r = generator_write_initrd_root_device_deps(arg_dest, "/dev/gpt-auto-root"); @@ -904,9 +912,12 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat /* Disable root disk logic if there's a root= value * specified (unless it happens to be "gpt-auto") */ - if (!streq(value, "gpt-auto")) { + if (streq(value, "gpt-auto")) { + arg_root_enabled = true; + log_debug("Enabling root partition auto-detection, root= is explicitly set to 'gpt_auto'."); + } else { arg_root_enabled = false; - log_debug("Disabling root partition auto-detection, root= is defined."); + log_debug("Disabling root partition auto-detection, root= is neither unset, nor set to 'gpt-auto'."); } } else if (streq(key, "roothash")) { @@ -917,6 +928,7 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat /* Disable root disk logic if there's roothash= defined (i.e. verity enabled) */ arg_root_enabled = false; + log_debug("Disabling root partition auto-detection, roothash= is set."); } else if (streq(key, "rootfstype")) { @@ -975,10 +987,7 @@ static int run(const char *dest, const char *dest_early, const char *dest_late) return 0; } - if (arg_root_enabled) - r = add_root_mount(); - else - r = 0; + r = add_root_mount(); if (!in_initrd()) RET_GATHER(r, add_mounts()); From e577d7d94a738282b4dc7562871198264e3927ce Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 19:22:09 +0100 Subject: [PATCH 17/30] efi: modernize reconnect_all_drivers() a bit --- src/boot/drivers.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/boot/drivers.c b/src/boot/drivers.c index 078c6cb993..414e27a0f4 100644 --- a/src/boot/drivers.c +++ b/src/boot/drivers.c @@ -55,15 +55,23 @@ EFI_STATUS reconnect_all_drivers(void) { /* Reconnects all handles, so that any loaded drivers can take effect. */ - err = BS->LocateHandleBuffer(AllHandles, NULL, NULL, &n_handles, &handles); - if (err != EFI_SUCCESS) + err = BS->LocateHandleBuffer( + AllHandles, + /* Protocol= */ NULL, + /* SearchKey= */ NULL, + &n_handles, &handles); + if (!IN_SET(err, EFI_SUCCESS, EFI_NOT_FOUND)) return log_error_status(err, "Failed to get list of handles: %m"); - for (size_t i = 0; i < n_handles; i++) + FOREACH_ARRAY(h, handles, n_handles) /* Some firmware gives us some bogus handles (or they might become bad due to * reconnecting everything). Security policy may also prevent us from doing so too. * There is nothing we can realistically do on errors anyways, so just ignore them. */ - (void) BS->ConnectController(handles[i], NULL, NULL, true); + (void) BS->ConnectController( + *h, + /* DriverImageHandle= */ NULL, + /* RemainingDevicePath= */ NULL, + /* Recursive= */ true); return EFI_SUCCESS; } From d9d3e9d6c14e1ffc2d5fb03c66a24a3e20f9bc1d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 19:22:28 +0100 Subject: [PATCH 18/30] efi-string: add new xstr16_to_ascii() helper --- src/boot/efi-string.c | 32 +++++++++++++++++++--- src/boot/efi-string.h | 7 ++++- src/boot/test-efi-string.c | 54 +++++++++++++++++++++++++++++++++----- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/boot/efi-string.c b/src/boot/efi-string.c index aee98a81e1..488798b393 100644 --- a/src/boot/efi-string.c +++ b/src/boot/efi-string.c @@ -188,8 +188,10 @@ static unsigned utf8_to_unichar(const char *utf8, size_t n, char32_t *c) { /* Convert UTF-8 to UCS-2, skipping any invalid or short byte sequences. */ char16_t *xstrn8_to_16(const char *str8, size_t n) { - if (!str8 || n == 0) - return NULL; + assert(str8 || n == 0); + + if (n == SIZE_MAX) + n = strlen8(str8); size_t i = 0; char16_t *str16 = xnew(char16_t, n + 1); @@ -209,10 +211,34 @@ char16_t *xstrn8_to_16(const char *str8, size_t n) { } } - str16[i] = '\0'; + str16[i] = u'\0'; return str16; } +char *xstrn16_to_ascii(const char16_t *str16, size_t n) { + assert(str16 || n == 0); + + if (n == SIZE_MAX) + n = strlen16(str16); + + _cleanup_free_ char *str8 = xnew(char, n + 1); + + size_t i = 0; + while (n > 0 && *str16 != u'\0') { + + if ((uint16_t) *str16 > 127U) /* Not ASCII? Fail! */ + return NULL; + + str8[i++] = (char) (uint16_t) *str16; + + str16++; + n--; + } + + str8[i] = '\0'; + return TAKE_PTR(str8); +} + char* startswith8(const char *s, const char *prefix) { size_t l; diff --git a/src/boot/efi-string.h b/src/boot/efi-string.h index 9a0f89d904..5c6326b495 100644 --- a/src/boot/efi-string.h +++ b/src/boot/efi-string.h @@ -98,7 +98,12 @@ static inline char16_t *xstrdup16(const char16_t *s) { char16_t *xstrn8_to_16(const char *str8, size_t n); static inline char16_t *xstr8_to_16(const char *str8) { - return xstrn8_to_16(str8, strlen8(str8)); + return xstrn8_to_16(str8, SIZE_MAX); +} + +char *xstrn16_to_ascii(const char16_t *str16, size_t n); +static inline char *xstr16_to_ascii(const char16_t *str16) { + return xstrn16_to_ascii(str16, SIZE_MAX); } char* startswith8(const char *s, const char *prefix); diff --git a/src/boot/test-efi-string.c b/src/boot/test-efi-string.c index b71a0c3402..98c92690a7 100644 --- a/src/boot/test-efi-string.c +++ b/src/boot/test-efi-string.c @@ -332,30 +332,70 @@ TEST(xstrdup16) { TEST(xstrn8_to_16) { char16_t *s = NULL; - assert_se(xstrn8_to_16(NULL, 1) == NULL); - assert_se(xstrn8_to_16("a", 0) == NULL); + assert_se(s = xstrn8_to_16(NULL, 0)); + ASSERT_TRUE(streq16(s, u"")); + free(s); + + assert_se(s = xstrn8_to_16("", 0)); + ASSERT_TRUE(streq16(s, u"")); + free(s); + + assert_se(s = xstrn8_to_16("a", 0)); + ASSERT_TRUE(streq16(s, u"")); + free(s); assert_se(s = xstrn8_to_16("", 1)); - assert_se(streq16(s, u"")); + ASSERT_TRUE(streq16(s, u"")); free(s); assert_se(s = xstrn8_to_16("1", 1)); - assert_se(streq16(s, u"1")); + ASSERT_TRUE(streq16(s, u"1")); free(s); assert_se(s = xstr8_to_16("abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~")); - assert_se(streq16(s, u"abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~")); + ASSERT_TRUE(streq16(s, u"abcxyzABCXYZ09 .,-_#*!\"§$%&/()=?`~")); free(s); assert_se(s = xstr8_to_16("ÿⱿ𝇉 😺")); - assert_se(streq16(s, u"ÿⱿ ")); + ASSERT_TRUE(streq16(s, u"ÿⱿ ")); free(s); assert_se(s = xstrn8_to_16("¶¶", 3)); - assert_se(streq16(s, u"¶")); + ASSERT_TRUE(streq16(s, u"¶")); free(s); } +TEST(xstrn16_to_ascii) { + char *s; + + assert_se(s = xstrn16_to_ascii(NULL, 0)); + ASSERT_TRUE(streq8(s, "")); + free(s); + + assert_se(s = xstrn16_to_ascii(u"", 0)); + ASSERT_TRUE(streq8(s, "")); + free(s); + + assert_se(s = xstrn16_to_ascii(u"a", 0)); + ASSERT_TRUE(streq8(s, "")); + free(s); + + assert_se(s = xstrn16_to_ascii(u"", 1)); + ASSERT_TRUE(streq8(s, "")); + free(s); + + assert_se(s = xstrn16_to_ascii(u"1", 1)); + ASSERT_TRUE(streq8(s, "1")); + free(s); + + assert_se(s = xstr16_to_ascii(u"abcxyzABCXYZ09 .,-_#*!\"$%&/()=?`~")); + ASSERT_TRUE(streq8(s, "abcxyzABCXYZ09 .,-_#*!\"$%&/()=?`~")); + free(s); + + assert_se(!xstr16_to_ascii(u"ÿⱿ𝇉 😺")); + assert_se(!xstr16_to_ascii(u"¶¶")); +} + TEST(startswith8) { assert_se(streq8(startswith8("", ""), "")); assert_se(streq8(startswith8("x", ""), "x")); From f6841680502d06f4d1e375144998955f245dfc23 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 23:47:00 +0100 Subject: [PATCH 19/30] efi: add strcspn16()/strspn16() to efi libs too --- src/boot/efi-string.c | 24 +++++++++++++++++++++++ src/boot/efi-string.h | 3 +++ src/boot/test-efi-string.c | 23 ++++++++++++++++++++++ src/fundamental/string-util-fundamental.h | 2 ++ 4 files changed, 52 insertions(+) diff --git a/src/boot/efi-string.c b/src/boot/efi-string.c index 488798b393..1f669a2749 100644 --- a/src/boot/efi-string.c +++ b/src/boot/efi-string.c @@ -1109,3 +1109,27 @@ void *memset(void *p, int c, size_t n) { return p; } + +size_t strspn16(const char16_t *p, const char16_t *good) { + assert(p); + assert(good); + + const char16_t *i = p; + for (; *i != 0; i++) + if (!strchr16(good, *i)) + break; + + return i - p; +} + +size_t strcspn16(const char16_t *p, const char16_t *bad) { + assert(p); + assert(bad); + + const char16_t *i = p; + for (; *i != 0; i++) + if (strchr16(bad, *i)) + break; + + return i - p; +} diff --git a/src/boot/efi-string.h b/src/boot/efi-string.h index 5c6326b495..fa1f63324a 100644 --- a/src/boot/efi-string.h +++ b/src/boot/efi-string.h @@ -108,6 +108,9 @@ static inline char *xstr16_to_ascii(const char16_t *str16) { char* startswith8(const char *s, const char *prefix); +size_t strspn16(const char16_t *p, const char16_t *good); +size_t strcspn16(const char16_t *p, const char16_t *bad); + bool efi_fnmatch(const char16_t *pattern, const char16_t *haystack); bool parse_number8(const char *s, uint64_t *ret_u, const char **ret_tail); diff --git a/src/boot/test-efi-string.c b/src/boot/test-efi-string.c index 98c92690a7..3278ac2c30 100644 --- a/src/boot/test-efi-string.c +++ b/src/boot/test-efi-string.c @@ -831,4 +831,27 @@ TEST(efi_memset) { assert_se(memcmp(buf, "aaaaaaaaaa", 10) == 0); } +TEST(efi_strspn) { + ASSERT_EQ(strspn16(u"foobar", u"fo"), 3U); + ASSERT_EQ(strspn16(u"foobar", u"ob"), 0U); + ASSERT_EQ(strspn16(u"foobar", u"fxayzy"), 1U); + ASSERT_EQ(strspn16(u"", u"abcdefef"), 0U); + ASSERT_EQ(strspn16(u"", u""), 0U); + ASSERT_EQ(strspn16(u"foobar", u""), 0U); + ASSERT_EQ(strspn16(u"foffffffffffoobar", u"fofo"), 14U); +} + +TEST(efi_strcspn) { + ASSERT_EQ(strcspn16(u"foobar", u"fo"), 0U); + ASSERT_EQ(strcspn16(u"foobar", u"bar"), 3U); + ASSERT_EQ(strcspn16(u"foobar", u"xhyfzy"), 0U); + ASSERT_EQ(strcspn16(u"foobar", u"xhyozy"), 1U); + ASSERT_EQ(strcspn16(u"foobar", u"xhyzy"), 6U); + ASSERT_EQ(strcspn16(u"", u"abcdefef"), 0U); + ASSERT_EQ(strcspn16(u"", u""), 0U); + ASSERT_EQ(strcspn16(u"foobar", u""), 6U); + ASSERT_EQ(strcspn16(u"foffffffffffoobar", u"fofo"), 0U); + ASSERT_EQ(strcspn16(u"foffffffffffoobar", u"a"), 15U); +} + DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/fundamental/string-util-fundamental.h b/src/fundamental/string-util-fundamental.h index 419f1cc3da..ec3ad48706 100644 --- a/src/fundamental/string-util-fundamental.h +++ b/src/fundamental/string-util-fundamental.h @@ -16,6 +16,8 @@ # define strncmp strncmp16 # define strcasecmp strcasecmp16 # define strncasecmp strncasecmp16 +# define strspn strspn16 +# define strcspn strcspn16 # define STR_C(str) (L ## str) typedef char16_t sd_char; #else From a6fbfd8db6c23233088b35abb97ba83b835a473c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 07:32:47 +0100 Subject: [PATCH 20/30] boot: bls type #1 with 'efi' stanza are bls type #1 too --- src/boot/boot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index 7c91e5d389..c2fb318eae 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -47,8 +47,8 @@ DECLARE_SBAT(SBAT_BOOT_SECTION_TEXT); typedef enum LoaderType { LOADER_UNDEFINED, LOADER_AUTO, - LOADER_EFI, - LOADER_LINUX, /* Boot loader spec type #1 entries */ + LOADER_EFI, /* Boot loader spec type #1 entries with "efi" line */ + LOADER_LINUX, /* Boot loader spec type #1 entries with "linux" line */ LOADER_UNIFIED_LINUX, /* Boot loader spec type #2 entries */ LOADER_SECURE_BOOT_KEYS, _LOADER_TYPE_MAX, From 149609cfcba9ffdc4f83f5057b979dc4e582236a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 07:33:34 +0100 Subject: [PATCH 21/30] boot: be stricter when filtering out invalid bls #1 entries --- src/boot/boot.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index c2fb318eae..15b05b77bd 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -51,6 +51,8 @@ typedef enum LoaderType { LOADER_LINUX, /* Boot loader spec type #1 entries with "linux" line */ LOADER_UNIFIED_LINUX, /* Boot loader spec type #2 entries */ LOADER_SECURE_BOOT_KEYS, + LOADER_BAD, /* Marker: this boot loader spec type #1 entry is invalid */ + LOADER_IGNORE, /* Marker: this boot loader spec type #1 entry does not match local host */ _LOADER_TYPE_MAX, } LoaderType; @@ -1473,26 +1475,38 @@ static void boot_entry_add_type1( entry->machine_id = xstr8_to_16(value); } else if (streq8(key, "linux")) { + + if (!IN_SET(entry->type, LOADER_UNDEFINED, LOADER_LINUX)) { + entry->type = LOADER_BAD; + break; + } + free(entry->loader); entry->type = LOADER_LINUX; entry->loader = xstr8_to_path(value); entry->key = 'l'; } else if (streq8(key, "efi")) { + + if (!IN_SET(entry->type, LOADER_UNDEFINED, LOADER_EFI)) { + entry->type = LOADER_BAD; + break; + } + entry->type = LOADER_EFI; free(entry->loader); entry->loader = xstr8_to_path(value); /* do not add an entry for ourselves */ if (strcaseeq16(entry->loader, loaded_image_path)) { - entry->type = LOADER_UNDEFINED; + entry->type = LOADER_IGNORE; break; } } else if (streq8(key, "architecture")) { /* do not add an entry for an EFI image of architecture not matching with that of the image */ if (!strcaseeq8(value, EFI_MACHINE_TYPE_NAME)) { - entry->type = LOADER_UNDEFINED; + entry->type = LOADER_IGNORE; break; } @@ -1520,7 +1534,8 @@ static void boot_entry_add_type1( entry->options = TAKE_PTR(new); } - if (entry->type == LOADER_UNDEFINED) + /* Filter all entries that are badly defined or don't apply to the local system. */ + if (IN_SET(entry->type, LOADER_UNDEFINED, LOADER_BAD, LOADER_IGNORE)) return; /* check existence */ From 06648d41877ad670a5eed98e906d2b3a4076f6f5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 07:35:02 +0100 Subject: [PATCH 22/30] boot: move behaviour checks into per-entry-type helpers --- src/boot/boot.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index 15b05b77bd..8a1224a45b 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -56,6 +56,15 @@ typedef enum LoaderType { _LOADER_TYPE_MAX, } LoaderType; +/* Which loader types permit command line editing */ +#define LOADER_TYPE_ALLOW_EDITOR(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX) + +/* Which loader types allow command line editing in SecureBoot mode */ +#define LOADER_TYPE_ALLOW_EDITOR_IN_SB(t) IN_SET(t, LOADER_EFI, LOADER_LINUX) + +/* Which loader types shall be considered for automatic selection */ +#define LOADER_TYPE_MAY_AUTO_SELECT(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX) + typedef struct { char16_t *id; /* The unique identifier for this entry (typically the filename of the file defining the entry, possibly suffixed with a profile id) */ char16_t *id_without_profile; /* same, but without any profile id suffixed */ @@ -996,7 +1005,7 @@ static bool menu_run( case KEYPRESS(0, 0, 'E'): /* only the options of configured entries can be edited */ if (!config->editor || - !IN_SET(config->entries[idx_highlight]->type, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX)) { + !LOADER_TYPE_ALLOW_EDITOR(config->entries[idx_highlight]->type)) { status = xstrdup16(u"Entry does not support editing the command line."); break; } @@ -1004,7 +1013,7 @@ static bool menu_run( /* Unified kernels that are signed as a whole will not accept command line options * when secure boot is enabled unless there is none embedded in the image. Do not try * to pretend we can edit it to only have it be ignored. */ - if (config->entries[idx_highlight]->type == LOADER_UNIFIED_LINUX && + if (!LOADER_TYPE_ALLOW_EDITOR_IN_SB(config->entries[idx_highlight]->type) && secure_boot_enabled() && config->entries[idx_highlight]->options) { status = xstrdup16(u"Entry not editable in SecureBoot mode."); @@ -1821,7 +1830,7 @@ static void config_select_default_entry(Config *config) { /* select the first suitable entry */ for (i = 0; i < config->n_entries; i++) - if (config->entries[i]->type != LOADER_AUTO && !config->entries[i]->call) { + if (LOADER_TYPE_MAY_AUTO_SELECT(config->entries[i]->type)) { config->idx_default = i; return; } From e2a3d562189c413de3262ec47cdc1e1b0b13d78b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 09:18:14 +0100 Subject: [PATCH 23/30] boot: add new bls type #1 stanza "uki" This one is between "efi" and "linux": we'll recognize such entries as linux, but we'll just invoke them as EFI binaries. This creates a high-level concept for invoking UKIs via indirection of a bls type #1 entry, for example to permit invocation from a non-standard path or for giving entries a different name. Companion BLS spec PR: https://github.com/uapi-group/specifications/pull/135 (Let's rename LOADER_UNIFIED_LINUX to LOADER_TYPE2_UKI at the same time to reduce confusion what is what) --- src/boot/boot.c | 22 ++++++++++++++++++---- src/bootctl/bootctl-status.c | 1 + src/fundamental/efivars-fundamental.h | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index 8a1224a45b..e57c598654 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -49,7 +49,8 @@ typedef enum LoaderType { LOADER_AUTO, LOADER_EFI, /* Boot loader spec type #1 entries with "efi" line */ LOADER_LINUX, /* Boot loader spec type #1 entries with "linux" line */ - LOADER_UNIFIED_LINUX, /* Boot loader spec type #2 entries */ + LOADER_UKI, /* Boot loader spec type #1 entries with "uki" line */ + LOADER_TYPE2_UKI, /* Boot loader spec type #2 entries */ LOADER_SECURE_BOOT_KEYS, LOADER_BAD, /* Marker: this boot loader spec type #1 entry is invalid */ LOADER_IGNORE, /* Marker: this boot loader spec type #1 entry does not match local host */ @@ -57,13 +58,13 @@ typedef enum LoaderType { } LoaderType; /* Which loader types permit command line editing */ -#define LOADER_TYPE_ALLOW_EDITOR(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX) +#define LOADER_TYPE_ALLOW_EDITOR(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_TYPE2_UKI) /* Which loader types allow command line editing in SecureBoot mode */ #define LOADER_TYPE_ALLOW_EDITOR_IN_SB(t) IN_SET(t, LOADER_EFI, LOADER_LINUX) /* Which loader types shall be considered for automatic selection */ -#define LOADER_TYPE_MAY_AUTO_SELECT(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UNIFIED_LINUX) +#define LOADER_TYPE_MAY_AUTO_SELECT(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_TYPE2_UKI) typedef struct { char16_t *id; /* The unique identifier for this entry (typically the filename of the file defining the entry, possibly suffixed with a profile id) */ @@ -1495,6 +1496,18 @@ static void boot_entry_add_type1( entry->loader = xstr8_to_path(value); entry->key = 'l'; + } else if (streq8(key, "uki")) { + + if (!IN_SET(entry->type, LOADER_UNDEFINED, LOADER_UKI)) { + entry->type = LOADER_BAD; + break; + } + + free(entry->loader); + entry->type = LOADER_UKI; + entry->loader = xstr8_to_path(value); + entry->key = 'l'; + } else if (streq8(key, "efi")) { if (!IN_SET(entry->type, LOADER_UNDEFINED, LOADER_EFI)) { @@ -2347,7 +2360,7 @@ static void boot_entry_add_type2( *entry = (BootEntry) { .id = strtolower16(TAKE_PTR(id)), .id_without_profile = profile > 0 ? strtolower16(xstrdup16(filename)) : NULL, - .type = LOADER_UNIFIED_LINUX, + .type = LOADER_TYPE2_UKI, .title = TAKE_PTR(title), .version = xstrdup16(good_version), .device = device, @@ -2789,6 +2802,7 @@ static void export_loader_variables( EFI_LOADER_FEATURE_MENU_DISABLE | EFI_LOADER_FEATURE_MULTI_PROFILE_UKI | EFI_LOADER_FEATURE_REPORT_URL | + EFI_LOADER_FEATURE_TYPE1_UKI | 0; assert(loaded_image); diff --git a/src/bootctl/bootctl-status.c b/src/bootctl/bootctl-status.c index 03ebea38df..541f7f25cc 100644 --- a/src/bootctl/bootctl-status.c +++ b/src/bootctl/bootctl-status.c @@ -391,6 +391,7 @@ int verb_status(int argc, char *argv[], void *userdata) { { EFI_LOADER_FEATURE_MENU_DISABLE, "Menu can be disabled" }, { EFI_LOADER_FEATURE_MULTI_PROFILE_UKI, "Multi-Profile UKIs are supported" }, { EFI_LOADER_FEATURE_REPORT_URL, "Loader reports network boot URL" }, + { EFI_LOADER_FEATURE_TYPE1_UKI, "Support Type #1 uki field" }, }; static const struct { uint64_t flag; diff --git a/src/fundamental/efivars-fundamental.h b/src/fundamental/efivars-fundamental.h index 678cdeeff8..b7197a874e 100644 --- a/src/fundamental/efivars-fundamental.h +++ b/src/fundamental/efivars-fundamental.h @@ -25,6 +25,7 @@ #define EFI_LOADER_FEATURE_MENU_DISABLE (UINT64_C(1) << 13) #define EFI_LOADER_FEATURE_MULTI_PROFILE_UKI (UINT64_C(1) << 14) #define EFI_LOADER_FEATURE_REPORT_URL (UINT64_C(1) << 15) +#define EFI_LOADER_FEATURE_TYPE1_UKI (UINT64_C(1) << 16) /* Features of the stub, i.e. systemd-stub */ #define EFI_STUB_FEATURE_REPORT_BOOT_PARTITION (UINT64_C(1) << 0) From 1089d0f89e3c7c8a2b17ebeba32d567061fb3ba1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Feb 2025 09:34:20 +0100 Subject: [PATCH 24/30] boot: add new 'uki-url' bls type #1 menu items for booting remote UKIs Companion BLS spec PR: https://github.com/uapi-group/specifications/pull/135 --- src/boot/boot.c | 230 +++++++++++++++++++---- src/boot/device-path-util.c | 44 ++++- src/boot/device-path-util.h | 8 + src/boot/shim.c | 15 +- src/boot/shim.h | 2 +- src/boot/stub.c | 2 +- src/boot/util.c | 40 +++- src/boot/util.h | 2 + src/bootctl/bootctl-status.c | 1 + src/fundamental/efivars-fundamental.h | 1 + src/fundamental/iovec-util-fundamental.h | 2 + src/fundamental/macro-fundamental.h | 4 + 12 files changed, 307 insertions(+), 44 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index e57c598654..980fcff7c3 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -18,6 +18,7 @@ #include "pe.h" #include "proto/block-io.h" #include "proto/device-path.h" +#include "proto/load-file.h" #include "proto/simple-text-io.h" #include "random-seed.h" #include "sbat.h" @@ -27,6 +28,7 @@ #include "ticks.h" #include "tpm2-pcr.h" #include "uki.h" +#include "url-discovery.h" #include "util.h" #include "version.h" #include "vmm.h" @@ -50,6 +52,7 @@ typedef enum LoaderType { LOADER_EFI, /* Boot loader spec type #1 entries with "efi" line */ LOADER_LINUX, /* Boot loader spec type #1 entries with "linux" line */ LOADER_UKI, /* Boot loader spec type #1 entries with "uki" line */ + LOADER_UKI_URL, /* Boot loader spec type #1 entries with "uki-url" line */ LOADER_TYPE2_UKI, /* Boot loader spec type #2 entries */ LOADER_SECURE_BOOT_KEYS, LOADER_BAD, /* Marker: this boot loader spec type #1 entry is invalid */ @@ -58,13 +61,13 @@ typedef enum LoaderType { } LoaderType; /* Which loader types permit command line editing */ -#define LOADER_TYPE_ALLOW_EDITOR(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_TYPE2_UKI) +#define LOADER_TYPE_ALLOW_EDITOR(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_UKI_URL, LOADER_TYPE2_UKI) /* Which loader types allow command line editing in SecureBoot mode */ #define LOADER_TYPE_ALLOW_EDITOR_IN_SB(t) IN_SET(t, LOADER_EFI, LOADER_LINUX) /* Which loader types shall be considered for automatic selection */ -#define LOADER_TYPE_MAY_AUTO_SELECT(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_TYPE2_UKI) +#define LOADER_TYPE_MAY_AUTO_SELECT(t) IN_SET(t, LOADER_EFI, LOADER_LINUX, LOADER_UKI, LOADER_UKI_URL, LOADER_TYPE2_UKI) typedef struct { char16_t *id; /* The unique identifier for this entry (typically the filename of the file defining the entry, possibly suffixed with a profile id) */ @@ -77,6 +80,7 @@ typedef struct { EFI_HANDLE *device; LoaderType type; char16_t *loader; + char16_t *url; char16_t *devicetree; char16_t *options; bool options_implied; /* If true, these options are implied if we invoke the PE binary without any parameters (as in: UKI). If false we must specify these options explicitly. */ @@ -620,6 +624,8 @@ static void print_status(Config *config, char16_t *loaded_image_path) { printf(" device: %ls\n", dp_str); if (entry->loader) printf(" loader: %ls\n", entry->loader); + if (entry->url) + printf(" url: %ls\n", entry->url); STRV_FOREACH(initrd, entry->initrd) printf(" initrd: %ls\n", *initrd); if (entry->devicetree) @@ -1211,6 +1217,7 @@ static BootEntry* boot_entry_free(BootEntry *entry) { free(entry->version); free(entry->machine_id); free(entry->loader); + free(entry->url); free(entry->devicetree); free(entry->options); strv_free(entry->initrd); @@ -1508,6 +1515,32 @@ static void boot_entry_add_type1( entry->loader = xstr8_to_path(value); entry->key = 'l'; + } else if (streq8(key, "uki-url")) { + + if (!IN_SET(entry->type, LOADER_UNDEFINED, LOADER_UKI_URL)) { + entry->type = LOADER_BAD; + break; + } + + _cleanup_free_ char16_t *p = xstr8_to_16(value); + + const char16_t *e = startswith(p, u":"); + if (e) { + _cleanup_free_ char16_t *origin = disk_get_url(device); + + if (!origin) { + /* Automatically hide entries that require an original URL but where none is available. */ + entry->type = LOADER_IGNORE; + break; + } + + entry->url = url_replace_last_component(origin, p); + } else + entry->url = TAKE_PTR(p); + + entry->type = LOADER_UKI_URL; + entry->key = 'l'; + } else if (streq8(key, "efi")) { if (!IN_SET(entry->type, LOADER_UNDEFINED, LOADER_EFI)) { @@ -1560,11 +1593,13 @@ static void boot_entry_add_type1( if (IN_SET(entry->type, LOADER_UNDEFINED, LOADER_BAD, LOADER_IGNORE)) return; - /* check existence */ - _cleanup_file_close_ EFI_FILE *handle = NULL; - err = root_dir->Open(root_dir, &handle, entry->loader, EFI_FILE_MODE_READ, 0ULL); - if (err != EFI_SUCCESS) - return; + /* Check existence of loader file */ + if (entry->loader) { + _cleanup_file_close_ EFI_FILE *handle = NULL; + err = root_dir->Open(root_dir, &handle, entry->loader, EFI_FILE_MODE_READ, 0ULL); + if (err != EFI_SUCCESS) + return; + } entry->device = device; entry->id = xstrdup16(file); @@ -2560,13 +2595,77 @@ static EFI_STATUS initrd_prepare( return EFI_SUCCESS; } +static EFI_STATUS expand_path( + EFI_HANDLE parent_image, + EFI_DEVICE_PATH *path, + EFI_DEVICE_PATH **ret_expanded_path) { + + EFI_STATUS err; + + assert(parent_image); + assert(path); + assert(ret_expanded_path); + + _cleanup_free_ EFI_HANDLE *handles = NULL; + size_t n_handles = 0; + err = BS->LocateHandleBuffer( + ByProtocol, + MAKE_GUID_PTR(EFI_LOAD_FILE_PROTOCOL), + /* SearchKey= */ NULL, + &n_handles, + &handles); + if (!IN_SET(err, EFI_SUCCESS, EFI_NOT_FOUND)) + return log_error_status(err, "Failed to get list of LoadFile protocol handles: %m"); + + FOREACH_ARRAY(h, handles, n_handles) { + EFI_LOAD_FILE_PROTOCOL *load_file = NULL; + err = BS->OpenProtocol( + *h, + MAKE_GUID_PTR(EFI_LOAD_FILE_PROTOCOL), + (void**) &load_file, + parent_image, + /* ControllerHandler= */ NULL, + EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (IN_SET(err, EFI_NOT_FOUND, EFI_INVALID_PARAMETER)) + continue; /* Skip over LoadFile() handles that are not suitable for this kind of device path */ + if (err != EFI_SUCCESS) { + log_warning_status(err, "Failed to get LoadFile() protocol, ignoring: %m"); + continue; + } + + /* Issue a LoadFile() request without interest in the actual data (i.e. size is zero and + * buffer pointer is NULL), but with BootPolicy set to true, this has the effect of + * downloading the URL and establishing a handle for it. */ + size_t size = 0; + err = load_file->LoadFile(load_file, path, /* BootPolicy= */ true, &size, /* Buffer= */ NULL); + if (IN_SET(err, EFI_NOT_FOUND, EFI_INVALID_PARAMETER)) + continue; /* Skip over LoadFile() handles that after all don't consider themselves + * appropriate for this kind of path */ + if (err != EFI_BUFFER_TOO_SMALL) { + log_warning_status(err, "Failed to get file via LoadFile() protocol, ignoring: %m"); + continue; + } + + /* Now read the updated file path */ + EFI_DEVICE_PATH *load_file_path = NULL; + err = BS->HandleProtocol(*h, MAKE_GUID_PTR(EFI_DEVICE_PATH_PROTOCOL), (void **) &load_file_path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to get LoadFile() device path: %m"); + + /* And return a copy */ + *ret_expanded_path = device_path_dup(load_file_path); + return EFI_SUCCESS; + } + + return EFI_NOT_FOUND; +} + static EFI_STATUS image_start( EFI_HANDLE parent_image, const BootEntry *entry) { _cleanup_(devicetree_cleanup) struct devicetree_state dtstate = {}; _cleanup_(unload_imagep) EFI_HANDLE image = NULL; - _cleanup_free_ EFI_DEVICE_PATH *path = NULL; EFI_STATUS err; assert(entry); @@ -2576,38 +2675,94 @@ static EFI_STATUS image_start( (void) entry->call(); _cleanup_file_close_ EFI_FILE *image_root = NULL; - err = open_volume(entry->device, &image_root); - if (err != EFI_SUCCESS) - return log_error_status(err, "Error opening root path: %m"); - - err = make_file_device_path(entry->device, entry->loader, &path); - if (err != EFI_SUCCESS) - return log_error_status(err, "Error making file device path: %m"); - - size_t initrd_size = 0; - _cleanup_pages_ Pages initrd_pages = {}; - _cleanup_free_ char16_t *options_initrd = NULL; - err = initrd_prepare(image_root, entry, &options_initrd, &initrd_pages, &initrd_size); - if (err != EFI_SUCCESS) - return log_error_status(err, "Error preparing initrd: %m"); - - err = shim_load_image(parent_image, path, &image); - if (err != EFI_SUCCESS) - return log_error_status(err, "Error loading %ls: %m", entry->loader); - - /* DTBs are loaded by the kernel before ExitBootServices, and they can be used to map and assign - * arbitrary memory ranges, so skip them when secure boot is enabled as the DTB here is unverified. - */ - if (entry->devicetree && !secure_boot_enabled()) { - err = devicetree_install(&dtstate, image_root, entry->devicetree); + _cleanup_free_ EFI_DEVICE_PATH *path = NULL; + bool boot_policy; + if (entry->url) { + /* Generate a device path that only contains the URL */ + err = make_url_device_path(entry->url, &path); if (err != EFI_SUCCESS) - return log_error_status(err, "Error loading %ls: %m", entry->devicetree); + return log_error_status(err, "Error making URL device path: %m"); + + /* Try to expand this path on all available NICs and IP protocols */ + _cleanup_free_ EFI_DEVICE_PATH *expanded_path = NULL; + for (unsigned n_attempt = 0;; n_attempt++) { + err = expand_path(parent_image, path, &expanded_path); + if (err == EFI_SUCCESS) { + /* If this worked then let's try to boot with the expanded path. */ + free(path); + path = TAKE_PTR(expanded_path); + break; + } + if (err != EFI_NOT_FOUND || n_attempt > 5) { + log_warning_status(err, "Failed to expand device path, ignoring: %m"); + break; + } + + /* Maybe the network devices have been configured for this yet (because we are the + * first piece of code trying to do networking)? Then let's connect them, and try + * again. */ + reconnect_all_drivers(); + } + + /* Note: if the path expansion doesn't work, we'll continue with the unexpanded path. Which + * will probably fail on many (most?) firmwares, but it's worth a try. */ + + boot_policy = true; /* Set BootPolicy parameter to LoadImage() to true, which ultimately + * controls whether the LoadFile (and thus HTTP boot) or LoadFile2 (which + * does not set up HTTP boot) protocol shall be used. */ + } else { + assert(entry->device); + assert(entry->loader); + + err = open_volume(entry->device, &image_root); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error opening root path: %m"); + + err = make_file_device_path(entry->device, entry->loader, &path); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error making file device path: %m"); + + boot_policy = false; + } + + /* Authenticate the image before we continue with initrd or DT stuff */ + err = shim_load_image(parent_image, path, boot_policy, &image); + if (err != EFI_SUCCESS) { + if (entry->url) { + /* EFI_NOT_FOUND typically indicates that no network stack or NIC was available, let's give the user a hint. */ + if (err == EFI_NOT_FOUND) { + log_info("Unable to boot remote UKI %ls, is networking available?", entry->url); + return err; + } + + return log_error_status(err, "Error loading loading remote UKI %ls: %m", entry->url); + } + + return log_error_status(err, "Error loading EFI binary %ls: %m", entry->loader); } _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL; - err = initrd_register(PHYSICAL_ADDRESS_TO_POINTER(initrd_pages.addr), initrd_size, &initrd_handle); - if (err != EFI_SUCCESS) - return log_error_status(err, "Error registering initrd: %m"); + _cleanup_free_ char16_t *options_initrd = NULL; + _cleanup_pages_ Pages initrd_pages = {}; + size_t initrd_size = 0; + if (image_root) { + err = initrd_prepare(image_root, entry, &options_initrd, &initrd_pages, &initrd_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error preparing initrd: %m"); + + /* DTBs are loaded by the kernel before ExitBootServices(), and they can be used to map and + * assign arbitrary memory ranges, so skip them when secure boot is enabled as the DTB here + * is unverified. */ + if (entry->devicetree && !secure_boot_enabled()) { + err = devicetree_install(&dtstate, image_root, entry->devicetree); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error loading %ls: %m", entry->devicetree); + } + + err = initrd_register(PHYSICAL_ADDRESS_TO_POINTER(initrd_pages.addr), initrd_size, &initrd_handle); + if (err != EFI_SUCCESS) + return log_error_status(err, "Error registering initrd: %m"); + } EFI_LOADED_IMAGE_PROTOCOL *loaded_image; err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); @@ -2673,7 +2828,7 @@ static EFI_STATUS image_start( err = EFI_UNSUPPORTED; } - return log_error_status(err, "Failed to execute %ls (%ls): %m", entry->title_show, entry->loader); + return log_error_status(err, "Failed to execute %ls (%ls): %m", entry->title_show, entry->loader ?: entry->url); } static void config_free(Config *config) { @@ -2803,6 +2958,7 @@ static void export_loader_variables( EFI_LOADER_FEATURE_MULTI_PROFILE_UKI | EFI_LOADER_FEATURE_REPORT_URL | EFI_LOADER_FEATURE_TYPE1_UKI | + EFI_LOADER_FEATURE_TYPE1_UKI_URL | 0; assert(loaded_image); diff --git a/src/boot/device-path-util.c b/src/boot/device-path-util.c index 2a85e8bbfc..0e991f941f 100644 --- a/src/boot/device-path-util.c +++ b/src/boot/device-path-util.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "device-path-util.h" +#include "efi-string.h" #include "util.h" EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DEVICE_PATH **ret_dp) { @@ -38,6 +39,38 @@ EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DE return EFI_SUCCESS; } +EFI_STATUS make_url_device_path(const char16_t *url, EFI_DEVICE_PATH **ret) { + assert(url); + assert(ret); + + /* Turns a URL into a simple one-element URL device path. */ + + _cleanup_free_ char* u = xstr16_to_ascii(url); + if (!u) + return EFI_INVALID_PARAMETER; + + size_t l = strlen8(u); + + size_t t = offsetof(URI_DEVICE_PATH, Uri) + l + sizeof(EFI_DEVICE_PATH); + EFI_DEVICE_PATH *dp = xmalloc(t); + + URI_DEVICE_PATH *udp = (URI_DEVICE_PATH*) dp; + udp->Header = (EFI_DEVICE_PATH) { + .Type = MESSAGING_DEVICE_PATH, + .SubType = MSG_URI_DP, + .Length = offsetof(URI_DEVICE_PATH, Uri) + l, + }; + memcpy(udp->Uri, u, l); + + EFI_DEVICE_PATH *end = device_path_next_node(dp); + *end = DEVICE_PATH_END_NODE; + + assert(((uint8_t*) end + sizeof(EFI_DEVICE_PATH)) == ((uint8_t*) dp + t)); + + *ret = TAKE_PTR(dp); + return EFI_SUCCESS; +} + static char16_t *device_path_to_str_internal(const EFI_DEVICE_PATH *dp) { char16_t *str = NULL; @@ -90,7 +123,7 @@ EFI_STATUS device_path_to_str(const EFI_DEVICE_PATH *dp, char16_t **ret) { return EFI_SUCCESS; } - str = dp_to_text->ConvertDevicePathToText(dp, false, false); + str = dp_to_text->ConvertDevicePathToText(dp, /* DisplayOnly=*/ false, /* AllowShortcuts= */ false); if (!str) return EFI_OUT_OF_RESOURCES; @@ -136,3 +169,12 @@ EFI_DEVICE_PATH *device_path_replace_node( *end = DEVICE_PATH_END_NODE; return ret; } + +size_t device_path_size(const EFI_DEVICE_PATH *dp) { + const EFI_DEVICE_PATH *i = ASSERT_PTR(dp); + + for (; !device_path_is_end(i); i = device_path_next_node(i)) + ; + + return (const uint8_t*) i - (const uint8_t*) dp + sizeof(EFI_DEVICE_PATH); +} diff --git a/src/boot/device-path-util.h b/src/boot/device-path-util.h index 08f1a9c216..c33669a866 100644 --- a/src/boot/device-path-util.h +++ b/src/boot/device-path-util.h @@ -2,8 +2,10 @@ #pragma once #include "proto/device-path.h" +#include "util.h" EFI_STATUS make_file_device_path(EFI_HANDLE device, const char16_t *file, EFI_DEVICE_PATH **ret_dp); +EFI_STATUS make_url_device_path(const char16_t *url, EFI_DEVICE_PATH **ret); EFI_STATUS device_path_to_str(const EFI_DEVICE_PATH *dp, char16_t **ret); bool device_path_startswith(const EFI_DEVICE_PATH *dp, const EFI_DEVICE_PATH *start); EFI_DEVICE_PATH *device_path_replace_node( @@ -25,3 +27,9 @@ static inline bool device_path_is_end(const EFI_DEVICE_PATH *dp) { .SubType = END_ENTIRE_DEVICE_PATH_SUBTYPE, \ .Length = sizeof(EFI_DEVICE_PATH) \ } + +size_t device_path_size(const EFI_DEVICE_PATH *dp); + +static inline EFI_DEVICE_PATH *device_path_dup(const EFI_DEVICE_PATH *dp) { + return xmemdup(ASSERT_PTR(dp), device_path_size(dp)); +} diff --git a/src/boot/shim.c b/src/boot/shim.c index 514a1d0eda..87d7dff191 100644 --- a/src/boot/shim.c +++ b/src/boot/shim.c @@ -81,7 +81,12 @@ static bool shim_validate( return shim_lock->shim_verify(file_buffer, file_size) == EFI_SUCCESS; } -EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image) { +EFI_STATUS shim_load_image( + EFI_HANDLE parent, + const EFI_DEVICE_PATH *device_path, + bool boot_policy, + EFI_HANDLE *ret_image) { + assert(device_path); assert(ret_image); @@ -91,8 +96,12 @@ EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path install_security_override(shim_validate, NULL); EFI_STATUS ret = BS->LoadImage( - /*BootPolicy=*/false, parent, (EFI_DEVICE_PATH *) device_path, NULL, 0, ret_image); - + /* BootPolicy= */ boot_policy, + parent, + (EFI_DEVICE_PATH *) device_path, + /* SourceBuffer= */ NULL, + /* SourceSize= */ 0, + ret_image); if (have_shim) uninstall_security_override(); diff --git a/src/boot/shim.h b/src/boot/shim.h index e0cb39f795..ef9dc7ba27 100644 --- a/src/boot/shim.h +++ b/src/boot/shim.h @@ -12,5 +12,5 @@ #include "efi.h" bool shim_loaded(void); -EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image); +EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, bool boot_policy, EFI_HANDLE *ret_image); void shim_retain_protocol(void); diff --git a/src/boot/stub.c b/src/boot/stub.c index 3b3a5f6a42..ac6c112459 100644 --- a/src/boot/stub.c +++ b/src/boot/stub.c @@ -598,7 +598,7 @@ static EFI_STATUS load_addons( /* By using shim_load_image, we cover both the case where the PE files are signed with MoK * and with DB, and running with or without shim. */ - err = shim_load_image(stub_image, addon_path, &addon); + err = shim_load_image(stub_image, addon_path, /* boot_policy= */ false, &addon); if (err != EFI_SUCCESS) { log_error_status(err, "Failed to read '%ls' from '%ls', ignoring: %m", diff --git a/src/boot/util.c b/src/boot/util.c index 395c1f2abc..5dd111e7c5 100644 --- a/src/boot/util.c +++ b/src/boot/util.c @@ -1,13 +1,14 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "device-path-util.h" +#include "efi-string.h" +#include "efivars.h" #include "memory-util-fundamental.h" #include "proto/device-path.h" #include "proto/simple-text-io.h" #include "ticks.h" #include "util.h" #include "version.h" -#include "efivars.h" /* Never try to read more than 16G into memory (and on 32bit 1G) */ #define FILE_READ_MAX MIN(SIZE_MAX/4, UINT64_C(16)*1024U*1024U*1024U) @@ -511,3 +512,40 @@ bool free_and_xstrdup16(char16_t **p, const char16_t *s) { *p = t; return true; } + +char16_t *url_replace_last_component(const char16_t *url, const char16_t *filename) { + assert(url); + assert(filename); + + /* Find colon separating protocol and hostname */ + const char16_t *d = strchr16(url, ':'); + if (!d || url == d) + return NULL; + d++; + + /* Skip slashes after colon */ + d += strspn(d, u"/"); + + /* Skip everything till next slash or end (i.e. the hostname) */ + size_t n = strcspn(d, u"/?#"); + if (n == 0) + return NULL; + + d += n; + + const char16_t *e = d + strcspn(d, u"?#"); /* Cut off "Query" and "Fragment" */ + + while (e > d && e[-1] == '/') /* Eat trailing slashes */ + e--; + + const char16_t *p = e; + while (p > d && p[-1] != '/') /* Find component before that */ + p--; + + if (e <= p) + return NULL; + + _cleanup_free_ char16_t *chopped = xstrndup16(url, p - url); + + return xasprintf("%ls/%ls", chopped, filename); +} diff --git a/src/boot/util.h b/src/boot/util.h index 49ba119613..2b5cc7213e 100644 --- a/src/boot/util.h +++ b/src/boot/util.h @@ -251,3 +251,5 @@ char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path); #define xnew0(type, n) ASSERT_PTR(new0(type, n)) #endif + +char16_t *url_replace_last_component(const char16_t *url, const char16_t *filename); diff --git a/src/bootctl/bootctl-status.c b/src/bootctl/bootctl-status.c index 541f7f25cc..798c86dba2 100644 --- a/src/bootctl/bootctl-status.c +++ b/src/bootctl/bootctl-status.c @@ -392,6 +392,7 @@ int verb_status(int argc, char *argv[], void *userdata) { { EFI_LOADER_FEATURE_MULTI_PROFILE_UKI, "Multi-Profile UKIs are supported" }, { EFI_LOADER_FEATURE_REPORT_URL, "Loader reports network boot URL" }, { EFI_LOADER_FEATURE_TYPE1_UKI, "Support Type #1 uki field" }, + { EFI_LOADER_FEATURE_TYPE1_UKI_URL, "Support Type #1 uki-url field" }, }; static const struct { uint64_t flag; diff --git a/src/fundamental/efivars-fundamental.h b/src/fundamental/efivars-fundamental.h index b7197a874e..d556e7718f 100644 --- a/src/fundamental/efivars-fundamental.h +++ b/src/fundamental/efivars-fundamental.h @@ -26,6 +26,7 @@ #define EFI_LOADER_FEATURE_MULTI_PROFILE_UKI (UINT64_C(1) << 14) #define EFI_LOADER_FEATURE_REPORT_URL (UINT64_C(1) << 15) #define EFI_LOADER_FEATURE_TYPE1_UKI (UINT64_C(1) << 16) +#define EFI_LOADER_FEATURE_TYPE1_UKI_URL (UINT64_C(1) << 17) /* Features of the stub, i.e. systemd-stub */ #define EFI_STUB_FEATURE_REPORT_BOOT_PARTITION (UINT64_C(1) << 0) diff --git a/src/fundamental/iovec-util-fundamental.h b/src/fundamental/iovec-util-fundamental.h index 68d5bf4ee0..edd95fa4e9 100644 --- a/src/fundamental/iovec-util-fundamental.h +++ b/src/fundamental/iovec-util-fundamental.h @@ -8,7 +8,9 @@ struct iovec { size_t iov_len; }; +DISABLE_WARNING_REDUNDANT_DECLS; static inline void free(void *p); +REENABLE_WARNING; #endif /* This accepts both const and non-const pointers */ diff --git a/src/fundamental/macro-fundamental.h b/src/fundamental/macro-fundamental.h index 78a87d150b..d56dcb6eb6 100644 --- a/src/fundamental/macro-fundamental.h +++ b/src/fundamental/macro-fundamental.h @@ -52,6 +52,10 @@ _Pragma("GCC diagnostic push"); \ _Pragma("GCC diagnostic ignored \"-Wstringop-truncation\"") +#define DISABLE_WARNING_REDUNDANT_DECLS \ + _Pragma("GCC diagnostic push"); \ + _Pragma("GCC diagnostic ignored \"-Wredundant-decls\"") + #if HAVE_WARNING_ZERO_LENGTH_BOUNDS # define DISABLE_WARNING_ZERO_LENGTH_BOUNDS \ _Pragma("GCC diagnostic push"); \ From dde4c13a7f118b877b22ae3783b4e9d1bc73b420 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Feb 2025 09:27:17 +0100 Subject: [PATCH 25/30] vmspawn: split out code that appends kernel command line into its own helper --- src/vmspawn/vmspawn.c | 78 ++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 7e1310fa4f..a5d18fd655 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -921,6 +921,46 @@ static int cmdline_add_vsock(char ***cmdline, int vsock_fd) { return 0; } +static int cmdline_add_kernel_cmdline(char ***cmdline, const char *kernel) { + assert(cmdline); + + if (strv_isempty(arg_kernel_cmdline_extra)) + return 0; + + _cleanup_free_ char *kcl = strv_join(arg_kernel_cmdline_extra, " "); + if (!kcl) + return log_oom(); + + if (kernel) { + if (strv_extend_many(cmdline, "-append", kcl) < 0) + return log_oom(); + } else { + if (!ARCHITECTURE_SUPPORTS_SMBIOS) { + log_warning("Cannot append extra args to kernel cmdline, native architecture doesn't support SMBIOS, ignoring."); + return 0; + } + + _cleanup_free_ char *escaped_kcl = NULL; + escaped_kcl = escape_qemu_value(kcl); + if (!escaped_kcl) + return log_oom(); + + if (strv_extend(cmdline, "-smbios") < 0) + return log_oom(); + + if (strv_extendf(cmdline, "type=11,value=io.systemd.stub.kernel-cmdline-extra=%s", escaped_kcl) < 0) + return log_oom(); + + if (strv_extend(cmdline, "-smbios") < 0) + return log_oom(); + + if (strv_extendf(cmdline, "type=11,value=io.systemd.boot.kernel-cmdline-extra=%s", escaped_kcl) < 0) + return log_oom(); + } + + return 0; +} + static int start_tpm( sd_bus *bus, const char *scope, @@ -1864,41 +1904,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_oom(); } - if (ARCHITECTURE_SUPPORTS_SMBIOS) { - _cleanup_free_ char *kcl = strv_join(arg_kernel_cmdline_extra, " "), *escaped_kcl = NULL; - if (!kcl) - return log_oom(); - - if (kernel) { - 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 - log_warning("Cannot append extra args to kernel cmdline, native architecture doesn't support SMBIOS"); + r = cmdline_add_kernel_cmdline(&cmdline, kernel); + if (r < 0) + return r; /* disable TPM autodetection if the user's hardware doesn't support it */ if (!ARCHITECTURE_SUPPORTS_TPM) { From 89cecfb1005656f05d837d749173950d6f22b77a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Feb 2025 09:32:18 +0100 Subject: [PATCH 26/30] vmspawn: add --smbios11= switch for passing arbitrary smbios type #11 strings to vm --- man/systemd-vmspawn.xml | 12 ++++++++++ src/vmspawn/vmspawn.c | 51 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index dfd3bbd7b4..0be886cb13 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -265,6 +265,18 @@ + + + + + + Passes the specified string into the VM as an SMBIOS Type #11 vendor string. This + is useful to parameterize the invoked VM in various ways. See + smbios-type-117 + for details. + + + diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index a5d18fd655..b38bb9ab93 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -117,6 +117,7 @@ static bool arg_pass_ssh_key = true; static char *arg_ssh_key_type = NULL; static bool arg_discard_disk = true; struct ether_addr arg_network_provided_mac = {}; +static char **arg_smbios11 = NULL; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); @@ -133,6 +134,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline_extra, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_extra_drives, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_background, freep); STATIC_DESTRUCTOR_REGISTER(arg_ssh_key_type, freep); +STATIC_DESTRUCTOR_REGISTER(arg_smbios11, strv_freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -167,6 +169,7 @@ static int help(void) { " --secure-boot=BOOL Enable searching for firmware supporting SecureBoot\n" " --firmware=PATH|list Select firmware definition file (or list available)\n" " --discard-disk=BOOL Control processing of discard requests\n" + " -s --smbios11=STRING Pass an arbitrary SMBIOS Type #11 string to the VM\n" "\n%3$sSystem Identity:%4$s\n" " -M --machine=NAME Set the machine name for the VM\n" " --uuid=UUID Set a specific machine UUID for the VM\n" @@ -295,6 +298,7 @@ static int parse_argv(int argc, char *argv[]) { { "firmware", required_argument, NULL, ARG_FIRMWARE }, { "discard-disk", required_argument, NULL, ARG_DISCARD_DISK }, { "background", required_argument, NULL, ARG_BACKGROUND }, + { "smbios11", required_argument, NULL, 's' }, {} }; @@ -304,7 +308,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argv); optind = 0; - while ((c = getopt_long(argc, argv, "+hD:i:M:nq", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "+hD:i:M:nqs:", options, NULL)) >= 0) switch (c) { case 'h': return help(); @@ -564,6 +568,20 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case 's': + if (isempty(optarg)) { + arg_smbios11 = strv_free(arg_smbios11); + break; + } + + if (!utf8_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "SMBIOS Type 11 string is not UTF-8 clean, refusing: %s", optarg); + + if (strv_extend(&arg_smbios11, optarg) < 0) + return log_oom(); + + break; + case '?': return -EINVAL; @@ -961,6 +979,33 @@ static int cmdline_add_kernel_cmdline(char ***cmdline, const char *kernel) { return 0; } +static int cmdline_add_smbios11(char ***cmdline) { + assert(cmdline); + + if (strv_isempty(arg_smbios11)) + return 0; + + if (!ARCHITECTURE_SUPPORTS_SMBIOS) { + log_warning("Cannot issue SMBIOS Type #11 strings, native architecture doesn't support SMBIOS, ignoring."); + return 0; + } + + STRV_FOREACH(i, arg_smbios11) { + _cleanup_free_ char *escaped = NULL; + escaped = escape_qemu_value(*i); + if (!escaped) + return log_oom(); + + if (strv_extend(cmdline, "-smbios") < 0) + return log_oom(); + + if (strv_extendf(cmdline, "type=11,value=%s", escaped) < 0) + return log_oom(); + } + + return 0; +} + static int start_tpm( sd_bus *bus, const char *scope, @@ -1908,6 +1953,10 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return r; + r = cmdline_add_smbios11(&cmdline); + if (r < 0) + return r; + /* disable TPM autodetection if the user's hardware doesn't support it */ if (!ARCHITECTURE_SUPPORTS_TPM) { if (arg_tpm < 0) { From fab0f6e0fc52f8f3d21b9c8c0e04a94d23e56622 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Feb 2025 09:32:49 +0100 Subject: [PATCH 27/30] vmspawn: simplify cmdline_add_vsock() a tiny bit --- src/vmspawn/vmspawn.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index b38bb9ab93..8776614001 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -926,9 +926,9 @@ static int cmdline_add_vsock(char ***cmdline, int vsock_fd) { union sockaddr_union addr; socklen_t addr_len = sizeof addr.vm; - r = getsockname(vsock_fd, &addr.sa, &addr_len); - if (r < 0) + if (getsockname(vsock_fd, &addr.sa, &addr_len) < 0) return -errno; + assert(addr_len >= sizeof addr.vm); assert(addr.vm.svm_family == AF_VSOCK); From 4f3a3b769549a7a2f788e06156b32afa3633a124 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Feb 2025 09:31:29 +0100 Subject: [PATCH 28/30] sd-boot: also read type #1 entries from SMBIOS Type #11 With this we can now do: systemd-vmspawn -n -i foobar.raw -s io.systemd.boot.entries-extra:particleos-current.conf=$'title ParticleOS Current\nuki-url http://example.com/somedir/uki.efi' Assuming sd-boot is available inside the ESP of foobar.raw a new item will show up in the boot menu that allows booting directly into the specified UKI. --- man/smbios-type-11.xml | 10 +++++++ man/systemd-boot.xml | 22 +++++++++++++++ src/boot/boot.c | 63 ++++++++++++++++++++++++++++++++++++------ src/boot/smbios.c | 8 +++--- src/boot/smbios.h | 2 +- src/boot/stub.c | 2 +- 6 files changed, 93 insertions(+), 14 deletions(-) diff --git a/man/smbios-type-11.xml b/man/smbios-type-11.xml index 4d78f612e7..b576677d27 100644 --- a/man/smbios-type-11.xml +++ b/man/smbios-type-11.xml @@ -74,6 +74,16 @@ + + + io.systemd.boot-entries.extra:ID=DEFINITION + + This allows inserting additional entries into the systemd-boot + menu. For details see + systemd-boot7 + + + diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml index 22f3cdb4ca..6da2714581 100644 --- a/man/systemd-boot.xml +++ b/man/systemd-boot.xml @@ -594,6 +594,28 @@ + + + io.systemd.boot-entries.extra:ID=DEFINITION + + This allows inserting additional entries into the systemd-boot + menu. Take a pair of menu entry identifier and menu entry definition string. The former should be + suitable for use as a filename of a Boot Loader Specification Type #1 entry filename (note that it is + used for identification purposes only, no file of this name is actually accessed), the latter shall + follow the syntax of the contents of a Type #1 entry. Any menu entry defined this way is processed + and shown in pretty much the same way as a Type #1 entry read from the ESP or XBOOTLDR + partition. Example: + + io.systemd.boot-entries.extra:fooos-current.conf=title FooOS (Current) +uki-url http://example.com/somedir/fooos.efi + + Note that this example contains a newline character. When generating this string from a shell + care must be taken to encode it correctly. + + Pass multiple strings formatted this way to generate multiple menu entries. + + + diff --git a/src/boot/boot.c b/src/boot/boot.c index 980fcff7c3..75f764bc4e 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -1464,7 +1464,6 @@ static void boot_entry_add_type1( assert(config); assert(device); assert(root_dir); - assert(path); assert(file); assert(content); @@ -1607,7 +1606,8 @@ static void boot_entry_add_type1( config_add_entry(config, entry); - boot_entry_parse_tries(entry, path, file, u".conf"); + if (path) + boot_entry_parse_tries(entry, path, file, u".conf"); TAKE_PTR(entry); } @@ -1717,6 +1717,19 @@ static void config_load_defaults(Config *config, EFI_FILE *root_dir) { (void) efivar_get_str16(MAKE_GUID_PTR(LOADER), u"LoaderEntryLastBooted", &config->entry_saved); } +static bool valid_type1_filename(const char16_t *fname) { + assert(fname); + + if (IN_SET(fname[0], u'.', u'\0')) + return false; + if (!endswith_no_case(fname, u".conf")) + return false; + if (startswith_no_case(fname, u"auto-")) + return false; + + return true; +} + static void config_load_type1_entries( Config *config, EFI_HANDLE *device, @@ -1747,13 +1760,9 @@ static void config_load_type1_entries( if (err != EFI_SUCCESS || !f) break; - if (f->FileName[0] == '.') - continue; if (FLAGS_SET(f->Attribute, EFI_FILE_DIRECTORY)) continue; - if (!endswith_no_case(f->FileName, u".conf")) - continue; - if (startswith_no_case(f->FileName, u"auto-")) + if (!valid_type1_filename(f->FileName)) continue; err = file_read(entries_dir, @@ -1769,6 +1778,41 @@ static void config_load_type1_entries( } } +static void config_load_smbios_entries( + Config *config, + EFI_HANDLE *device, + EFI_FILE *root_dir, + const char16_t *loaded_image_path) { + + assert(config); + assert(device); + assert(root_dir); + + /* Loads Boot Loader Type #1 entries from SMBIOS 11 */ + + if (is_confidential_vm()) + return; /* Don't consume SMBIOS in CoCo contexts */ + + for (const char *after = NULL, *extra;; after = extra) { + extra = smbios_find_oem_string("io.systemd.boot.entries-extra:", after); + if (!extra) + break; + + const char *eq = strchr8(extra, '='); + if (!eq) + continue; + + _cleanup_free_ char16_t *fname = xstrn8_to_16(extra, eq - extra); + if (!valid_type1_filename(fname)) + continue; + + /* Make a copy, since boot_entry_add_type1() wants to modify it */ + _cleanup_free_ char *contents = xstrdup8(eq + 1); + + boot_entry_add_type1(config, device, root_dir, /* path= */ NULL, fname, contents, loaded_image_path); + } +} + static int boot_entry_compare(const BootEntry *a, const BootEntry *b) { int r; @@ -2775,7 +2819,7 @@ static EFI_STATUS image_start( _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"); + const char *extra = smbios_find_oem_string("io.systemd.boot.kernel-cmdline-extra=", /* after= */ NULL); if (extra) { _cleanup_free_ char16_t *tmp = TAKE_PTR(options), *extra16 = xstr8_to_16(extra); if (isempty(tmp)) @@ -2989,6 +3033,9 @@ static void config_load_all_entries( /* Similar, but on any XBOOTLDR partition */ config_load_xbootldr(config, loaded_image->DeviceHandle); + /* Pick up entries defined via SMBIOS Type #11 */ + config_load_smbios_entries(config, loaded_image->DeviceHandle, root_dir, loaded_image_path); + /* Sort entries after version number */ sort_pointer_array((void **) config->entries, config->n_entries, (compare_pointer_func_t) boot_entry_compare); diff --git a/src/boot/smbios.c b/src/boot/smbios.c index e1bcd62e2c..184aca14ce 100644 --- a/src/boot/smbios.c +++ b/src/boot/smbios.c @@ -182,7 +182,7 @@ bool smbios_in_hypervisor(void) { return FLAGS_SET(type0->bios_characteristics_ext[1], 1 << 4); } -const char* smbios_find_oem_string(const char *name) { +const char* smbios_find_oem_string(const char *name, const char *after) { uint64_t left; assert(name); @@ -199,9 +199,9 @@ const char* smbios_find_oem_string(const char *name) { 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; + const char *suffix = startswith8(p, name); + if (suffix && (!after || suffix > after)) + return suffix; p = e + 1; } diff --git a/src/boot/smbios.h b/src/boot/smbios.h index 83c3c43589..694ef568e6 100644 --- a/src/boot/smbios.h +++ b/src/boot/smbios.h @@ -5,7 +5,7 @@ bool smbios_in_hypervisor(void); -const char* smbios_find_oem_string(const char *name); +const char* smbios_find_oem_string(const char *name, const char *after); typedef struct RawSmbiosInfo { const char *manufacturer; diff --git a/src/boot/stub.c b/src/boot/stub.c index ac6c112459..4793391b06 100644 --- a/src/boot/stub.c +++ b/src/boot/stub.c @@ -783,7 +783,7 @@ static void cmdline_append_and_measure_smbios(char16_t **cmdline, int *parameter if (is_confidential_vm()) return; - const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra"); + const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra=", /* after= */ NULL); if (!extra) return; From fc8f0089bfd2850fe212756fcd63b107b9ef4266 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Feb 2025 22:42:10 +0100 Subject: [PATCH 29/30] boot: when we detect that sd-boot is called as NBP, print friendly message Fixes: #11850 --- src/boot/boot.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/boot/boot.c b/src/boot/boot.c index 75f764bc4e..6e55a659dd 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -3118,21 +3118,34 @@ static EFI_STATUS run(EFI_HANDLE image) { init_usec = time_usec(); - /* Ask Shim to leave its protocol around, so that the stub can use it to validate PEs. - * By default, Shim uninstalls its protocol when calling StartImage(). */ - shim_retain_protocol(); - err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); if (err != EFI_SUCCESS) return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m"); + err = discover_root_dir(loaded_image, &root_dir); + if (err != EFI_SUCCESS) { + log_error_status(err, "Unable to open root directory: %m"); + + /* If opening the root directory fails this typically means someone is trying to boot our + * systemd-boot EFI PE binary as network boot NBP. That cannot work however, since we + * wouldn't find any menu entries. Provide a helpful message what to try instead. */ + + if (err == EFI_UNSUPPORTED) + log_info("| Note that invoking systemd-boot directly as UEFI network boot NBP is not\n" + "| supported. Instead of booting the systemd-boot PE binary (i.e. an .efi file)\n" + "| via the network, use an EFI GPT disk image (i.e. a file with .img suffix)\n" + "| containing systemd-boot instead."); + + return err; + } + + /* Ask Shim to leave its protocol around, so that the stub can use it to validate PEs. + * By default, Shim uninstalls its protocol when calling StartImage(). */ + shim_retain_protocol(); + export_common_variables(loaded_image); export_loader_variables(loaded_image, init_usec); - err = discover_root_dir(loaded_image, &root_dir); - if (err != EFI_SUCCESS) - return log_error_status(err, "Unable to open root directory: %m"); - (void) load_drivers(image, loaded_image, root_dir); _cleanup_free_ char16_t *loaded_image_path = NULL; From 898944a51a41dfde41701a93b92dddec86f120be Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Feb 2025 13:18:25 +0100 Subject: [PATCH 30/30] update TODO --- TODO | 61 ------------------------------------------------------------ 1 file changed, 61 deletions(-) diff --git a/TODO b/TODO index dba77b4172..8580d9a088 100644 --- a/TODO +++ b/TODO @@ -179,9 +179,6 @@ Features: Net result: people who try to debug why their process gets killed should have some minimal, nice metadata directly on the signal event. -* import-generator: add option to download into /run/ rather than /var/, and - make it default in the initrd - * sd-boot/sd-stub: install a uefi "handle" to a sidecar dir of bls type #1 entries with an "uki" or "uki-url" stanza, and make sd-stub look for that. That way we can parameterize type #1 entries nicely. @@ -212,19 +209,11 @@ Features: own image loading code paths. Given that everything's statically linked anyway on UEFI it should be easy to just jump into the already loaded image. -* introduce smbios type 11 variables for inserting additional entries into boot - loader spec type 1 menu list. Usecase is in particular with http boot: allow - VMMs to insert additional cheap payloads that are acquired on demand via - http. for example: always install diskomator or such. - * storagetm: maybe also serve the specified disk via HTTP? we have glue for microhttpd anyway already. Idea would also be serve currently booted UKI as separate HTTP resource, so that EFI http boot on another system could directly boot from our system, with full access to the hdd. -* support boot-into-tarball in systemd-import-generator: optionally bind mount - downloaded image to /sysroot/ - * support specifying download hash sum in systemd-import-generator expression to pin image/tarball. @@ -274,14 +263,6 @@ Features: * systemd-cryptenroll: add --firstboot or so, that will interactively ask user whether recovery key shall be enrolled and do so -* sd-boot: when looking for a BLS type #1 resource and it cannot be found in - the ESP check if ESP is backed by ramdisk/http and then request it from same - http base. Then: make mkosi build a 2nd esp maybe called the "netesp" that - contains bls type #1 entries pointing to the UKIs which would then be - requested via http this way. also set an efi var indicating that this - happened, so that initrd can recognize this and take into consdiration when - looking for root fs - * bootctl: add tool for registering BootXXX entry that boots from some http server of your choice (i.e. like kernel-bootcfg --add-uri=) @@ -1044,30 +1025,12 @@ Features: into two of the three PAM stacks gdm provides. See discussion at https://github.com/authselect/authselect/pull/311 -* sd-boot: make boot loader spec type #1 accept http urls in "linux" - lines. Then, do the uefi http dance to download kernels and boot them. This - is then useful for network boot, by embedding a cpio with type #1 snippets - in sd-boot, which reference remote kernels. - * maybe prohibit setuid() to the nobody user, to lock things down, via seccomp. the nobody is not a user any code should run under, ever, as that user would possibly get a lot of access to resources it really shouldn't be getting access to due to the userns + nfs semantics of the user. Alternatively: use the seccomp log action, and allow it. -* sd-boot: add a new PE section .bls or so that carries a cpio with additional - boot loader entries (both type1 and type2). Then when initializing, find this - section, iterate through it and populate menu with it. cpio is simple enough - to make a parser for this reasonably robust. use same path structures as in - the ESP. Similar add one for signature key drop-ins. - -* sd-boot: also allow passing in the cpio as in the previous item via SMBIOS - -* add a new EFI tool "sd-fetch" or so. It looks in a PE section ".url" for an - URL, then downloads the file from it using UEFI HTTP APIs, and executes it. - Use case: provide a minimal ESP with sd-boot and a couple of these sd-fetch - binaries in place of UKIs, and download them on-the-fly. - * maybe: systemd-loop-generator that sets up loopback devices if requested via kernel cmdline. use case: include encrypted/verity root fs in UKI. @@ -1307,9 +1270,6 @@ Features: * systemd-measure tool: - pre-calculate PCR 12 (command line) + PCR 13 (sysext) the same way we can precalculate PCR 11 -* in sd-boot: load EFI drivers from a new PE section. That way, one can have a - "supercharged" sd-boot binary, that could carry ext4 drivers built-in. - * sd-device: add an API for acquiring list of child devices, given a device objects (i.e. all child dirents that dirs or symlinks to dirs) @@ -1503,27 +1463,6 @@ Features: * sd-event: add 1st class event source for timezone changes -* support uefi/http boots with sd-boot: instead of looking for dropin files in - /loader/entries/ dir, look for a file /loader/entries/SHA256SUMS and use that - as directory manifest. The file would be a standard directory listing as - generated by GNU sha256sums. - -* sd-boot: maybe add support for embedding the various auxiliary resources we - look for right in the sd-boot binary. i.e. take inspiration from sd-stub - logic: allow combining sd-boot via ukify with kernels to enumerate, .conf - files, drivers, keys to enroll and so on. Then, add whatever we find that way - to the menu. Use case: allow building a single PE image you can boot into via - UEFI HTTP boot. - -* maybe add a new UEFI stub binary "sd-http". It works similar to sd-stub, but - all it does is download a file from a http server, and execute it, after - optionally checking its hash sum. idea would be: combine this "sd-http" stub - binary with some minimal info about a URL + hash sum, plus .osrel data, and - drop it into the unified kernel dir in the ESP. And bam you have something - that is tiny, feels a lot like a unified kernel, but all it does is chainload - the real kernel. benefit: downloading these stubs would be tiny and quick, - hence cheap for enumeration. - * sysext: measure all activated sysext into a TPM PCR * systemd-dissect: show available versions inside of a disk image, i.e. if