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

Merge pull request #34294 from poettering/uki-with-many-core

multi-profile UKIs (systemd-stub hookup)
This commit is contained in:
Lennart Poettering 2024-09-10 08:59:53 +02:00 committed by GitHub
commit f2129f1d8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 625 additions and 146 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 <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 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 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> <itemizedlist>
<!-- Let's keep this in the canonical order we also measure the sections by, i.e. as in <!-- 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 --> 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 <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 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> signature data in the <literal>.pcrsig</literal> section.</para></listitem>
</itemizedlist> </itemizedlist>
<para>If UEFI SecureBoot is enabled and the <literal>.cmdline</literal> section is present in the executed <para>Generally, the sections above should appear at most once in a UKI. That said, a concept of
image, any attempts to override the kernel command line by passing one as invocation parameters to the "profiles" is defined, that allows multiple sets of these sections to exist in a single UKI file, of
EFI binary are ignored. Thus, in order to allow overriding the kernel command line, either disable UEFI which one can be selected at boot. For this an additional PE section <literal>.profile</literal> is
SecureBoot, or don't include a kernel command line PE section in the kernel image file. If a command line defined which can be used as separator between multiple sets of these settings. The
is accepted via EFI invocation parameters to the EFI binary it is measured into TPM PCR 12 (if a TPM is <literal>.profile</literal> section itself may contain meta-information about the section, and follows a
present).</para> 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 <para>If non-zero, the selected numeric profile is measured into PCR 12.</para>
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>When <literal>.pcrsig</literal> and/or <literal>.pcrpkey</literal> sections are present in a <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 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> details); in case of the system extension images by using signed Verity images.</para>
</refsect1> </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> <refsect1>
<title>TPM PCR Notes</title> <title>TPM PCR Notes</title>
<para>Note that when a unified kernel using <command>systemd-stub</command> is invoked the firmware will <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 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 <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 every type of initrd (of the selected UKI profile) will possibly be measured two or three times: the
measured to PCR 4, PCR 9 and PCR 11; the initrd synthesized from credentials (and the one synthesized initrds embedded in the kernel image will be measured to PCR 4, PCR 9 and PCR 11; the initrd synthesized
from configuration extensions) will be measured to both PCR 9 and PCR 12; the initrd synthesized from from credentials (and the one synthesized from configuration extensions) will be measured to both PCR 9
system extensions will be measured to both PCR 4 and PCR 9. Let's summarize the OS resources and the PCRs and PCR 12; the initrd synthesized from system extensions will be measured to both PCR 4 and PCR 9. Let's
they are measured to:</para> summarize the OS resources and the PCRs they are measured to:</para>
<table> <table>
<title>OS Resource PCR Summary</title> <title>OS Resource PCR Summary</title>
@ -324,6 +438,11 @@
<entry>Configuration Extensions (synthesized initrd from companion files)</entry> <entry>Configuration Extensions (synthesized initrd from companion files)</entry>
<entry>9 + 12</entry> <entry>9 + 12</entry>
</row> </row>
<row>
<entry>Selected profile unless zero</entry>
<entry>12</entry>
</row>
</tbody> </tbody>
</tgroup> </tgroup>
</table> </table>
@ -422,6 +541,16 @@
<xi:include href="version-info.xml" xpointer="v255"/></listitem> <xi:include href="version-info.xml" xpointer="v255"/></listitem>
</varlistentry> </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> </variablelist>
<para>Note that some of the variables above may also be set by the boot loader. The stub will only set <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> <xi:include href="version-info.xml" xpointer="v252"/></listitem>
</varlistentry> </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> </variablelist>
<para>Note that all these files are located in the <literal>tmpfs</literal> file system the kernel sets <para>Note that all these files are located in the <literal>tmpfs</literal> file system the kernel sets

View File

@ -106,19 +106,6 @@ typedef struct PeFileHeader {
PeOptionalHeader OptionalHeader; PeOptionalHeader OptionalHeader;
} _packed_ PeFileHeader; } _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) #define SECTION_TABLE_BYTES_MAX (16U * 1024U * 1024U)
static bool verify_dos(const DosFileHeader *dos) { 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 /* 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 * 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++) for (size_t i = 0; section_names[i]; i++)
FOREACH_ARRAY(j, section_table, n_section_table) { 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; 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( EFI_STATUS pe_memory_locate_sections(
const void *base, const void *base,
const char *const section_names[], const char *const section_names[],
PeSectionVector sections[]) { PeSectionVector sections[]) {
const DosFileHeader *dos; EFI_STATUS err;
const PeFileHeader *pe;
size_t offset;
assert(base); assert(base);
assert(section_names); assert(section_names);
assert(sections); assert(sections);
dos = (const DosFileHeader *) base; const PeSectionHeader *section_table;
if (!verify_dos(dos)) size_t n_section_table;
return EFI_LOAD_ERROR; err = pe_section_table_from_base(base, &section_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( pe_locate_sections(
(const PeSectionHeader *) ((const uint8_t *) base + offset), section_table,
pe->FileHeader.NumberOfSections, n_section_table,
section_names, section_names,
PTR_TO_SIZE(base), PTR_TO_SIZE(base),
sections); sections);
@ -345,27 +350,19 @@ EFI_STATUS pe_memory_locate_sections(
return EFI_SUCCESS; return EFI_SUCCESS;
} }
EFI_STATUS pe_file_locate_sections( EFI_STATUS pe_section_table_from_file(
EFI_FILE *dir, EFI_FILE *handle,
const char16_t *path, PeSectionHeader **ret_section_table,
const char *const section_names[], size_t *ret_n_section_table) {
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 err; EFI_STATUS err;
size_t len;
assert(dir); assert(handle);
assert(path); assert(ret_section_table);
assert(section_names); assert(ret_n_section_table);
assert(sections);
err = dir->Open(dir, &handle, (char16_t *) path, EFI_FILE_MODE_READ, 0ULL);
if (err != EFI_SUCCESS)
return err;
DosFileHeader dos;
len = sizeof(dos); len = sizeof(dos);
err = handle->Read(handle, &len, &dos); err = handle->Read(handle, &len, &dos);
if (err != EFI_SUCCESS) if (err != EFI_SUCCESS)
@ -377,6 +374,7 @@ EFI_STATUS pe_file_locate_sections(
if (err != EFI_SUCCESS) if (err != EFI_SUCCESS)
return err; return err;
PeFileHeader pe;
len = sizeof(pe); len = sizeof(pe);
err = handle->Read(handle, &len, &pe); err = handle->Read(handle, &len, &pe);
if (err != EFI_SUCCESS) if (err != EFI_SUCCESS)
@ -388,10 +386,11 @@ EFI_STATUS pe_file_locate_sections(
if ((size_t) pe.FileHeader.NumberOfSections > SIZE_MAX / sizeof(PeSectionHeader)) if ((size_t) pe.FileHeader.NumberOfSections > SIZE_MAX / sizeof(PeSectionHeader))
return EFI_OUT_OF_RESOURCES; return EFI_OUT_OF_RESOURCES;
REENABLE_WARNING; REENABLE_WARNING;
section_table_len = (size_t) pe.FileHeader.NumberOfSections * sizeof(PeSectionHeader); size_t n_section_table = (size_t) pe.FileHeader.NumberOfSections;
if (section_table_len > SECTION_TABLE_BYTES_MAX) if (n_section_table * sizeof(PeSectionHeader) > SECTION_TABLE_BYTES_MAX)
return EFI_OUT_OF_RESOURCES; return EFI_OUT_OF_RESOURCES;
section_table = xmalloc(section_table_len);
_cleanup_free_ PeSectionHeader *section_table = xnew(PeSectionHeader, n_section_table);
if (!section_table) if (!section_table)
return EFI_OUT_OF_RESOURCES; return EFI_OUT_OF_RESOURCES;
@ -399,19 +398,138 @@ EFI_STATUS pe_file_locate_sections(
if (err != EFI_SUCCESS) if (err != EFI_SUCCESS)
return err; return err;
len = section_table_len; len = n_section_table * sizeof(PeSectionHeader);
err = handle->Read(handle, &len, section_table); err = handle->Read(handle, &len, section_table);
if (err != EFI_SUCCESS) if (err != EFI_SUCCESS)
return err; return err;
if (len != section_table_len) if (len != n_section_table * sizeof(PeSectionHeader))
return EFI_LOAD_ERROR; 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, &section_table, &n_section_table);
if (err != EFI_SUCCESS)
return err;
pe_locate_sections( pe_locate_sections(
section_table, section_table,
pe.FileHeader.NumberOfSections, n_section_table,
section_names, section_names,
/* validate_base= */ 0, /* don't validate base */ /* validate_base= */ 0, /* don't validate base */
sections); sections);
return EFI_SUCCESS; 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;
}

View File

@ -3,6 +3,20 @@
#include "efi.h" #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 /* This is a subset of the full PE section header structure, with validated values, and without
* the noise. */ * the noise. */
typedef struct PeSectionVector { typedef struct PeSectionVector {
@ -15,6 +29,24 @@ static inline bool PE_SECTION_VECTOR_IS_SET(const PeSectionVector *v) {
return v && v->size != 0; 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( EFI_STATUS pe_memory_locate_sections(
const void *base, const void *base,
const char *const section_names[], const char *const section_names[],

View File

@ -38,6 +38,8 @@ enum {
INITRD_CONFEXT, INITRD_CONFEXT,
INITRD_PCRSIG, INITRD_PCRSIG,
INITRD_PCRPKEY, INITRD_PCRPKEY,
INITRD_OSREL,
INITRD_PROFILE,
_INITRD_MAX, _INITRD_MAX,
}; };
@ -139,7 +141,7 @@ static EFI_STATUS combine_initrds(
return EFI_SUCCESS; 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 = static const uint64_t stub_features =
EFI_STUB_FEATURE_REPORT_BOOT_PARTITION | /* We set LoaderDevicePartUUID */ EFI_STUB_FEATURE_REPORT_BOOT_PARTITION | /* We set LoaderDevicePartUUID */
EFI_STUB_FEATURE_PICK_UP_CREDENTIALS | /* We pick up credentials from the boot partition */ 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_ADDONS | /* We pick up .cmdline addons */
EFI_STUB_FEATURE_CMDLINE_SMBIOS | /* We support extending kernel cmdline from SMBIOS Type #11 */ 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_DEVICETREE_ADDONS | /* We pick up .dtb addons */
EFI_STUB_FEATURE_MULTI_PROFILE_UKI | /* We grok the "@1" profile command line argument */
0; 0;
assert(loaded_image); 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_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_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_HANDLE stub_image,
EFI_LOADED_IMAGE_PROTOCOL *loaded_image, EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
bool have_cmdline, unsigned *ret_profile,
char16_t **ret) { char16_t **ret_cmdline) {
assert(stub_image); assert(stub_image);
assert(loaded_image); assert(loaded_image);
assert(ret); assert(ret_profile);
assert(ret_cmdline);
/* 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;
/* The UEFI shell registers EFI_SHELL_PARAMETERS_PROTOCOL onto images it runs. This lets us know that /* 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. */ * 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 /* 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?). */ * 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) 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 /* Not running from EFI shell, use entire LoadOptions. Note that LoadOptions is a void*, so
* it could be anything! */ * it could actually be anything! */
*ret = mangle_stub_cmdline(xstrndup16(loaded_image->LoadOptions, loaded_image->LoadOptionsSize / sizeof(char16_t))); char16_t *c = xstrndup16(loaded_image->LoadOptions, loaded_image->LoadOptionsSize / sizeof(char16_t));
return true; parse_profile_from_cmdline(&c, ret_profile);
*ret_cmdline = mangle_stub_cmdline(c);
return;
} }
if (shell->Argc < 2) if (shell->Argc <= 1) /* No arguments were provided? Then we fall back to built-in cmdline. */
/* No arguments were provided? Then we fall back to built-in cmdline. */ goto nothing;
return false;
/* Assemble the command line ourselves without our stub path. */ size_t i = 1;
*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]);
}
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( static EFI_STATUS load_addons_from_dir(
@ -734,43 +804,53 @@ static void generate_embedded_initrds(
const PeSectionVector sections[static _UNIFIED_SECTION_MAX], const PeSectionVector sections[static _UNIFIED_SECTION_MAX],
struct iovec initrds[static _INITRD_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(loaded_image);
assert(initrds); assert(initrds);
/* If the PCR signature was embedded in the PE image, then let's wrap it in a cpio and also pass it FOREACH_ELEMENT(t, table) {
* to the kernel, so that it can be read from /.extra/tpm2-pcr-signature.json. Note that this section if (!PE_SECTION_VECTOR_IS_SET(sections + t->section))
* is not measured, neither as raw section (see above), nor as cpio (here), because it is the continue;
* 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);
/* 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( (void) pack_cpio_literal(
(const uint8_t*) loaded_image->ImageBase + sections[UNIFIED_SECTION_PCRPKEY].memory_offset, (const uint8_t*) loaded_image->ImageBase + sections[t->section].memory_offset,
sections[UNIFIED_SECTION_PCRPKEY].size, sections[t->section].size,
".extra", ".extra",
u"tpm2-pcr-public-key.pem", t->filename,
/* dir_mode= */ 0555, /* dir_mode= */ 0555,
/* access_mode= */ 0444, /* access_mode= */ 0444,
/* tpm_pcr= */ UINT32_MAX, /* tpm_pcr= */ UINT32_MAX,
/* tpm_description= */ NULL, /* tpm_description= */ NULL,
initrds + INITRD_PCRPKEY, initrds + t->initrd_index,
/* ret_measured= */ NULL); /* ret_measured= */ NULL);
}
} }
static void lookup_embedded_initrds( 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); graphics_splash((const uint8_t*) loaded_image->ImageBase + sections[UNIFIED_SECTION_SPLASH].memory_offset, sections[UNIFIED_SECTION_SPLASH].size);
} }
static void determine_cmdline( static EFI_STATUS find_sections(
EFI_HANDLE image,
EFI_LOADED_IMAGE_PROTOCOL *loaded_image, EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
const PeSectionVector sections[static _UNIFIED_SECTION_MAX], unsigned profile,
char16_t **ret_cmdline, PeSectionVector sections[static _UNIFIED_SECTION_MAX]) {
int *parameters_measured) {
EFI_STATUS err;
assert(loaded_image); assert(loaded_image);
assert(sections); assert(sections);
if (use_load_options(image, loaded_image, /* have_cmdline= */ PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_CMDLINE), ret_cmdline)) { const PeSectionHeader *section_table;
/* Let's measure the passed kernel command line into the TPM. Note that this possibly size_t n_section_table;
* duplicates what we already did in the boot menu, if that was already used. However, since err = pe_section_table_from_base(loaded_image->ImageBase, &section_table, &n_section_table);
* we want the boot menu to support an EFI binary, and want to this stub to be usable from if (err != EFI_SUCCESS)
* any boot menu, let's measure things anyway. */ return log_error_status(err, "Unable to locate PE section table: %m");
bool m = false;
(void) tpm_log_load_options(*ret_cmdline, &m); /* Get the base sections */
combine_measured_flag(parameters_measured, m); err = pe_locate_profile_sections(
} else section_table,
*ret_cmdline = mangle_stub_cmdline(pe_section_to_str16(loaded_image, sections + UNIFIED_SECTION_CMDLINE)); 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) { 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; size_t n_dt_addons = 0, n_ucode_addons = 0;
_cleanup_free_ struct iovec *all_initrds = NULL; _cleanup_free_ struct iovec *all_initrds = NULL;
size_t n_all_initrds = 0; size_t n_all_initrds = 0;
unsigned profile = 0;
EFI_STATUS err; EFI_STATUS err;
err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image); err = BS->HandleProtocol(image, MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL), (void **) &loaded_image);
if (err != EFI_SUCCESS) if (err != EFI_SUCCESS)
return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m"); return log_error_status(err, "Error getting a LoadedImageProtocol handle: %m");
err = pe_memory_locate_sections(loaded_image->ImageBase, unified_sections, sections); /* Pick up the arguments passed to us, split out the prefixing profile parameter, and return the rest
if (err != EFI_SUCCESS) * as potential command line to use. */
return log_error_status(err, "Unable to locate embedded PE sections: %m"); (void) process_arguments(image, loaded_image, &profile, &cmdline);
if (!PE_SECTION_VECTOR_IS_SET(sections + UNIFIED_SECTION_LINUX))
return log_error_status(EFI_NOT_FOUND, "Image lacks .linux section.");
/* 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); measure_sections(loaded_image, sections, &sections_measured);
/* Show splash screen as early as possible, but after measuring it */ /* 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); 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) /* 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. */ * 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); cmdline_append_and_measure_smbios(&cmdline, &parameters_measured);
export_common_variables(loaded_image); 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. */ /* 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); install_embedded_devicetree(loaded_image, sections, &dt_state);

View File

@ -492,3 +492,24 @@ void *xmalloc(size_t size) {
assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS); assert_se(BS->AllocatePool(EfiLoaderData, size, &p) == EFI_SUCCESS);
return p; 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;
}

View File

@ -55,6 +55,8 @@ static inline void* xmemdup(const void *p, size_t l) {
#define xnew(type, n) ((type *) xmalloc_multiply((n), sizeof(type))) #define xnew(type, n) ((type *) xmalloc_multiply((n), sizeof(type)))
bool free_and_xstrdup16(char16_t **p, const char16_t *s);
typedef struct { typedef struct {
EFI_PHYSICAL_ADDRESS addr; EFI_PHYSICAL_ADDRESS addr;
size_t n_pages; size_t n_pages;

View File

@ -34,6 +34,7 @@
#define EFI_STUB_FEATURE_CMDLINE_SMBIOS (UINT64_C(1) << 6) #define EFI_STUB_FEATURE_CMDLINE_SMBIOS (UINT64_C(1) << 6)
#define EFI_STUB_FEATURE_DEVICETREE_ADDONS (UINT64_C(1) << 7) #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_PICK_UP_CONFEXTS (UINT64_C(1) << 8)
#define EFI_STUB_FEATURE_MULTI_PROFILE_UKI (UINT64_C(1) << 9)
typedef enum SecureBootMode { typedef enum SecureBootMode {
SECURE_BOOT_UNSUPPORTED, 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) */ /* The tag used for EV_EVENT_TAG event log records covering ucode addons (effectively initrds) */
#define UCODE_ADDON_EVENT_TAG_ID UINT32_C(0xdac08e1a) #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)