1
0
mirror of https://github.com/systemd/systemd.git synced 2024-12-22 17:35:35 +03:00

stub: add ability to place multiple alternative PE sections of a specific type in the same UKI ("Multi-Profile UKIs")

This adds a ability to add alternative sections of a specific type in
the same UKI. The primary usecase is for supporting multiple different
kernel cmdlines that are baked into a UKI.

The mechanism is relatively simple (I think), in order to make it robust.

1. A new PE section ".profile" is introduced, that is a lot like
   ".osrel", but contains information about a specific "profile" to
   boot. The ".profile" section can appear multiple times in the same
   PE, and acts as delimiter indicating where a new profile starts.
   Everything before the first ".profile" is called the "base profile",
   and is shared among all other profiles, which can then override or
   add addition PE sections on top.

2. An UKI's command line can be prefixed with an argument such as "@0" or
   "@1" or "@2" which indicates the "profile" to boot. If no argument is
   specified the default is profile 0. Also, a UKI that lacks any
   .profile section is treated like one with only a profile 0, but with
   no data in that profile section.

3. The stub will first search for its usual set of PE sections
   (hereafter called "base sections"), and stop at the first .profile PE
   section if any. It will then find the .profile matching the selected
   profile by its index, and any sections found as part of that profile
   on top of the base sections.

And that's already it.

Example: let's say a distro wants to provide a single UKI that can be
invoked in one of three ways:

1. The regular profile that just boots the system
2. A profile that boots into storagetm
3. A profile that initiates factory reset and reboots.

For this it would define a classic UKI with sections .linux, .initrd,
.cmdline, and whatever else it needs. The .cmdline section would contain
the kernel command line for the regular profile.

It would then insert one ".profile" section, with a contents like the
following:

    ID=regular

This is the profile for profile 0. It would immediately afterwards add
another ".profile" section:

    ID=storagetm
    TITLE=Boot into Storage Target Mode

This would then followed with a .cmdline section that is just like the
basic one, but with "rd.systemd.unit=storage-target-mode.target"
suffixed. Then, another .profile section would be added:

    ID=factory-reset
    TITLE=Factory Reset

Which is then followed by one last PE section: a .cmdline one with
"systemd.unit=factory-reset.target" suffixed to te regular command line.

i.e. expressed in tabular form the above would be:

    The base profile:
          .linux
          .initrd
          .cmdline
          .osrel
    The regular boot profile:
          .profile
    The storagetm profile:
          .profile
          .cmdline
    The factory reset profile:
          .profile
          .cmdline

You might wonder why the first .cmdline in the list above is placed in
the base profile rather than in the regular boot profile, given that it
is overriden in all other profiles anyway. And you are right. The only
reason I'd place it in the base profile is that it makes the UKI more
nicely extensible if later profiles are added that want to replace
something else instead of the .cmdline, for example .ucode or so. But it
really doesn't matter much.

While the primary usecase is of course multiple alternative command
lines, the concept is more powerful than that: for various usecases it
might be valuable to offer multiple choices of devicetree, ucode or
initrds.

The .profile contents is also passed to the invoked kernel as a file in
/.extra/profile (via a synthetic initrd). Thus, this functionality can
even be useful without overriding any section at all, simply by means of
reading that file from userspace.

Design choices:

1. On purposes I used a special command line marker (i.e. the "@" thing,
   which maybe we should call the "profile selector"), that doesn't look
   like a regular kernel command line option.  This is because this is
   really not a regular kernel command line option – we process it in
   the stub, then remove it as prefix, and measure the unprefixed
   command line only after that. The kernel will not see the profile
   selector either. I think these special semantics are best
   communicated by making it look substantially different from regular
   options.

2. This moves around measurements a bit. Previously we measured our UKI
   sections right after finding them. Now we first parse the profile
   number from the command line, then search for the profile's sections,
   and only then measure the sections we actually end up using for this
   profile. I think that this logic makes most sense: measure what we
   are using, not what we are overriding. Or in other words, if you boot
   profile @3, then we'll measure .cmdline (assuming it exists) of
   profile 3, and *not* measure .cmdline of the base profile. Also note
   that if the user passes in a custom kernel command line via command
   line arguments we'll strip off the profile selector (i.e. the initial
   "@X" thing) before we pass it on.

3. The .profile stuff is supposed to be generic and extensible. For
   example we could use it in future to mark "dangerous" options such as
   factory reset, so that boot menus can ask for confirmation before
   booting into it. Or we could introduce match expressions against
   SMBIOS or other system identifiers, to filter out profiles on
   specific hw.

Note btw, that PE allows defining multiple sections that point to the
same offsets in the file. This allows sharing payload under different
names. For example, if profile @4 and @7 shall carry the same .ucode
section, they can define .ucode in each profile and then make it point to
the same offset.

Also note that that one can even "mask" a base section in a profile, by
inserting an empty section. For example, if the base .dtb section should
not be used for profile @4, then add a section .dtb right after the
fourth .profile with a zero size to the UKI, and you will get your wish
fulfilled.

This code only contains changes to sd-stub. A follow-up commit will
teach sd-boot to also find this profile PE sections to synthesize
additional menu entries from a single UKI.

A later commit will add support for gnerating this via ukify.

Fixes: #24539
This commit is contained in:
Lennart Poettering 2024-06-27 22:21:58 +02:00
parent f4e081051d
commit a632d8dd9f
4 changed files with 401 additions and 95 deletions

View File

@ -53,13 +53,14 @@
<para>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:</para>
individual resources at once. Specifically it may include the following PE sections:</para>
<itemizedlist>
<!-- Let's keep this in the canonical order we also measure the sections by, i.e. as in
src/fundamental/uki.h's UnifiedSection enum -->
<listitem><para>A <literal>.linux</literal> section with the ELF Linux kernel image.</para></listitem>
<listitem><para>A <literal>.linux</literal> section with the ELF Linux kernel
image. (Required)</para></listitem>
<listitem><para>An <literal>.osrel</literal> section with OS release information, i.e. the contents of
the <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry> file
@ -95,21 +96,29 @@
signature data in the <literal>.pcrsig</literal> section.</para></listitem>
</itemizedlist>
<para>If UEFI SecureBoot is enabled and the <literal>.cmdline</literal> 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).</para>
<para>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 <literal>.profile</literal> is
defined which can be used as separator between multiple sets of these settings. The
<literal>.profile</literal> section itself may contain meta-information about the section, and follows a
similar structure as the contents of the <literal>.osrel</literal> section. For further details about
multi-profile UKIs, see below.</para> <para>If UEFI SecureBoot is enabled and the
<literal>.cmdline</literal> 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).</para> <para>If a
DeviceTree is embedded in the <literal>.dtb</literal> section, it replaces an existing DeviceTree in the
corresponding EFI configuration table. systemd-stub will ask the firmware via the
<literal>EFI_DT_FIXUP_PROTOCOL</literal> for hardware specific fixups to the DeviceTree.</para> <para>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 <literal>.pcrsig</literal> 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.</para>
<para>If a DeviceTree is embedded in the <literal>.dtb</literal> section, it replaces an existing
DeviceTree in the corresponding EFI configuration table. systemd-stub will ask the firmware via the
<literal>EFI_DT_FIXUP_PROTOCOL</literal> for hardware specific fixups to the DeviceTree.</para>
<para>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 <literal>.pcrsig</literal> 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.</para>
<para>If non-zero, the selected numeric profile is measured into PCR 12.</para>
<para>When <literal>.pcrsig</literal> and/or <literal>.pcrpkey</literal> 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.</para>
</refsect1>
<refsect1>
<title>Multi-Profile UKIs</title>
<para>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
<citerefentry><refentrytitle>systemd-storagetm.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>). Each
profile would then use the same <literal>.linux</literal> and <literal>.initrd</literal> sections, but would
have a separate <literal>.cmdline</literal> section. For example the latter two profiles would extend the
regular kernel command line with <literal>systemd.unit=factory-reset.target</literal> or
<literal>rd.systemd.unit=storagetm.target</literal>.</para>
<para>A single UKI may support multiple profiles by means of the special <literal>.profile</literal> PE
section. This section acts as separator between the PE sections of the individual
profiles. <literal>.profile</literal> PE sections hence may appear multiple times in a single UKI, and
the other PE sections listed above may appear multiple times too, if <literal>.profile</literal> are
used, but only once before the first <literal>.profile</literal> section, once between each subsequent
pair, and once after the last appearance of <literal>.profile</literal>. The sections listed before the
first <literal>.profile</literal> are considered the "base" profile of the UKI. Each
<literal>.profile</literal> section then introduces a new profile, which are numbered starting from
zero. The PE sections following each <literal>.profile</literal> 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.</para> <para>A UKI that contains no <literal>.profile</literal> is consider equivalent to one
that just contains a single <literal>.profile</literal>, as having only a single profile @0.</para>
<para>Here's a simple example for a multi-profile UKI's sections, inspired by the setup suggested above:</para>
<table>
<title>Multi-Profile UKI Example</title>
<tgroup cols='2' align='left' colsep='1' rowsep='1'>
<colspec colname="section" />
<colspec colname="profile" />
<thead>
<row>
<entry>Section</entry>
<entry>Profile</entry>
</row>
</thead>
<tbody>
<row>
<entry><literal>.linux</literal></entry>
<entry morerows="3" valign="middle">Base profile</entry>
</row>
<row>
<entry><literal>.osrel</literal></entry>
</row>
<row>
<entry><literal>.cmdline</literal></entry>
</row>
<row>
<entry><literal>.initrd</literal></entry>
</row>
<row>
<entry><literal>.profile</literal></entry>
<entry>Profile @0</entry>
</row>
<row>
<entry><literal>.profile</literal></entry>
<entry morerows="1" valign="middle">Profile @1</entry>
</row>
<row>
<entry><literal>.cmdline</literal></entry>
</row>
<row>
<entry><literal>.profile</literal></entry>
<entry morerows="1" valign="middle">Profile @2</entry>
</row>
<row>
<entry><literal>.cmdline</literal></entry>
</row>
</tbody>
</tgroup>
</table>
<para>The section list above would define three profiles. The first four sections make up the base
profile. A <literal>.profile</literal> 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
<literal>.profile</literal> 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 <literal>.cmdline</literal> could also moved behind the first
<literal>.profile</literal> 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.)</para>
<para>The profile to boot may be controlled via the UKI's own command line: if the first argument starts
with <literal>@</literal>, 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.</para>
<para>A <literal>.profile</literal> section may contain meta-information about the profile. It follows a
similar format as <literal>.osrel</literal> (i.e. an environment-variable-assignment-block-like list of
newline separated strings). Currently two fields are defined: <literal>ID=</literal> is supposed to carry
a short identifying string that identifies the profile
(e.g. <literal>ID=factory-reset</literal>). <literal>TITLE=</literal> should contain a human readable
string that may appear in the boot menu entry for this profile (e.g. <literal>TITLE='Factory Reset this
Device'</literal>).</para>
</refsect1>
<refsect1>
<title>TPM PCR Notes</title>
<para>Note that when a unified kernel using <command>systemd-stub</command> 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).</para>
core kernel, the embedded initrd and kernel command line (see above for a full list), including all UKI
profiles.</para>
<para>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:</para>
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:</para>
<table>
<title>OS Resource PCR Summary</title>
@ -324,6 +438,11 @@
<entry>Configuration Extensions (synthesized initrd from companion files)</entry>
<entry>9 + 12</entry>
</row>
<row>
<entry>Selected profile unless zero</entry>
<entry>12</entry>
</row>
</tbody>
</tgroup>
</table>
@ -422,6 +541,16 @@
<xi:include href="version-info.xml" xpointer="v255"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>StubProfile</varname></term>
<listitem><para>The numeric index of the selected profile, without the <literal>@</literal>,
formatted as decimal string. Set both on single-profile and multi-profile UKIs. (In the former case
this variable will be set to <literal>0</literal> unconditionally.)</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
</variablelist>
<para>Note that some of the variables above may also be set by the boot loader. The stub will only set
@ -500,6 +629,15 @@
<xi:include href="version-info.xml" xpointer="v252"/></listitem>
</varlistentry>
<varlistentry>
<term><filename>/.extra/profile</filename></term>
<term><filename>/.extra/os-release</filename></term>
<listitem><para>The contents of the <literal>.profile</literal> and <literal>.osrel</literal>
sections of the selected profile, if any.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
</variablelist>
<para>Note that all these files are located in the <literal>tmpfs</literal> file system the kernel sets

View File

@ -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, &section_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, &parameters_measured);
measure_sections(loaded_image, sections, &sections_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, &parameters_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, &parameters_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, &parameters_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);

View File

@ -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,

View File

@ -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)