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)