diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index bb0376ed865..9f87f19f4cc 100644 --- a/man/systemd-stub.xml +++ b/man/systemd-stub.xml @@ -53,13 +53,14 @@ The UEFI boot stub looks for various resources for the kernel invocation inside the UEFI PE binary itself. This allows combining various resources inside a single PE binary image (usually called "Unified Kernel Image", or "UKI" for short), which may then be signed via UEFI SecureBoot as a whole, covering all - individual resources at once. Specifically it may include: + individual resources at once. Specifically it may include the following PE sections: - A .linux section with the ELF Linux kernel image. + A .linux section with the ELF Linux kernel + image. (Required) An .osrel section with OS release information, i.e. the contents of the os-release5 file @@ -95,21 +96,29 @@ signature data in the .pcrsig section. - If UEFI SecureBoot is enabled and the .cmdline section is present in the executed - image, any attempts to override the kernel command line by passing one as invocation parameters to the - EFI binary are ignored. Thus, in order to allow overriding the kernel command line, either disable UEFI - SecureBoot, or don't include a kernel command line PE section in the kernel image file. If a command line - is accepted via EFI invocation parameters to the EFI binary it is measured into TPM PCR 12 (if a TPM is - present). + Generally, the sections above should appear at most once in a UKI. That said, a concept of + "profiles" is defined, that allows multiple sets of these sections to exist in a single UKI file, of + which one can be selected at boot. For this an additional PE section .profile is + defined which can be used as separator between multiple sets of these settings. The + .profile section itself may contain meta-information about the section, and follows a + similar structure as the contents of the .osrel section. For further details about + multi-profile UKIs, see below. If UEFI SecureBoot is enabled and the + .cmdline section is present in the executed image, any attempts to override the kernel + command line by passing one as invocation parameters to the EFI binary are ignored. Thus, in order to + allow overriding the kernel command line, either disable UEFI SecureBoot, or don't include a kernel + command line PE section in the kernel image file. If a command line is accepted via EFI invocation + parameters to the EFI binary it is measured into TPM PCR 12 (if a TPM is present). If a + DeviceTree is embedded in the .dtb section, it replaces an existing DeviceTree in the + corresponding EFI configuration table. systemd-stub will ask the firmware via the + EFI_DT_FIXUP_PROTOCOL for hardware specific fixups to the DeviceTree. The + contents of 11 of these 12 sections are measured into TPM PCR 11. It is otherwise not used and thus the + result can be pre-calculated without too much effort. The .pcrsig section is not + included in this PCR measurement, since it is supposed to contain signatures for the output of the + measurement operation, and thus cannot also be input to it. If an UKI contains multiple profiles, only + the PE sections of the selected profile (and those of the base profile, except if overriden) are + measured. - If a DeviceTree is embedded in the .dtb section, it replaces an existing - DeviceTree in the corresponding EFI configuration table. systemd-stub will ask the firmware via the - EFI_DT_FIXUP_PROTOCOL for hardware specific fixups to the DeviceTree. - - The contents of eight of these nine sections are measured into TPM PCR 11. It is otherwise not used - and thus the result can be pre-calculated without too much effort. The .pcrsig section - is not included in this PCR measurement, since it is supposed to contain signatures for the output of the - measurement operation, and thus cannot also be input to it. + If non-zero, the selected numeric profile is measured into PCR 12. When .pcrsig and/or .pcrpkey sections are present in a unified kernel image their contents are passed to the booted kernel in an synthetic initrd cpio archive @@ -231,19 +240,124 @@ details); in case of the system extension images by using signed Verity images. + + Multi-Profile UKIs + + In many contexts it is useful to allow invocation of a single UKI in multiple different modes (or + "profiles") without compromising the cryptographic integrity, measurements and so on of the boot + process. For example, a single UKI might provide three distinct profiles: a regular boot one, one that + invokes a "factory reset" operation, and one that boots into a storage target mode (as in + systemd-storagetm.service8). Each + profile would then use the same .linux and .initrd sections, but would + have a separate .cmdline section. For example the latter two profiles would extend the + regular kernel command line with systemd.unit=factory-reset.target or + rd.systemd.unit=storagetm.target. + + A single UKI may support multiple profiles by means of the special .profile PE + section. This section acts as separator between the PE sections of the individual + profiles. .profile PE sections hence may appear multiple times in a single UKI, and + the other PE sections listed above may appear multiple times too, if .profile are + used, but only once before the first .profile section, once between each subsequent + pair, and once after the last appearance of .profile. The sections listed before the + first .profile are considered the "base" profile of the UKI. Each + .profile section then introduces a new profile, which are numbered starting from + zero. The PE sections following each .profile are specific to that profile. When + booting into a specific profile the base section's profiles are used in combination with the specific + profile's sections: if the same section is defined in both, the per-profile section overrides the base + profile's version, otherwise the per-profile sections is used together with the base profile + sections. A UKI that contains no .profile is consider equivalent to one + that just contains a single .profile, as having only a single profile @0. + + Here's a simple example for a multi-profile UKI's sections, inspired by the setup suggested above: + + + Multi-Profile UKI Example + + + + + + + + Section + Profile + + + + + + .linux + Base profile + + + .osrel + + + .cmdline + + + .initrd + + + .profile + Profile @0 + + + .profile + Profile @1 + + + .cmdline + + + .profile + Profile @2 + + + .cmdline + + + +
+ + The section list above would define three profiles. The first four sections make up the base + profile. A .profile section then introduces profile @0. It doesn't override any + sections (or add any) from the base section, hence it is immediately followed by another + .profile section that then introduces section @1. This profile overrides the kernel + command line. Finally, the last two sections define section @2, again overriding the command line. (Note + that in this example the first .cmdline could also moved behind the first + .profile with equivalent effect. To keep things nicely extensible, it's probably a + good idea to keep the generic command line in the base section instead of profile 0, in case later added + profiles might want to reuse it.) + + The profile to boot may be controlled via the UKI's own command line: if the first argument starts + with @, followed by a positive integer number in decimal, it selects the profile to + boot into. If the first argument is not specified like that, the UKI will automatically boot into profile + 0. + + A .profile section may contain meta-information about the profile. It follows a + similar format as .osrel (i.e. an environment-variable-assignment-block-like list of + newline separated strings). Currently two fields are defined: ID= is supposed to carry + a short identifying string that identifies the profile + (e.g. ID=factory-reset). TITLE= should contain a human readable + string that may appear in the boot menu entry for this profile (e.g. TITLE='Factory Reset this + Device'). +
+ TPM PCR Notes Note that when a unified kernel using systemd-stub is invoked the firmware will measure it as a whole to TPM PCR 4, covering all embedded resources, such as the stub code itself, the - core kernel, the embedded initrd and kernel command line (see above for a full list). + core kernel, the embedded initrd and kernel command line (see above for a full list), including all UKI + profiles. Also note that the Linux kernel will measure all initrds it receives into TPM PCR 9. This means - every type of initrd will be measured two or three times: the initrds embedded in the kernel image will be - measured to PCR 4, PCR 9 and PCR 11; the initrd synthesized from credentials (and the one synthesized - from configuration extensions) will be measured to both PCR 9 and PCR 12; the initrd synthesized from - system extensions will be measured to both PCR 4 and PCR 9. Let's summarize the OS resources and the PCRs - they are measured to: + every type of initrd (of the selected UKI profile) will possibly be measured two or three times: the + initrds embedded in the kernel image will be measured to PCR 4, PCR 9 and PCR 11; the initrd synthesized + from credentials (and the one synthesized from configuration extensions) will be measured to both PCR 9 + and PCR 12; the initrd synthesized from system extensions will be measured to both PCR 4 and PCR 9. Let's + summarize the OS resources and the PCRs they are measured to: OS Resource PCR Summary @@ -324,6 +438,11 @@ Configuration Extensions (synthesized initrd from companion files)9 + 12 + + + Selected profile unless zero + 12 +
@@ -422,6 +541,16 @@ + + + StubProfile + + The numeric index of the selected profile, without the @, + formatted as decimal string. Set both on single-profile and multi-profile UKIs. (In the former case + this variable will be set to 0 unconditionally.) + + + Note that some of the variables above may also be set by the boot loader. The stub will only set @@ -500,6 +629,15 @@ + + + /.extra/profile + /.extra/os-release + The contents of the .profile and .osrel + sections of the selected profile, if any. + + + Note that all these files are located in the tmpfs file system the kernel sets diff --git a/src/boot/efi/pe.c b/src/boot/efi/pe.c index 5817806d72b..7eb8c4fa202 100644 --- a/src/boot/efi/pe.c +++ b/src/boot/efi/pe.c @@ -106,19 +106,6 @@ typedef struct PeFileHeader { PeOptionalHeader OptionalHeader; } _packed_ PeFileHeader; -typedef struct PeSectionHeader { - uint8_t Name[8]; - uint32_t VirtualSize; - uint32_t VirtualAddress; - uint32_t SizeOfRawData; - uint32_t PointerToRawData; - uint32_t PointerToRelocations; - uint32_t PointerToLinenumbers; - uint16_t NumberOfRelocations; - uint16_t NumberOfLinenumbers; - uint32_t Characteristics; -} _packed_ PeSectionHeader; - #define SECTION_TABLE_BYTES_MAX (16U * 1024U * 1024U) static bool verify_dos(const DosFileHeader *dos) { @@ -188,7 +175,7 @@ static void pe_locate_sections( /* Searches for the sections listed in 'sections[]' within the section table. Validates the resulted * data. If 'validate_base' is non-zero also takes base offset when loaded into memory into account for - * qchecking for overflows. */ + * checking for overflows. */ for (size_t i = 0; section_names[i]; i++) FOREACH_ARRAY(j, section_table, n_section_table) { @@ -313,31 +300,49 @@ EFI_STATUS pe_kernel_info(const void *base, uint32_t *ret_compat_address, size_t return EFI_SUCCESS; } +EFI_STATUS pe_section_table_from_base( + const void *base, + const PeSectionHeader **ret_section_table, + size_t *ret_n_section_table) { + + assert(base); + assert(ret_section_table); + assert(ret_n_section_table); + + const DosFileHeader *dos = (const DosFileHeader*) base; + if (!verify_dos(dos)) + return EFI_LOAD_ERROR; + + const PeFileHeader *pe = (const PeFileHeader*) ((const uint8_t*) base + dos->ExeHeader); + if (!verify_pe(dos, pe, /* allow_compatibility= */ false)) + return EFI_LOAD_ERROR; + + *ret_section_table = (const PeSectionHeader*) ((const uint8_t*) base + section_table_offset(dos, pe)); + *ret_n_section_table = pe->FileHeader.NumberOfSections; + + return EFI_SUCCESS; +} + EFI_STATUS pe_memory_locate_sections( const void *base, const char *const section_names[], PeSectionVector sections[]) { - const DosFileHeader *dos; - const PeFileHeader *pe; - size_t offset; + EFI_STATUS err; assert(base); assert(section_names); assert(sections); - dos = (const DosFileHeader *) base; - if (!verify_dos(dos)) - return EFI_LOAD_ERROR; + const PeSectionHeader *section_table; + size_t n_section_table; + err = pe_section_table_from_base(base, §ion_table, &n_section_table); + if (err != EFI_SUCCESS) + return err; - pe = (const PeFileHeader *) ((const uint8_t *) base + dos->ExeHeader); - if (!verify_pe(dos, pe, /* allow_compatibility= */ false)) - return EFI_LOAD_ERROR; - - offset = section_table_offset(dos, pe); pe_locate_sections( - (const PeSectionHeader *) ((const uint8_t *) base + offset), - pe->FileHeader.NumberOfSections, + section_table, + n_section_table, section_names, PTR_TO_SIZE(base), sections); @@ -345,27 +350,19 @@ EFI_STATUS pe_memory_locate_sections( return EFI_SUCCESS; } -EFI_STATUS pe_file_locate_sections( - EFI_FILE *dir, - const char16_t *path, - const char *const section_names[], - PeSectionVector sections[]) { - _cleanup_free_ PeSectionHeader *section_table = NULL; - _cleanup_(file_closep) EFI_FILE *handle = NULL; - DosFileHeader dos; - PeFileHeader pe; - size_t len, section_table_len; +EFI_STATUS pe_section_table_from_file( + EFI_FILE *handle, + PeSectionHeader **ret_section_table, + size_t *ret_n_section_table) { + EFI_STATUS err; + size_t len; - assert(dir); - assert(path); - assert(section_names); - assert(sections); - - err = dir->Open(dir, &handle, (char16_t *) path, EFI_FILE_MODE_READ, 0ULL); - if (err != EFI_SUCCESS) - return err; + assert(handle); + assert(ret_section_table); + assert(ret_n_section_table); + DosFileHeader dos; len = sizeof(dos); err = handle->Read(handle, &len, &dos); if (err != EFI_SUCCESS) @@ -377,6 +374,7 @@ EFI_STATUS pe_file_locate_sections( if (err != EFI_SUCCESS) return err; + PeFileHeader pe; len = sizeof(pe); err = handle->Read(handle, &len, &pe); if (err != EFI_SUCCESS) @@ -388,10 +386,11 @@ EFI_STATUS pe_file_locate_sections( if ((size_t) pe.FileHeader.NumberOfSections > SIZE_MAX / sizeof(PeSectionHeader)) return EFI_OUT_OF_RESOURCES; REENABLE_WARNING; - section_table_len = (size_t) pe.FileHeader.NumberOfSections * sizeof(PeSectionHeader); - if (section_table_len > SECTION_TABLE_BYTES_MAX) + size_t n_section_table = (size_t) pe.FileHeader.NumberOfSections; + if (n_section_table * sizeof(PeSectionHeader) > SECTION_TABLE_BYTES_MAX) return EFI_OUT_OF_RESOURCES; - section_table = xmalloc(section_table_len); + + _cleanup_free_ PeSectionHeader *section_table = xnew(PeSectionHeader, n_section_table); if (!section_table) return EFI_OUT_OF_RESOURCES; @@ -399,19 +398,138 @@ EFI_STATUS pe_file_locate_sections( if (err != EFI_SUCCESS) return err; - len = section_table_len; + len = n_section_table * sizeof(PeSectionHeader); err = handle->Read(handle, &len, section_table); if (err != EFI_SUCCESS) return err; - if (len != section_table_len) + if (len != n_section_table * sizeof(PeSectionHeader)) return EFI_LOAD_ERROR; + *ret_section_table = TAKE_PTR(section_table); + *ret_n_section_table = n_section_table; + return EFI_SUCCESS; +} + +EFI_STATUS pe_file_locate_sections( + EFI_FILE *dir, + const char16_t *path, + const char* const section_names[], + PeSectionVector sections[]) { + + _cleanup_free_ PeSectionHeader *section_table = NULL; + _cleanup_(file_closep) EFI_FILE *handle = NULL; + size_t n_section_table; + EFI_STATUS err; + + assert(dir); + assert(path); + assert(section_names); + assert(sections); + + err = dir->Open(dir, &handle, (char16_t *) path, EFI_FILE_MODE_READ, 0ULL); + if (err != EFI_SUCCESS) + return err; + + err = pe_section_table_from_file(handle, §ion_table, &n_section_table); + if (err != EFI_SUCCESS) + return err; + pe_locate_sections( section_table, - pe.FileHeader.NumberOfSections, + n_section_table, section_names, /* validate_base= */ 0, /* don't validate base */ sections); return EFI_SUCCESS; } + +static const PeSectionHeader* pe_section_table_find_profile_start( + const PeSectionHeader *section_table, + size_t n_section_table, + unsigned profile) { + + assert(section_table || n_section_table == 0); + + if (profile == UINT_MAX) /* base profile? that starts at the beginning */ + return section_table; + + unsigned current_profile = UINT_MAX; + FOREACH_ARRAY(p, section_table, n_section_table) { + + if (!pe_section_name_equal((const char*) p->Name, ".profile")) + continue; + + if (current_profile == UINT_MAX) + current_profile = 0; + else + current_profile++; + + if (current_profile == profile) /* Found our profile! */ + return p; + } + + /* We reached the end of the table? Then this section does not exist */ + return NULL; +} + +static size_t pe_section_table_find_profile_length( + const PeSectionHeader *section_table, + size_t n_section_table, + const PeSectionHeader *start, + unsigned profile) { + + assert(section_table); + assert(n_section_table > 0); + assert(start >= section_table); + assert(start < section_table + n_section_table); + + /* Look for the next .profile (or the end of the table), this is where the the sections for this + * profile end. The base profile does not start with a .profile, the others do, hence conditionally + * skip over the first entry. */ + const PeSectionHeader *e; + if (profile == UINT_MAX) /* Base profile */ + e = start; + else { + assert(pe_section_name_equal((const char *) start->Name, ".profile")); + e = start + 1; + } + + for (; e < section_table + n_section_table; e++) + if (pe_section_name_equal((const char*) e->Name, ".profile")) + return e - start; + + return (section_table + n_section_table) - start; +} + +EFI_STATUS pe_locate_profile_sections( + const PeSectionHeader section_table[], + size_t n_section_table, + const char* const section_names[], + unsigned profile, + size_t validate_base, + PeSectionVector sections[]) { + + assert(section_table || n_section_table == 0); + assert(section_names); + assert(sections); + + /* Now scan through the section table until we skipped over the right number of .profile sections */ + const PeSectionHeader *p = pe_section_table_find_profile_start(section_table, n_section_table, profile); + if (!p) + return EFI_NOT_FOUND; + + /* Look for the next .profile (or the end of the table), this is where the the sections for this + * profile end. */ + size_t n = pe_section_table_find_profile_length(section_table, n_section_table, p, profile); + + /* And now parse everything between the start and end of our profile */ + pe_locate_sections( + p, + n, + section_names, + validate_base, + sections); + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/pe.h b/src/boot/efi/pe.h index 863f870b9f1..31efbebbeb6 100644 --- a/src/boot/efi/pe.h +++ b/src/boot/efi/pe.h @@ -3,6 +3,20 @@ #include "efi.h" +/* This is the actual PE format of the section header*/ +typedef struct PeSectionHeader { + uint8_t Name[8]; + uint32_t VirtualSize; + uint32_t VirtualAddress; + uint32_t SizeOfRawData; + uint32_t PointerToRawData; + uint32_t PointerToRelocations; + uint32_t PointerToLinenumbers; + uint16_t NumberOfRelocations; + uint16_t NumberOfLinenumbers; + uint32_t Characteristics; +} _packed_ PeSectionHeader; + /* This is a subset of the full PE section header structure, with validated values, and without * the noise. */ typedef struct PeSectionVector { @@ -15,6 +29,24 @@ static inline bool PE_SECTION_VECTOR_IS_SET(const PeSectionVector *v) { return v && v->size != 0; } +EFI_STATUS pe_section_table_from_base( + const void *base, + const PeSectionHeader **ret_section_table, + size_t *ret_n_section_table); + +EFI_STATUS pe_section_table_from_file( + EFI_FILE *handle, + PeSectionHeader **ret_section_table, + size_t *ret_n_section_table); + +EFI_STATUS pe_locate_profile_sections( + const PeSectionHeader section_table[], + size_t n_section_table, + const char* const section_names[], + unsigned profile, + size_t validate_base, + PeSectionVector sections[]); + EFI_STATUS pe_memory_locate_sections( const void *base, const char *const section_names[], diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c index 7f8671a9cf7..0ace97013f0 100644 --- a/src/boot/efi/stub.c +++ b/src/boot/efi/stub.c @@ -38,6 +38,8 @@ enum { INITRD_CONFEXT, INITRD_PCRSIG, INITRD_PCRPKEY, + INITRD_OSREL, + INITRD_PROFILE, _INITRD_MAX, }; @@ -139,7 +141,7 @@ static EFI_STATUS combine_initrds( return EFI_SUCCESS; } -static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) { +static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image, unsigned profile) { static const uint64_t stub_features = EFI_STUB_FEATURE_REPORT_BOOT_PARTITION | /* We set LoaderDevicePartUUID */ EFI_STUB_FEATURE_PICK_UP_CREDENTIALS | /* We pick up credentials from the boot partition */ @@ -150,6 +152,7 @@ static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) { EFI_STUB_FEATURE_CMDLINE_ADDONS | /* We pick up .cmdline addons */ EFI_STUB_FEATURE_CMDLINE_SMBIOS | /* We support extending kernel cmdline from SMBIOS Type #11 */ EFI_STUB_FEATURE_DEVICETREE_ADDONS | /* We pick up .dtb addons */ + EFI_STUB_FEATURE_MULTI_PROFILE_UKI | /* We grok the "@1" profile command line argument */ 0; assert(loaded_image); @@ -159,23 +162,76 @@ static void export_stub_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) { (void) efivar_set_str16(MAKE_GUID_PTR(LOADER), u"StubInfo", u"systemd-stub " GIT_VERSION, 0); (void) efivar_set_uint64_le(MAKE_GUID_PTR(LOADER), u"StubFeatures", stub_features, 0); + + (void) efivar_set_uint64_str16(MAKE_GUID_PTR(LOADER), u"StubProfile", profile, 0); } -static bool use_load_options( +static bool parse_profile_from_cmdline(char16_t **cmdline, unsigned *ret_profile) { + assert(cmdline); + assert(*cmdline); + assert(ret_profile); + + const char16_t *p = *cmdline; + if (p[0] != '@') + goto nothing; + + uint64_t u; + const char16_t *tail; + if (!parse_number16(p + 1, &u, &tail)) + goto nothing; + if (u > UINT_MAX) + goto nothing; + /* Remove exactly one separating space. No further mangling, in order to not disturb measurements – + * and thus making prediction harder –, after all we want that people can safely prefix their command + * lines with a profile without having to be bothered with additional whitespace the command line + * might already contain. */ + if (tail[0] == u' ') + tail++; + else if (tail[0] != 0) /* If this is neither a space nor the end of the string, it must be something else */ + goto nothing; + + /* Drop prefix */ + free_and_xstrdup16(cmdline, tail); + *ret_profile = u; + return true; + +nothing: + *ret_profile = 0; + return false; +} + +static bool parse_profile_from_argument(const char16_t *arg, unsigned *ret_profile) { + assert(arg); + assert(ret_profile); + + if (arg[0] != '@') + goto nothing; + + uint64_t u; + if (!parse_number16(arg + 1, &u, /* ret_tail= */ NULL)) + goto nothing; + + if (u > UINT_MAX) + goto nothing; + + *ret_profile = u; + return true; + +nothing: + *ret_profile = 0; + return false; +} + +static void process_arguments( EFI_HANDLE stub_image, EFI_LOADED_IMAGE_PROTOCOL *loaded_image, - bool have_cmdline, - char16_t **ret) { + unsigned *ret_profile, + char16_t **ret_cmdline) { assert(stub_image); assert(loaded_image); - assert(ret); - - /* We only allow custom command lines if we aren't in secure boot or if no cmdline was baked into - * the stub image. - * We also don't allow it if we are in confidential vms and secureboot is on. */ - if (secure_boot_enabled() && (have_cmdline || is_confidential_vm())) - return false; + assert(ret_profile); + assert(ret_cmdline); /* The UEFI shell registers EFI_SHELL_PARAMETERS_PROTOCOL onto images it runs. This lets us know that * LoadOptions starts with the stub binary path which we want to strip off. */ @@ -185,26 +241,40 @@ static bool use_load_options( /* We also do a superficial check whether first character of passed command line * is printable character (for compat with some Dell systems which fill in garbage?). */ if (loaded_image->LoadOptionsSize < sizeof(char16_t) || ((const char16_t *) loaded_image->LoadOptions)[0] <= 0x1F) - return false; + goto nothing; /* Not running from EFI shell, use entire LoadOptions. Note that LoadOptions is a void*, so - * it could be anything! */ - *ret = mangle_stub_cmdline(xstrndup16(loaded_image->LoadOptions, loaded_image->LoadOptionsSize / sizeof(char16_t))); - return true; + * it could actually be anything! */ + char16_t *c = xstrndup16(loaded_image->LoadOptions, loaded_image->LoadOptionsSize / sizeof(char16_t)); + parse_profile_from_cmdline(&c, ret_profile); + *ret_cmdline = mangle_stub_cmdline(c); + return; } - if (shell->Argc < 2) - /* No arguments were provided? Then we fall back to built-in cmdline. */ - return false; + if (shell->Argc <= 1) /* No arguments were provided? Then we fall back to built-in cmdline. */ + goto nothing; - /* Assemble the command line ourselves without our stub path. */ - *ret = xstrdup16(shell->Argv[1]); - for (size_t i = 2; i < shell->Argc; i++) { - _cleanup_free_ char16_t *old = *ret; - *ret = xasprintf("%ls %ls", old, shell->Argv[i]); - } + size_t i = 1; - return true; + /* The first argument is possibly an "@5" style profile specifier */ + i += parse_profile_from_argument(shell->Argv[i], ret_profile); + + if (i < shell->Argc) { + /* Assemble the command line ourselves without our stub path. */ + *ret_cmdline = xstrdup16(shell->Argv[i++]); + for (; i < shell->Argc; i++) { + _cleanup_free_ char16_t *old = *ret_cmdline; + *ret_cmdline = xasprintf("%ls %ls", old, shell->Argv[i]); + } + } else + *ret_cmdline = NULL; + + return; + +nothing: + *ret_profile = 0; + *ret_cmdline = NULL; + return; } static EFI_STATUS load_addons_from_dir( @@ -734,43 +804,53 @@ static void generate_embedded_initrds( const PeSectionVector sections[static _UNIFIED_SECTION_MAX], struct iovec initrds[static _INITRD_MAX]) { + static const struct { + UnifiedSection section; + size_t initrd_index; + const char16_t *filename; + } table[] = { + /* If the PCR signature was embedded in the PE image, then let's wrap it in a cpio and also pass it + * to the kernel, so that it can be read from /.extra/tpm2-pcr-signature.json. Note that this section + * is not measured, neither as raw section (see above), nor as cpio (here), because it is the + * signature of expected PCR values, i.e. its input are PCR measurements, and hence it shouldn't + * itself be input for PCR measurements. */ + { UNIFIED_SECTION_PCRSIG, INITRD_PCRSIG, u"tpm2-pcr-signature.json" }, + + /* If the public key used for the PCR signatures was embedded in the PE image, then let's + * wrap it in a cpio and also pass it to the kernel, so that it can be read from + * /.extra/tpm2-pcr-public-key.pem. This section is already measured above, hence we won't + * measure the cpio. */ + { UNIFIED_SECTION_PCRPKEY, INITRD_PCRPKEY, u"tpm2-pcr-public-key.pem" }, + + /* If we boot a specific profile, let's place the chosen profile in a file that userspace can + * make use of this information reasonably. */ + { UNIFIED_SECTION_PROFILE, INITRD_PROFILE, u"profile" }, + + /* Similar, pass the .osrel section too. Userspace should have this information anyway, but + * it's so nicely symmetric to the .profile section which we pass around, and who knows, + * maybe this is useful to some. */ + { UNIFIED_SECTION_OSREL, INITRD_OSREL, u"os-release" }, + }; + assert(loaded_image); assert(initrds); - /* If the PCR signature was embedded in the PE image, then let's wrap it in a cpio and also pass it - * to the kernel, so that it can be read from /.extra/tpm2-pcr-signature.json. Note that this section - * is not measured, neither as raw section (see above), nor as cpio (here), because it is the - * signature of expected PCR values, i.e. its input are PCR measurements, and hence it shouldn't - * itself be input for PCR measurements. */ - if (PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_PCRSIG)) - (void) pack_cpio_literal( - (const uint8_t*) loaded_image->ImageBase + sections[UNIFIED_SECTION_PCRSIG].memory_offset, - sections[UNIFIED_SECTION_PCRSIG].size, - ".extra", - u"tpm2-pcr-signature.json", - /* dir_mode= */ 0555, - /* access_mode= */ 0444, - /* tpm_pcr= */ UINT32_MAX, - /* tpm_description= */ NULL, - initrds + INITRD_PCRSIG, - /* ret_measured= */ NULL); + FOREACH_ELEMENT(t, table) { + if (!PE_SECTION_VECTOR_IS_SET(sections + t->section)) + continue; - /* If the public key used for the PCR signatures was embedded in the PE image, then let's wrap it in - * a cpio and also pass it to the kernel, so that it can be read from - * /.extra/tpm2-pcr-public-key.pem. This section is already measure above, hence we won't measure the - * cpio. */ - if (PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_PCRPKEY)) (void) pack_cpio_literal( - (const uint8_t*) loaded_image->ImageBase + sections[UNIFIED_SECTION_PCRPKEY].memory_offset, - sections[UNIFIED_SECTION_PCRPKEY].size, + (const uint8_t*) loaded_image->ImageBase + sections[t->section].memory_offset, + sections[t->section].size, ".extra", - u"tpm2-pcr-public-key.pem", + t->filename, /* dir_mode= */ 0555, /* access_mode= */ 0444, /* tpm_pcr= */ UINT32_MAX, /* tpm_description= */ NULL, - initrds + INITRD_PCRPKEY, + initrds + t->initrd_index, /* ret_measured= */ NULL); + } } static void lookup_embedded_initrds( @@ -898,26 +978,103 @@ static void display_splash( graphics_splash((const uint8_t*) loaded_image->ImageBase + sections[UNIFIED_SECTION_SPLASH].memory_offset, sections[UNIFIED_SECTION_SPLASH].size); } -static void determine_cmdline( - EFI_HANDLE image, +static EFI_STATUS find_sections( EFI_LOADED_IMAGE_PROTOCOL *loaded_image, - const PeSectionVector sections[static _UNIFIED_SECTION_MAX], - char16_t **ret_cmdline, - int *parameters_measured) { + unsigned profile, + PeSectionVector sections[static _UNIFIED_SECTION_MAX]) { + + EFI_STATUS err; assert(loaded_image); assert(sections); - if (use_load_options(image, loaded_image, /* have_cmdline= */ PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_CMDLINE), ret_cmdline)) { - /* Let's measure the passed kernel command line into the TPM. Note that this possibly - * duplicates what we already did in the boot menu, if that was already used. However, since - * we want the boot menu to support an EFI binary, and want to this stub to be usable from - * any boot menu, let's measure things anyway. */ - bool m = false; - (void) tpm_log_load_options(*ret_cmdline, &m); - combine_measured_flag(parameters_measured, m); - } else - *ret_cmdline = mangle_stub_cmdline(pe_section_to_str16(loaded_image, sections + UNIFIED_SECTION_CMDLINE)); + const PeSectionHeader *section_table; + size_t n_section_table; + err = pe_section_table_from_base(loaded_image->ImageBase, §ion_table, &n_section_table); + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to locate PE section table: %m"); + + /* Get the base sections */ + err = pe_locate_profile_sections( + section_table, + n_section_table, + unified_sections, + /* profile= */ UINT_MAX, + /* validate_base= */ PTR_TO_SIZE(loaded_image->ImageBase), + sections); + if (err != EFI_SUCCESS) + return log_error_status(err, "Unable to locate embedded base PE sections: %m"); + + if (profile != UINT_MAX) { + /* And then override them with the per-profile sections of the selected profile */ + err = pe_locate_profile_sections( + section_table, + n_section_table, + unified_sections, + profile, + /* validate_base= */ PTR_TO_SIZE(loaded_image->ImageBase), + sections); + if (err != EFI_SUCCESS && !(err == EFI_NOT_FOUND && profile == 0)) /* the first profile is implied if it doesn't exist */ + return log_error_status(err, "Unable to locate embedded per-profile PE sections: %m"); + } + + if (!PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_LINUX)) + return log_error_status(EFI_NOT_FOUND, "Image lacks .linux section."); + + return EFI_SUCCESS; +} + +static void settle_command_line( + EFI_LOADED_IMAGE_PROTOCOL *loaded_image, + const PeSectionVector sections[static _UNIFIED_SECTION_MAX], + char16_t **cmdline, + int *parameters_measured) { + + assert(loaded_image); + assert(sections); + assert(cmdline); + + /* This determines which command line to use. On input *cmdline contains the custom passed in cmdline + * if there is any. + * + * We'll suppress the custom cmdline if we are in Secure Boot mode, and if either there is already + * a cmdline baked into the UKI or we are in confidential VM mode. */ + + if (!isempty(*cmdline)) { + if (secure_boot_enabled() && (PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_CMDLINE) || is_confidential_vm())) + /* Drop the custom cmdline */ + *cmdline = mfree(*cmdline); + else { + /* Let's measure the passed kernel command line into the TPM. Note that this possibly + * duplicates what we already did in the boot menu, if that was already + * used. However, since we want the boot menu to support an EFI binary, and want to + * this stub to be usable from any boot menu, let's measure things anyway. */ + bool m = false; + (void) tpm_log_load_options(*cmdline, &m); + combine_measured_flag(parameters_measured, m); + } + } + + /* No cmdline specified? Or suppressed? Then let's take the one from the UKI, if there is any. */ + if (isempty(*cmdline)) + *cmdline = mangle_stub_cmdline(pe_section_to_str16(loaded_image, sections + UNIFIED_SECTION_CMDLINE)); +} + +static void measure_profile(unsigned profile, int *parameters_measured) { + if (profile == 0) /* don't measure anything about the default profile */ + return; + + _cleanup_free_ char16_t *s = xasprintf("%u", profile); + + bool m = false; + (void) tpm_log_tagged_event( + TPM2_PCR_KERNEL_CONFIG, + POINTER_TO_PHYSICAL_ADDRESS(s), + strsize16(s), + UKI_PROFILE_EVENT_TAG_ID, + s, + &m); + combine_measured_flag(parameters_measured, m); } static EFI_STATUS run(EFI_HANDLE image) { @@ -932,18 +1089,24 @@ static EFI_STATUS run(EFI_HANDLE image) { size_t n_dt_addons = 0, n_ucode_addons = 0; _cleanup_free_ struct iovec *all_initrds = NULL; size_t n_all_initrds = 0; + unsigned profile = 0; EFI_STATUS err; 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 = pe_memory_locate_sections(loaded_image->ImageBase, unified_sections, sections); - if (err != EFI_SUCCESS) - return log_error_status(err, "Unable to locate embedded PE sections: %m"); - if (!PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_LINUX)) - return log_error_status(EFI_NOT_FOUND, "Image lacks .linux section."); + /* Pick up the arguments passed to us, split out the prefixing profile parameter, and return the rest + * as potential command line to use. */ + (void) process_arguments(image, loaded_image, &profile, &cmdline); + /* Find the sections we want to operate on, both the basic ones, and the one appropriate for the + * selected profile. */ + err = find_sections(loaded_image, profile, sections); + if (err != EFI_SUCCESS) + return err; + + measure_profile(profile, ¶meters_measured); measure_sections(loaded_image, sections, §ions_measured); /* Show splash screen as early as possible, but after measuring it */ @@ -953,7 +1116,8 @@ static EFI_STATUS run(EFI_HANDLE image) { uname = pe_section_to_str8(loaded_image, sections + UNIFIED_SECTION_UNAME); - determine_cmdline(image, loaded_image, sections, &cmdline, ¶meters_measured); + /* Let's now check if we actually want to use the command line, measure it if it was passed in. */ + settle_command_line(loaded_image, sections, &cmdline, ¶meters_measured); /* Now that we have the UKI sections loaded, also load global first and then local (per-UKI) * addons. The data is loaded at once, and then used later. */ @@ -969,7 +1133,7 @@ static EFI_STATUS run(EFI_HANDLE image) { cmdline_append_and_measure_smbios(&cmdline, ¶meters_measured); export_common_variables(loaded_image); - export_stub_variables(loaded_image); + export_stub_variables(loaded_image, profile); /* First load the base device tree, then fix it up using addons - global first, then per-UKI. */ install_embedded_devicetree(loaded_image, sections, &dt_state); diff --git a/src/boot/efi/util.c b/src/boot/efi/util.c index ae605bf5a50..f5f748bc6cf 100644 --- a/src/boot/efi/util.c +++ b/src/boot/efi/util.c @@ -492,3 +492,24 @@ void *xmalloc(size_t size) { assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS); return p; } + +bool free_and_xstrdup16(char16_t **p, const char16_t *s) { + char16_t *t; + + assert(p); + + /* Replaces a string pointer with a strdup()ed new string, + * possibly freeing the old one. */ + + if (streq_ptr(*p, s)) + return false; + + if (s) + t = xstrdup16(s); + else + t = NULL; + + free(*p); + *p = t; + return true; +} diff --git a/src/boot/efi/util.h b/src/boot/efi/util.h index d8d695ea795..50025a34a83 100644 --- a/src/boot/efi/util.h +++ b/src/boot/efi/util.h @@ -55,6 +55,8 @@ static inline void* xmemdup(const void *p, size_t l) { #define xnew(type, n) ((type *) xmalloc_multiply((n), sizeof(type))) +bool free_and_xstrdup16(char16_t **p, const char16_t *s); + typedef struct { EFI_PHYSICAL_ADDRESS addr; size_t n_pages; diff --git a/src/fundamental/efivars-fundamental.h b/src/fundamental/efivars-fundamental.h index 01b18ec2416..10afe38c176 100644 --- a/src/fundamental/efivars-fundamental.h +++ b/src/fundamental/efivars-fundamental.h @@ -34,6 +34,7 @@ #define EFI_STUB_FEATURE_CMDLINE_SMBIOS (UINT64_C(1) << 6) #define EFI_STUB_FEATURE_DEVICETREE_ADDONS (UINT64_C(1) << 7) #define EFI_STUB_FEATURE_PICK_UP_CONFEXTS (UINT64_C(1) << 8) +#define EFI_STUB_FEATURE_MULTI_PROFILE_UKI (UINT64_C(1) << 9) typedef enum SecureBootMode { SECURE_BOOT_UNSUPPORTED, diff --git a/src/fundamental/tpm2-pcr.h b/src/fundamental/tpm2-pcr.h index ff957afcd0b..9f5529b8272 100644 --- a/src/fundamental/tpm2-pcr.h +++ b/src/fundamental/tpm2-pcr.h @@ -52,3 +52,6 @@ enum { /* The tag used for EV_EVENT_TAG event log records covering ucode addons (effectively initrds) */ #define UCODE_ADDON_EVENT_TAG_ID UINT32_C(0xdac08e1a) + +/* The tag used for EV_EVENT_TAG event log records covering the selected UKI profile */ +#define UKI_PROFILE_EVENT_TAG_ID UINT32_C(0x13aed6db)