diff options
Diffstat (limited to 'src/boot/efi/cpio.c')
-rw-r--r-- | src/boot/efi/cpio.c | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/src/boot/efi/cpio.c b/src/boot/efi/cpio.c new file mode 100644 index 0000000..5b90e17 --- /dev/null +++ b/src/boot/efi/cpio.c @@ -0,0 +1,512 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "cpio.h" +#include "device-path-util.h" +#include "measure.h" +#include "proto/device-path.h" +#include "util.h" + +static char *write_cpio_word(char *p, uint32_t v) { + static const char hex[] = "0123456789abcdef"; + + assert(p); + + /* Writes a CPIO header 8 character hex value */ + + for (size_t i = 0; i < 8; i++) + p[7-i] = hex[(v >> (4 * i)) & 0xF]; + + return p + 8; +} + +static char *mangle_filename(char *p, const char16_t *f) { + char* 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 char *pad4(char *p, const char *start) { + assert(p); + assert(start); + assert(p >= start); + + /* Appends NUL bytes to 'p', until the address is divisible by 4, when taken relative to 'start' */ + + while ((p - start) % 4 != 0) + *(p++) = 0; + + return p; +} + +static EFI_STATUS pack_cpio_one( + const char16_t *fname, + const void *contents, + size_t contents_size, + const char *target_dir_prefix, + uint32_t access_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *cpio_buffer_size) { + + size_t l, target_dir_prefix_size, fname_size, q; + char *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://docs.kernel.org/driver-api/early-userspace/buffer-format.html */ + + if (contents_size > UINT32_MAX) /* cpio cannot deal with > 32-bit 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 = strlen8(target_dir_prefix); + if (l > SIZE_MAX - target_dir_prefix_size) + return EFI_OUT_OF_RESOURCES; + l += target_dir_prefix_size; + + fname_size = strlen16(fname); + if (l > SIZE_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 = ALIGN4(l); + if (l == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + /* Align the contents to 4 byte size */ + q = ALIGN4(contents_size); + if (q == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (l > SIZE_MAX - q) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + l += q; /* Add contents to header */ + + if (*cpio_buffer_size > SIZE_MAX - l) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + a = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l); + + *cpio_buffer = a; + a = (char *) *cpio_buffer + *cpio_buffer_size; + + a = mempcpy(a, "070701", 6); /* magic ID */ + + 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 + 2); /* fname size */ + a = write_cpio_word(a, 0); /* "crc" */ + + a = mempcpy(a, target_dir_prefix, target_dir_prefix_size); + *(a++) = '/'; + a = mangle_filename(a, fname); + + /* Pad to next multiple of 4 */ + a = pad4(a, *cpio_buffer); + + a = mempcpy(a, contents, contents_size); + + /* Pad to next multiple of 4 */ + a = pad4(a, *cpio_buffer); + + assert(a == (char *) *cpio_buffer + *cpio_buffer_size + l); + *cpio_buffer_size += l; + + return EFI_SUCCESS; +} + +static EFI_STATUS pack_cpio_dir( + const char *path, + uint32_t access_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *cpio_buffer_size) { + + size_t l, path_size; + char *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 = strlen8(path); + if (l > SIZE_MAX - path_size) + return EFI_OUT_OF_RESOURCES; + l += path_size; + + /* Align the whole header to 4 byte size */ + l = ALIGN4(l); + if (l == SIZE_MAX) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + if (*cpio_buffer_size > SIZE_MAX - l) /* overflow check */ + return EFI_OUT_OF_RESOURCES; + + *cpio_buffer = a = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + l); + a = (char *) *cpio_buffer + *cpio_buffer_size; + + a = mempcpy(a, "070701", 6); /* magic ID */ + + 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" */ + + a = mempcpy(a, path, path_size + 1); + + /* Pad to next multiple of 4 */ + a = pad4(a, *cpio_buffer); + + assert(a == (char *) *cpio_buffer + *cpio_buffer_size + l); + + *cpio_buffer_size += l; + return EFI_SUCCESS; +} + +static EFI_STATUS pack_cpio_prefix( + const char *path, + uint32_t dir_mode, + uint32_t *inode_counter, + void **cpio_buffer, + size_t *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 char *p = path;;) { + const char *e; + + e = strchr8(p, '/'); + if (!e) + break; + + if (e > p) { + _cleanup_free_ char *t = NULL; + + t = xstrndup8(path, e - path); + if (!t) + return EFI_OUT_OF_RESOURCES; + + err = pack_cpio_dir(t, 0555, inode_counter, cpio_buffer, cpio_buffer_size); + if (err != EFI_SUCCESS) + 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, + size_t *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 */ + + /* 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); + + *cpio_buffer = xrealloc(*cpio_buffer, *cpio_buffer_size, *cpio_buffer_size + sizeof(trailer)); + memcpy((uint8_t*) *cpio_buffer + *cpio_buffer_size, trailer, sizeof(trailer)); + *cpio_buffer_size += sizeof(trailer); + + return EFI_SUCCESS; +} + +EFI_STATUS pack_cpio( + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const char16_t *dropin_dir, + const char16_t *match_suffix, + const char *target_dir_prefix, + uint32_t dir_mode, + uint32_t access_mode, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured) { + + _cleanup_(file_closep) EFI_FILE *root = NULL, *extra_dir = NULL; + size_t dirent_size = 0, buffer_size = 0, n_items = 0, n_allocated = 0; + _cleanup_free_ char16_t *rel_dropin_dir = NULL; + _cleanup_free_ EFI_FILE_INFO *dirent = NULL; + _cleanup_(strv_freep) char16_t **items = NULL; + _cleanup_free_ void *buffer = NULL; + uint32_t 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); + + if (!loaded_image->DeviceHandle) + goto nothing; + + err = open_volume(loaded_image->DeviceHandle, &root); + if (err == EFI_UNSUPPORTED) + /* Error will be unsupported if the bootloader doesn't implement the file system protocol on + * its file handles. */ + goto nothing; + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to open root directory: %m"); + + if (!dropin_dir) { + dropin_dir = rel_dropin_dir = get_extra_dir(loaded_image->FilePath); + if (!dropin_dir) + goto nothing; + } + + err = open_directory(root, dropin_dir, &extra_dir); + if (err == EFI_NOT_FOUND) + /* No extra subdir, that's totally OK */ + goto nothing; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to open extra directory of loaded image: %m"); + + for (;;) { + _cleanup_free_ char16_t *d = NULL; + + err = readdir(extra_dir, &dirent, &dirent_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to read extra directory of loaded image: %m"); + if (!dirent) /* End of directory */ + break; + + if (dirent->FileName[0] == '.') + continue; + if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY)) + continue; + if (match_suffix && !endswith_no_case(dirent->FileName, match_suffix)) + continue; + if (!is_ascii(dirent->FileName)) + continue; + if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */ + continue; + + d = xstrdup16(dirent->FileName); + + if (n_items+2 > n_allocated) { + /* We allocate 16 entries at a time, as a matter of optimization */ + if (n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */ + return log_oom(); + + size_t m = n_items + 16; + items = xrealloc(items, n_allocated * sizeof(uint16_t *), m * sizeof(uint16_t *)); + 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 */ + goto nothing; + + /* 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) strcmp16); + + /* 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 (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio prefix: %m"); + + for (size_t i = 0; i < n_items; i++) { + _cleanup_free_ char *content = NULL; + size_t contentsize = 0; /* avoid false maybe-uninitialized warning */ + + err = file_read(extra_dir, items[i], 0, 0, &content, &contentsize); + if (err != EFI_SUCCESS) { + log_error_status(err, "Failed to read %ls, ignoring: %m", items[i]); + continue; + } + + err = pack_cpio_one( + items[i], + content, contentsize, + target_dir_prefix, + access_mode, + &inode, + &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio file %ls: %m", dirent->FileName); + } + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio trailer: %m"); + + err = tpm_log_event( + tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m", + tpm_pcr, + tpm_description); + + *ret_buffer = TAKE_PTR(buffer); + *ret_buffer_size = buffer_size; + + return EFI_SUCCESS; + +nothing: + *ret_buffer = NULL; + *ret_buffer_size = 0; + + if (ret_measured) + *ret_measured = false; + + return EFI_SUCCESS; +} + +EFI_STATUS pack_cpio_literal( + const void *data, + size_t data_size, + const char *target_dir_prefix, + const char16_t *target_filename, + uint32_t dir_mode, + uint32_t access_mode, + uint32_t tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + size_t *ret_buffer_size, + bool *ret_measured) { + + uint32_t inode = 1; /* inode counter, so that each item gets a new inode */ + _cleanup_free_ void *buffer = NULL; + size_t buffer_size = 0; + EFI_STATUS err; + + assert(data || data_size == 0); + assert(target_dir_prefix); + assert(target_filename); + assert(ret_buffer); + assert(ret_buffer_size); + + /* 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 (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio prefix: %m"); + + err = pack_cpio_one( + target_filename, + data, data_size, + target_dir_prefix, + access_mode, + &inode, + &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio file %ls: %m", target_filename); + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to pack cpio trailer: %m"); + + err = tpm_log_event( + tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return log_error_status( + err, + "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m", + tpm_pcr, + tpm_description); + + *ret_buffer = TAKE_PTR(buffer); + *ret_buffer_size = buffer_size; + + return EFI_SUCCESS; +} |