--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cpio.h"
+#include "measure.h"
+#include "util.h"
+
+#define EXTRA_DIR_SUFFIX L".extra.d"
+
+static CHAR8* write_cpio_word(CHAR8 *p, UINT32 v) {
+ static const char hex[] = "0123456789abcdef";
+
+ assert(p);
+
+ /* Writes a CPIO header 8 character hex value */
+
+ for (UINTN i = 0; i < 8; i++)
+ p[7-i] = hex[(v >> (4 * i)) & 0xF];
+
+ return p + 8;
+}
+
+static CHAR8* mangle_filename(CHAR8 *p, const CHAR16 *f) {
+ CHAR8* w;
+
+ assert(p);
+ assert(f);
+
+ /* Basically converts UTF-16 to plain ASCII (note that we filtered non-ASCII filenames beforehand, so
+ * this operation is always safe) */
+
+ for (w = p; *f != 0; f++) {
+ assert(*f <= 0x7fu);
+
+ *(w++) = *f;
+ }
+
+ *w = 0;
+ return w;
+}
+
+static CHAR8* pad4(CHAR8 *p, const CHAR8* start) {
+ assert(p);
+ assert(start);
+ assert(p >= start);
+
+ /* Appends NUL bytes to 'p', until the address is divisable by 4, when taken relative to 'start' */
+
+ while ((p - start) % 4 != 0)
+ *(p++) = 0;
+
+ return p;
+}
+
+static EFI_STATUS pack_cpio_one(
+ const CHAR16 *fname,
+ const VOID *contents,
+ UINTN contents_size,
+ const CHAR8 *target_dir_prefix,
+ UINT32 access_mode,
+ UINT32 *inode_counter,
+ VOID **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ UINTN l, target_dir_prefix_size, fname_size, q;
+ CHAR8 *a;
+
+ assert(fname);
+ assert(contents_size || contents_size == 0);
+ assert(target_dir_prefix);
+ assert(inode_counter);
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+
+ /* Serializes one file in the cpio format understood by the kernel initrd logic.
+ *
+ * See: https://www.kernel.org/doc/Documentation/early-userspace/buffer-format.txt */
+
+ if (contents_size > UINT32_MAX) /* cpio cannot deal with > 32bit file sizes */
+ return EFI_LOAD_ERROR;
+
+ if (*inode_counter == UINT32_MAX) /* more than 2^32-1 inodes? yikes. cpio doesn't support that either */
+ return EFI_OUT_OF_RESOURCES;
+
+ l = 6 + 13*8 + 1 + 1; /* Fixed CPIO header size, slash separator, and NUL byte after the file name*/
+
+ target_dir_prefix_size = strlena(target_dir_prefix);
+ if (l > UINTN_MAX - target_dir_prefix_size)
+ return EFI_OUT_OF_RESOURCES;
+ l += target_dir_prefix_size;
+
+ fname_size = StrLen(fname);
+ if (l > UINTN_MAX - fname_size)
+ return EFI_OUT_OF_RESOURCES;
+ l += fname_size; /* append space for file name */
+
+ /* CPIO can't deal with fnames longer than 2^32-1 */
+ if (target_dir_prefix_size + fname_size >= UINT32_MAX)
+ return EFI_OUT_OF_RESOURCES;
+
+ /* Align the whole header to 4 byte size */
+ l = ALIGN_TO(l, 4);
+ if (l == UINTN_MAX) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ /* Align the contents to 4 byte size */
+ q = ALIGN_TO(contents_size, 4);
+ if (q == UINTN_MAX) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ if (l > UINTN_MAX - q) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+ l += q; /* Add contents to header */
+
+ if (*cpio_buffer_size > UINTN_MAX - l) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+ a = ReallocatePool(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l);
+ if (!a)
+ return EFI_OUT_OF_RESOURCES;
+
+ *cpio_buffer = a;
+ a = (CHAR8*) *cpio_buffer + *cpio_buffer_size;
+
+ CopyMem(a, "070701", 6); /* magic ID */
+ a += 6;
+
+ a = write_cpio_word(a, (*inode_counter)++); /* inode */
+ a = write_cpio_word(a, access_mode | 0100000 /* = S_IFREG */); /* mode */
+ a = write_cpio_word(a, 0); /* uid */
+ a = write_cpio_word(a, 0); /* gid */
+ a = write_cpio_word(a, 1); /* nlink */
+
+ /* Note: we don't make any attempt to propagate the mtime here, for two reasons: it's a mess given
+ * that FAT usually is assumed to operate with timezoned timestamps, while UNIX does not. More
+ * importantly though: the modifications times would hamper our goals of providing stable
+ * measurements for the same boots. After all we extend the initrds we generate here into TPM2
+ * PCRs. */
+ a = write_cpio_word(a, 0); /* mtime */
+ a = write_cpio_word(a, contents_size); /* size */
+ a = write_cpio_word(a, 0); /* major(dev) */
+ a = write_cpio_word(a, 0); /* minor(dev) */
+ a = write_cpio_word(a, 0); /* major(rdev) */
+ a = write_cpio_word(a, 0); /* minor(rdev) */
+ a = write_cpio_word(a, target_dir_prefix_size + fname_size + 1); /* fname size */
+ a = write_cpio_word(a, 0); /* "crc" */
+
+ CopyMem(a, target_dir_prefix, target_dir_prefix_size);
+ a += target_dir_prefix_size;
+ *(a++) = '/';
+ a = mangle_filename(a, fname);
+
+ /* Pad to next multiple of 4 */
+ a = pad4(a, *cpio_buffer);
+
+ CopyMem(a, contents, contents_size);
+ a += contents_size;
+
+ /* Pad to next multiple of 4 */
+ a = pad4(a, *cpio_buffer);
+
+ assert(a == (CHAR8*) *cpio_buffer + *cpio_buffer_size + l);
+ *cpio_buffer_size += l;
+
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS pack_cpio_dir(
+ const CHAR8 *path,
+ UINT32 access_mode,
+ UINT32 *inode_counter,
+ VOID **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ UINTN l, path_size;
+ CHAR8 *a;
+
+ assert(path);
+ assert(inode_counter);
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+
+ /* Serializes one directory inode in cpio format. Note that cpio archives must first create the dirs
+ * they want to place files in. */
+
+ if (*inode_counter == UINT32_MAX)
+ return EFI_OUT_OF_RESOURCES;
+
+ l = 6 + 13*8 + 1; /* Fixed CPIO header size, and NUL byte after the file name*/
+
+ path_size = strlena(path);
+ if (l > UINTN_MAX - path_size)
+ return EFI_OUT_OF_RESOURCES;
+ l += path_size;
+
+ /* Align the whole header to 4 byte size */
+ l = ALIGN_TO(l, 4);
+ if (l == UINTN_MAX) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+
+ if (*cpio_buffer_size > UINTN_MAX - l) /* overflow check */
+ return EFI_OUT_OF_RESOURCES;
+ a = ReallocatePool(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l);
+ if (!a)
+ return EFI_OUT_OF_RESOURCES;
+
+ *cpio_buffer = a;
+ a = (CHAR8*) *cpio_buffer + *cpio_buffer_size;
+
+ CopyMem(a, "070701", 6); /* magic ID */
+ a += 6;
+
+ a = write_cpio_word(a, (*inode_counter)++); /* inode */
+ a = write_cpio_word(a, access_mode | 0040000 /* = S_IFDIR */); /* mode */
+ a = write_cpio_word(a, 0); /* uid */
+ a = write_cpio_word(a, 0); /* gid */
+ a = write_cpio_word(a, 1); /* nlink */
+ a = write_cpio_word(a, 0); /* mtime */
+ a = write_cpio_word(a, 0); /* size */
+ a = write_cpio_word(a, 0); /* major(dev) */
+ a = write_cpio_word(a, 0); /* minor(dev) */
+ a = write_cpio_word(a, 0); /* major(rdev) */
+ a = write_cpio_word(a, 0); /* minor(rdev) */
+ a = write_cpio_word(a, path_size + 1); /* fname size */
+ a = write_cpio_word(a, 0); /* "crc" */
+
+ CopyMem(a, path, path_size + 1);
+ a += path_size + 1;
+
+ /* Pad to next multiple of 4 */
+ a = pad4(a, *cpio_buffer);
+
+ assert(a == (CHAR8*) *cpio_buffer + *cpio_buffer_size + l);
+
+ *cpio_buffer_size += l;
+ return EFI_SUCCESS;
+}
+
+static EFI_STATUS pack_cpio_prefix(
+ const CHAR8 *path,
+ UINT32 dir_mode,
+ UINT32 *inode_counter,
+ VOID **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ EFI_STATUS err;
+
+ assert(path);
+ assert(inode_counter);
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+
+ /* Serializes directory inodes of all prefix paths of the specified path in cpio format. Note that
+ * (similar to mkdir -p behaviour) all leading paths are created with 0555 access mode, only the
+ * final dir is created with the specified directory access mode. */
+
+ for (const CHAR8 *p = path;;) {
+ const CHAR8 *e;
+
+ e = strchra(p, '/');
+ if (!e)
+ break;
+
+ if (e > p) {
+ _cleanup_freepool_ CHAR8 *t = NULL;
+
+ t = strndup8(path, e - path);
+ if (!t)
+ return EFI_OUT_OF_RESOURCES;
+
+ err = pack_cpio_dir(t, 0555, inode_counter, cpio_buffer, cpio_buffer_size);
+ if (EFI_ERROR(err))
+ return err;
+ }
+
+ p = e + 1;
+ }
+
+ return pack_cpio_dir(path, dir_mode, inode_counter, cpio_buffer, cpio_buffer_size);
+}
+
+static EFI_STATUS pack_cpio_trailer(
+ VOID **cpio_buffer,
+ UINTN *cpio_buffer_size) {
+
+ static const char trailer[] =
+ "070701"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000001"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "00000000"
+ "0000000B"
+ "00000000"
+ "TRAILER!!!\0\0\0"; /* There's a fourth NUL byte appended here, because this is a string */
+
+ VOID *a;
+
+ /* Generates the cpio trailer record that indicates the end of our initrd cpio archive */
+
+ assert(cpio_buffer);
+ assert(cpio_buffer_size);
+ assert_cc(sizeof(trailer) % 4 == 0);
+
+ a = ReallocatePool(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + sizeof(trailer));
+ if (!a)
+ return EFI_OUT_OF_RESOURCES;
+
+ *cpio_buffer = a;
+ CopyMem((UINT8*) *cpio_buffer + *cpio_buffer_size, trailer, sizeof(trailer));
+ *cpio_buffer_size += sizeof(trailer);
+
+ return EFI_SUCCESS;
+}
+
+EFI_STATUS pack_cpio(
+ EFI_LOADED_IMAGE *loaded_image,
+ const CHAR16 *match_suffix,
+ const CHAR8 *target_dir_prefix,
+ UINT32 dir_mode,
+ UINT32 access_mode,
+ UINTN tpm_pcr,
+ const CHAR16 *tpm_description,
+ VOID **ret_buffer,
+ UINTN *ret_buffer_size) {
+
+ _cleanup_(FileHandleClosep) EFI_FILE_HANDLE root = NULL, extra_dir = NULL;
+ UINTN dirent_size = 0, buffer_size = 0, n_items = 0, n_allocated = 0;
+ _cleanup_freepool_ CHAR16 *loaded_image_path = NULL, *j = NULL;
+ _cleanup_freepool_ EFI_FILE_INFO *dirent = NULL;
+ _cleanup_(strv_freep) CHAR16 **items = NULL;
+ _cleanup_freepool_ VOID *buffer = NULL;
+ UINT32 inode = 1; /* inode counter, so that each item gets a new inode */
+ EFI_STATUS err;
+
+ assert(loaded_image);
+ assert(target_dir_prefix);
+ assert(ret_buffer);
+ assert(ret_buffer_size);
+
+ root = LibOpenRoot(loaded_image->DeviceHandle);
+ if (!root)
+ return log_error_status_stall(EFI_LOAD_ERROR, L"Unable to open root directory.");
+
+ loaded_image_path = DevicePathToStr(loaded_image->FilePath);
+ if (!loaded_image_path)
+ return log_oom();
+
+ j = PoolPrint(L"%s" EXTRA_DIR_SUFFIX, loaded_image_path);
+ if (!j)
+ return log_oom();
+
+ err = open_directory(root, j, &extra_dir);
+ if (err == EFI_NOT_FOUND) {
+ /* No extra subdir, that's totally OK */
+ *ret_buffer = NULL;
+ *ret_buffer_size = 0;
+ return EFI_SUCCESS;
+ }
+ if (EFI_ERROR(err))
+ return log_error_status_stall(err, L"Failed to open extra directory of loaded image: %r", err);
+
+ for (;;) {
+ _cleanup_freepool_ CHAR16 *d = NULL;
+
+ err = readdir_harder(extra_dir, &dirent, &dirent_size);
+ if (EFI_ERROR(err))
+ return log_error_status_stall(err, L"Failed to read extra directory of loaded image: %r", err);
+ if (!dirent) /* End of directory */
+ break;
+
+ if (dirent->FileName[0] == '.')
+ continue;
+ if (dirent->Attribute & EFI_FILE_DIRECTORY)
+ continue;
+ if (match_suffix && !endswith_no_case(dirent->FileName, match_suffix))
+ continue;
+ if (!is_ascii(dirent->FileName))
+ continue;
+ if (StrLen(dirent->FileName) > 255) /* Max filename size on Linux */
+ continue;
+
+ d = StrDuplicate(dirent->FileName);
+ if (!d)
+ return log_oom();
+
+ if (n_items+2 > n_allocated) {
+ UINTN m;
+
+ /* We allocate 16 entries at a time, as a matter of optimization */
+ if (n_items > (UINTN_MAX / sizeof(UINT16)) - 16) /* Overflow check, just in case */
+ return log_oom();
+
+ m = n_items + 16;
+ items = ReallocatePool(items, n_allocated * sizeof(UINT16*), m * sizeof(UINT16*));
+ if (!items)
+ return log_oom();
+
+ n_allocated = m;
+ }
+
+ items[n_items++] = TAKE_PTR(d);
+ items[n_items] = NULL; /* Let's always NUL terminate, to make freeing via strv_free() easy */
+ }
+
+ if (n_items == 0) {
+ /* Empty directory */
+ *ret_buffer = NULL;
+ *ret_buffer_size = 0;
+ return EFI_SUCCESS;
+ }
+
+ /* Now, sort the files we found, to make this uniform and stable (and to ensure the TPM measurements
+ * are not dependent on read order) */
+ sort_pointer_array((VOID**) items, n_items, (compare_pointer_func_t) StrCmp);
+
+ /* 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. */
+ err = pack_cpio_prefix(target_dir_prefix, dir_mode, &inode, &buffer, &buffer_size);
+ if (EFI_ERROR(err))
+ return log_error_status_stall(err, L"Failed to pack cpio prefix: %r", err);
+
+ for (UINTN i = 0; i < n_items; i++) {
+ _cleanup_freepool_ CHAR8 *content = NULL;
+ UINTN contentsize;
+
+ err = file_read(extra_dir, items[i], 0, 0, &content, &contentsize);
+ if (EFI_ERROR(err)) {
+ log_error_status_stall(err, L"Failed to read %s, ignoring: %r", items[i], err);
+ continue;
+ }
+
+ err = pack_cpio_one(
+ items[i],
+ content, contentsize,
+ target_dir_prefix,
+ access_mode,
+ &inode,
+ &buffer, &buffer_size);
+ if (EFI_ERROR(err))
+ return log_error_status_stall(err, L"Failed to pack cpio file %s: %r", dirent->FileName, err);
+ }
+
+ err = pack_cpio_trailer(&buffer, &buffer_size);
+ if (EFI_ERROR(err))
+ return log_error_status_stall(err, L"Failed to pack cpio trailer: %r");
+
+#if ENABLE_TPM
+ err = tpm_log_event(
+ tpm_pcr,
+ (EFI_PHYSICAL_ADDRESS) (UINTN) buffer,
+ buffer_size,
+ tpm_description);
+ if (EFI_ERROR(err))
+ log_error_stall(L"Unable to add initrd TPM measurement for PCR %u (%s), ignoring: %r", tpm_pcr, tpm_description, err);
+#endif
+
+ *ret_buffer = TAKE_PTR(buffer);
+ *ret_buffer_size = buffer_size;
+
+ return EFI_SUCCESS;
+}
#include <efi.h>
#include <efilib.h>
+#include "cpio.h"
#include "disk.h"
#include "graphics.h"
#include "linux.h"
/* magic string to find in the binary image */
static const char __attribute__((used)) magic[] = "#### LoaderInfo: systemd-stub " GIT_VERSION " ####";
+static EFI_STATUS combine_initrd(
+ EFI_PHYSICAL_ADDRESS initrd_base, UINTN initrd_size,
+ const VOID *credential_initrd, UINTN credential_initrd_size,
+ const VOID *sysext_initrd, UINTN sysext_initrd_size,
+ EFI_PHYSICAL_ADDRESS *ret_initrd_base, UINTN *ret_initrd_size) {
+
+ EFI_PHYSICAL_ADDRESS base = UINT32_MAX; /* allocate an area below the 32bit boundary for this */
+ EFI_STATUS err;
+ UINT8 *p;
+ UINTN n;
+
+ assert(ret_initrd_base);
+ assert(ret_initrd_size);
+
+ /* Combines three initrds into one, by simple concatenation in memory */
+
+ n = ALIGN_TO(initrd_size, 4); /* main initrd might not be padded yet */
+ if (credential_initrd) {
+ if (n > UINTN_MAX - credential_initrd_size)
+ return EFI_OUT_OF_RESOURCES;
+
+ n += credential_initrd_size;
+ }
+ if (sysext_initrd) {
+ if (n > UINTN_MAX - sysext_initrd_size)
+ return EFI_OUT_OF_RESOURCES;
+
+ n += sysext_initrd_size;
+ }
+
+ err = uefi_call_wrapper(
+ BS->AllocatePages, 4,
+ AllocateMaxAddress,
+ EfiLoaderData,
+ EFI_SIZE_TO_PAGES(n),
+ &base);
+ if (EFI_ERROR(err))
+ return log_error_status_stall(err, L"Failed to allocate space for combined initrd: %r", err);
+
+ p = (UINT8*) (UINTN) base;
+ if (initrd_base != 0) {
+ UINTN pad;
+
+ /* Order matters, the real initrd must come first, since it might include microcode updates
+ * which the kernel only looks for in the first cpio archive */
+ CopyMem(p, (VOID*) (UINTN) initrd_base, initrd_size);
+ p += initrd_size;
+
+ pad = ALIGN_TO(initrd_size, 4) - initrd_size;
+ if (pad > 0) {
+ ZeroMem(p, pad);
+ p += pad;
+ }
+ }
+
+ if (credential_initrd) {
+ CopyMem(p, credential_initrd, credential_initrd_size);
+ p += credential_initrd_size;
+ }
+
+ if (sysext_initrd) {
+ CopyMem(p, sysext_initrd, sysext_initrd_size);
+ p += sysext_initrd_size;
+ }
+
+ assert((UINT8*) (UINTN) base + n == p);
+
+ *ret_initrd_base = base;
+ *ret_initrd_size = n;
+
+ return EFI_SUCCESS;
+}
+
EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) {
- EFI_LOADED_IMAGE *loaded_image;
enum {
SECTION_CMDLINE,
NULL,
};
+ UINTN cmdline_len = 0, initrd_size, credential_initrd_size = 0, sysext_initrd_size = 0;
+ _cleanup_freepool_ VOID *credential_initrd = NULL, *sysext_initrd = NULL;
EFI_PHYSICAL_ADDRESS linux_base, initrd_base;
- UINTN cmdline_len = 0, initrd_size;
+ EFI_LOADED_IMAGE *loaded_image;
UINTN addrs[_SECTION_MAX] = {};
UINTN szs[_SECTION_MAX] = {};
CHAR8 *cmdline = NULL;
if (szs[SECTION_SPLASH] > 0)
graphics_splash((UINT8*) (UINTN) loaded_image->ImageBase + addrs[SECTION_SPLASH], szs[SECTION_SPLASH], NULL);
+ (VOID) pack_cpio(loaded_image,
+ L".cred",
+ (const CHAR8*) ".extra/credentials",
+ /* dir_mode= */ 0500,
+ /* access_mode= */ 0400,
+ /* tpm_pcr= */ TPM_PCR_INDEX_KERNEL_PARAMETERS,
+ L"Credentials initrd",
+ &credential_initrd,
+ &credential_initrd_size);
+
+ (VOID) pack_cpio(loaded_image,
+ L".raw",
+ (const CHAR8*) ".extra/sysext",
+ /* dir_mode= */ 0555,
+ /* access_mode= */ 0444,
+ /* tpm_pcr= */ TPM_PCR_INDEX_INITRD,
+ L"System extension initrd",
+ &sysext_initrd,
+ &sysext_initrd_size);
+
linux_base = (EFI_PHYSICAL_ADDRESS) (UINTN) loaded_image->ImageBase + addrs[SECTION_LINUX];
initrd_size = szs[SECTION_INITRD];
initrd_base = initrd_size != 0 ? (EFI_PHYSICAL_ADDRESS) (UINTN) loaded_image->ImageBase + addrs[SECTION_INITRD] : 0;
- err = linux_exec(image, cmdline, cmdline_len, linux_base, initrd_base, initrd_size);
+ if (credential_initrd || sysext_initrd) {
+ /* If we have generated initrds dynamically, let's combine them with the built-in initrd. */
+ err = combine_initrd(
+ initrd_base, initrd_size,
+ credential_initrd, credential_initrd_size,
+ sysext_initrd, sysext_initrd_size,
+ &initrd_base, &initrd_size);
+ if (EFI_ERROR(err))
+ return err;
+
+ /* Given these might be large let's free them explicitly, quickly. */
+ if (credential_initrd) {
+ FreePool(credential_initrd);
+ credential_initrd = NULL;
+ }
+
+ if (sysext_initrd) {
+ FreePool(sysext_initrd);
+ sysext_initrd = NULL;
+ }
+ }
+ err = linux_exec(image, cmdline, cmdline_len, linux_base, initrd_base, initrd_size);
graphics_mode(FALSE);
return log_error_status_stall(err, L"Execution of embedded linux image failed: %r", err);
}