]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
boot: load extra files for UKIs into memory and register as initrds
authorLennart Poettering <lennart@amutable.com>
Wed, 25 Mar 2026 17:15:38 +0000 (18:15 +0100)
committerLennart Poettering <lennart@amutable.com>
Wed, 29 Apr 2026 11:36:56 +0000 (13:36 +0200)
This generates on-the-fly cpio initrds from 'extra' resources declared
in Type #1 entries and installs them via the Linux initrd protocol so
that they get passed to the Linux kernel.

Replaces: #39286

src/boot/boot.c
src/boot/meson.build

index 6ee2aded0895fbdf1e110acadde6700ddf519256..fff35de864b8c46280fa207f8fed372595b4c58d 100644 (file)
@@ -3,6 +3,7 @@
 #include "bcd.h"
 #include "bootspec-fundamental.h"
 #include "console.h"
+#include "cpio.h"
 #include "device-path-util.h"
 #include "devicetree.h"
 #include "drivers.h"
@@ -38,6 +39,9 @@
 #include "version.h"
 #include "vmm.h"
 
+/* Safety margin, refuse larger extra files (this is not load bearing, only a safety net for robustness reasons). */
+#define EXTRA_SIZE_MAX (1024U * 1024U * 1536U)
+
 /* Magic string for recognizing our own binaries */
 #define SD_MAGIC "#### LoaderInfo: systemd-boot " GIT_VERSION " ####"
 DECLARE_NOALLOC_SECTION(".sdmagic", SD_MAGIC);
@@ -2581,7 +2585,9 @@ static EFI_STATUS initrd_prepare(
         assert(ret_initrd_pages);
         assert(ret_initrd_size);
 
-        if (entry->type != LOADER_LINUX || strv_isempty(entry->initrd)) {
+        assert(entry->type == LOADER_LINUX);
+
+        if (strv_isempty(entry->initrd)) {
                 *ret_options = NULL;
                 *ret_initrd_pages = (Pages) {};
                 *ret_initrd_size = 0;
@@ -2685,6 +2691,174 @@ static EFI_STATUS initrd_prepare(
         return EFI_SUCCESS;
 }
 
+static EFI_STATUS load_extras(
+                EFI_FILE *root,
+                const BootEntry *entry,
+                Pages *ret_initrd_pages,
+                size_t *ret_initrd_size) {
+
+        EFI_STATUS err;
+
+        assert(root);
+        assert(entry);
+        assert(ret_initrd_pages);
+        assert(ret_initrd_size);
+
+        assert(IN_SET(entry->type, LOADER_UKI, LOADER_UKI_URL));
+
+        _cleanup_(iovec_done) struct iovec previous_initrd = {}, confext_initrd = {}, sysext_initrd = {}, credential_initrd = {};
+
+        const struct ExtraResourceInfo {
+                const char16_t *suffix;
+                const CpioTarget *target;
+                struct iovec *iovec;
+                const char16_t *tpm_description;
+        } table[] = {
+                { u".cred",        &cpio_target_credentials, &credential_initrd, u"Entry credentials initrd"             },
+                { u".sysext.raw",  &cpio_target_sysext,      &sysext_initrd,     u"Entry system extension initrd"        },
+                { u".confext.raw", &cpio_target_confext,     &confext_initrd,    u"Entry configuration extension initrd" },
+        };
+
+        if (strv_isempty(entry->extras))
+                goto nothing;
+
+        uint32_t inode = 1; /* inode counter, so that each item gets a new inode */
+        unsigned n = 0;
+
+        STRV_FOREACH(i, entry->extras) {
+                _cleanup_file_close_ EFI_FILE *handle = NULL;
+                err = root->Open(root, &handle, *i, EFI_FILE_MODE_READ, /* Attributes= */ 0);
+                if (err != EFI_SUCCESS) {
+                        log_warning_status(err, "Failed to open extra file '%ls', ignoring: %m", *i);
+                        continue;
+                }
+
+                _cleanup_free_ EFI_FILE_INFO *info = NULL;
+                err = get_file_info(handle, &info, /* ret_size= */ NULL);
+                if (err != EFI_SUCCESS) {
+                        log_warning_status(err, "Failed to get information about file '%ls', ignoring: %m", *i);
+                        continue;
+                }
+
+                if (FLAGS_SET(info->Attribute, EFI_FILE_DIRECTORY)) {
+                        log_warning("Extra file '%ls' is a directory, ignoring.", *i);
+                        continue;
+                }
+
+                if (info->FileSize == 0) {
+                        log_warning("Extra file '%ls' is empty, ignoring.", *i);
+                        continue;
+                }
+                if (info->FileSize > EXTRA_SIZE_MAX) {
+                        log_warning("Extra file '%ls' is larger than allowed extra file size, ignoring.", *i);
+                        continue;
+                }
+
+                if (!is_ascii(info->FileName)) {
+                        log_warning("Extra file name '%ls' is not valid ASCII, ignoring.", *i);
+                        continue;
+                }
+                if (strlen16(info->FileName) > 255) { /* Max filename size on Linux */
+                        log_warning("Filename '%ls' too long, ignoring.", *i);
+                        continue;
+                }
+
+                const struct ExtraResourceInfo *x = NULL;
+                FOREACH_ELEMENT(j, table) {
+                        if (endswith_no_case(info->FileName, j->suffix)) {
+                                x = j;
+                                break;
+                        }
+                }
+                if (!x) {
+                        log_warning("Unrecognized type of extra file '%ls', ignoring.", info->FileName);
+                        continue;
+                }
+
+                _cleanup_free_ char *content = NULL;
+                size_t contentsize = 0;  /* avoid false maybe-uninitialized warning */
+                err = file_handle_read(handle, /* offset= */ 0, info->FileSize, &content, &contentsize);
+                if (err != EFI_SUCCESS) {
+                        log_warning_status(err, "Failed to read '%ls', ignoring: %m", *i);
+                        continue;
+                }
+
+                /* Generate the leading directory inodes right before adding the first files to the
+                 * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't
+                 * exist. Note that we potentially do redundant work here: a prior iteration might already
+                 * have created the prefix for us, but to simplify this we regenerate it anyway. It's very
+                 * little data, and simplifies the implementation here a lot. */
+                err = pack_cpio_prefix(x->target, &inode, &x->iovec->iov_base, &x->iovec->iov_len);
+                if (err != EFI_SUCCESS)
+                        return log_error_status(err, "Failed to pack cpio prefix '%s': %m", x->target->directory);
+
+                err = pack_cpio_one(
+                                info->FileName,
+                                content, contentsize,
+                                x->target,
+                                &inode,
+                                &x->iovec->iov_base, &x->iovec->iov_len);
+                if (err != EFI_SUCCESS)
+                        return log_error_status(err, "Failed to pack cpio file '%ls': %m", info->FileName);
+
+                n++;
+        }
+
+        if (n == 0) /* Nothing actually loaded */
+                goto nothing;
+
+        FOREACH_ELEMENT(x, table) {
+                if (x->iovec->iov_len <= 0)
+                        continue;
+
+                err = pack_cpio_trailer(&x->iovec->iov_base, &x->iovec->iov_len);
+                if (err != EFI_SUCCESS)
+                        return log_error_status(err, "Failed to pack cpio trailer: %m");
+
+                err = tpm_log_ipl_event(
+                                x->target->tpm_pcr,
+                                POINTER_TO_PHYSICAL_ADDRESS(x->iovec->iov_base),
+                                x->iovec->iov_len,
+                                x->tpm_description,
+                                /* ret_measured= */ NULL);
+                if (err != EFI_SUCCESS)
+                        return log_error_status(
+                                        err,
+                                        "Unable to add cpio TPM measurement for PCR %u (%ls): %m",
+                                        x->target->tpm_pcr,
+                                        x->tpm_description);
+        }
+
+        /* Be nice: pick up any previously registered initrds and prepend them to what we are generating here */
+        err = initrd_read_previous(&previous_initrd);
+        if (err == EFI_NOT_FOUND)
+                log_debug_status(err, "No previous initrd installed.");
+        else if (err != EFI_SUCCESS)
+                log_warning_status(err, "Failed to read previously registered initrd, ignoring.");
+        else
+                log_debug("Successfully loaded previously installed initrd (%zu bytes).", previous_initrd.iov_len);
+
+        err = combine_initrds(
+                        (const struct iovec[]) {
+                                previous_initrd,
+                                credential_initrd,
+                                sysext_initrd,
+                                confext_initrd,
+                        },
+                        /* n_initrds= */ 4,
+                        ret_initrd_pages,
+                        ret_initrd_size);
+        if (err != EFI_SUCCESS)
+                return log_error_status(err, "Failed to combine previous with extra initrds: %m");
+
+        return EFI_SUCCESS;
+
+nothing:
+        *ret_initrd_pages = (Pages) {};
+        *ret_initrd_size = 0;
+        return EFI_SUCCESS;
+}
+
 static EFI_STATUS expand_path(
                 EFI_HANDLE parent_image,
                 EFI_DEVICE_PATH *path,
@@ -2833,15 +3007,11 @@ static EFI_STATUS call_image_start(
                 return log_error_status(err, "Error loading EFI binary %ls: %m", entry->loader);
         }
 
-        _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL;
         _cleanup_free_ char16_t *options_initrd = NULL;
-        _cleanup_pages_ Pages initrd_pages = {};
+        _cleanup_pages_ Pages initrd_pages = {};                        /* Note: please keep order intact: these pages should be released after the initrd handle is released */
+        _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL;
         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. */
@@ -2851,9 +3021,35 @@ static EFI_STATUS call_image_start(
                                 return log_error_status(err, "Error loading %ls: %m", entry->devicetree);
                 }
 
+                switch (entry->type) {
+
+                case LOADER_LINUX:
+                        /* For traditional Linux we follow 'initrd' links, because that's how things worked in the good old days */
+                        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");
+
+                        break;
+
+                case LOADER_UKI:
+                case LOADER_UKI_URL:
+                        /* For modern UKIs we'll not bother with 'initrd', but we'll instead support 'extra'
+                         * for loading credentials, sysext and confext. */
+
+                        err = load_extras(image_root, entry, &initrd_pages, &initrd_size);
+                        if (err != EFI_SUCCESS)
+                                return err; /* load_extras() logs on its own */
+                        break;
+
+                default:
+                        ;
+                }
+
                 err = initrd_register(&IOVEC_MAKE(PHYSICAL_ADDRESS_TO_POINTER(initrd_pages.addr), initrd_size), &initrd_handle);
                 if (err != EFI_SUCCESS)
                         return log_error_status(err, "Error registering initrd: %m");
+
+                /* NB: the initrd pages remain in our possession, we will free them if executing the image fails below */
         }
 
         EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
index dfac98f034a6d072bbbe942a3ccfcf9215590a14..29fb64efbee1b52a932130bd59959a68fc16c946 100644 (file)
@@ -309,6 +309,7 @@ endif
 libefi_sources = files(
         'chid.c',
         'console.c',
+        'cpio.c',
         'device-path-util.c',
         'devicetree.c',
         'drivers.c',
@@ -341,7 +342,6 @@ systemd_boot_sources = files(
 
 stub_sources = files(
         'boot-secret.c',
-        'cpio.c',
         'linux.c',
         'splash.c',
         'stub.c',