diff --git a/src/boot/efi/cpio.c b/src/boot/efi/cpio.c new file mode 100644 index 0000000000..9c99454d9a --- /dev/null +++ b/src/boot/efi/cpio.c @@ -0,0 +1,466 @@ +/* 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; +} diff --git a/src/boot/efi/cpio.h b/src/boot/efi/cpio.h new file mode 100644 index 0000000000..8a6d093122 --- /dev/null +++ b/src/boot/efi/cpio.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +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 pcr, + const CHAR16 *tpm_description, + VOID **ret_buffer, + UINTN *ret_buffer_size); diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build index a90f04e766..3ae1c2f180 100644 --- a/src/boot/efi/meson.build +++ b/src/boot/efi/meson.build @@ -2,6 +2,7 @@ efi_headers = files(''' console.h + cpio.h devicetree.h disk.h graphics.h @@ -39,6 +40,7 @@ stub_sources = ''' linux.c splash.c stub.c + cpio.c '''.split() if conf.get('ENABLE_EFI') == 1 and get_option('gnu-efi') != 'false' diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c index 9a3c9982d9..ae06599005 100644 --- a/src/boot/efi/stub.c +++ b/src/boot/efi/stub.c @@ -3,6 +3,7 @@ #include #include +#include "cpio.h" #include "disk.h" #include "graphics.h" #include "linux.h" @@ -15,8 +16,80 @@ /* 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, @@ -34,8 +107,10 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { 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; @@ -123,13 +198,54 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { 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); } diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h index ea32a76166..3a29ff2d7b 100644 --- a/src/boot/efi/util.h +++ b/src/boot/efi/util.h @@ -6,6 +6,13 @@ #include "string-util-fundamental.h" +/* This TPM PCR is where most Linux infrastructure extends the kernel command line into, and so do we. We also extend + * any passed credentials here. */ +#define TPM_PCR_INDEX_KERNEL_PARAMETERS 8 + +/* This TPM PCR is where most Linux infrastructure extends the initrd binary images into, and so do we. */ +#define TPM_PCR_INDEX_INITRD 4 + #define OFFSETOF(x,y) __builtin_offsetof(x,y) #define UINTN_MAX (~(UINTN)0)