Merge pull request #27358 from bluca/pe_mule

stub: allow loading and verifying kernel command line addons
This commit is contained in:
Lennart Poettering
2023-05-25 03:56:06 -07:00
committed by GitHub
20 changed files with 421 additions and 39 deletions
+15
View File
@@ -0,0 +1,15 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "efi.h"
#include "version.h"
/* Magic string for recognizing our own binaries */
_used_ _section_(".sdmagic") static const char magic[] =
"#### LoaderInfo: systemd-addon " GIT_VERSION " ####";
/* This is intended to carry data, not to be executed */
EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table);
EFIAPI EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *system_table) {
return EFI_UNSUPPORTED;
}
+4
View File
@@ -2641,6 +2641,10 @@ static EFI_STATUS run(EFI_HANDLE image) {
init_usec = time_usec();
/* Ask Shim to leave its protocol around, so that the stub can use it to validate PEs.
* By default, Shim uninstalls its protocol when calling StartImage(). */
shim_retain_protocol();
err = BS->OpenProtocol(
image,
MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL),
+1 -24
View File
@@ -301,29 +301,6 @@ static EFI_STATUS pack_cpio_trailer(
return EFI_SUCCESS;
}
static char16_t *get_dropin_dir(const EFI_DEVICE_PATH *file_path) {
if (!file_path)
return NULL;
/* A device path is allowed to have more than one file path node. If that is the case they are
* supposed to be concatenated. Unfortunately, the device path to text protocol simply converts the
* nodes individually and then combines those with the usual '/' for device path nodes. But this does
* not create a legal EFI file path that the file protocol can use. */
/* Make sure we really only got file paths. */
for (const EFI_DEVICE_PATH *node = file_path; !device_path_is_end(node);
node = device_path_next_node(node))
if (node->Type != MEDIA_DEVICE_PATH || node->SubType != MEDIA_FILEPATH_DP)
return NULL;
_cleanup_free_ char16_t *file_path_str = NULL;
if (device_path_to_str(file_path, &file_path_str) != EFI_SUCCESS)
return NULL;
convert_efi_path(file_path_str);
return xasprintf("%ls.extra.d", file_path_str);
}
EFI_STATUS pack_cpio(
EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
const char16_t *dropin_dir,
@@ -363,7 +340,7 @@ EFI_STATUS pack_cpio(
return log_error_status(err, "Unable to open root directory: %m");
if (!dropin_dir)
dropin_dir = rel_dropin_dir = get_dropin_dir(loaded_image->FilePath);
dropin_dir = rel_dropin_dir = get_extra_dir(loaded_image->FilePath);
err = open_directory(root, dropin_dir, &extra_dir);
if (err == EFI_NOT_FOUND)
+23 -2
View File
@@ -242,6 +242,7 @@ libefi_sources = files(
'pe.c',
'random-seed.c',
'secure-boot.c',
'shim.c',
'ticks.c',
'util.c',
'vmm.c',
@@ -249,7 +250,6 @@ libefi_sources = files(
systemd_boot_sources = files(
'boot.c',
'shim.c',
)
stub_sources = files(
@@ -259,6 +259,10 @@ stub_sources = files(
'stub.c',
)
addon_sources = files(
'addon.c',
)
if get_option('b_sanitize') == 'undefined'
libefi_sources += files('ubsan.c')
endif
@@ -328,12 +332,28 @@ foreach archspec : efi_archspecs
override_options : efi_override_options,
name_suffix : 'elf.stub',
pie : true)
efi_elf_binaries += executable(
'addon' + archspec['arch'],
addon_sources,
include_directories : efi_includes,
c_args : archspec['c_args'],
link_args : archspec['link_args'],
link_depends : elf2efi_lds,
dependencies : versiondep,
gnu_symbol_visibility : 'hidden',
override_options : efi_override_options,
name_suffix : 'elf.stub',
pie : true)
endforeach
foreach efi_elf_binary : efi_elf_binaries
# FIXME: Use build_tgt.name() with meson >= 0.54.0
name = fs.name(efi_elf_binary.full_path()).split('.')[0]
name += name.startswith('linux') ? '.efi.stub' : '.efi'
name += name.startswith('systemd-boot') ? '.efi' : '.efi.stub'
# For the addon, given it's empty, we need to explicitly reserve space in the header to account for
# the sections that ukify will add.
minimum_sections = name.startswith('addon') ? '7' : '0'
exe = custom_target(
name,
output : name,
@@ -348,6 +368,7 @@ foreach efi_elf_binary : efi_elf_binaries
'--efi-major=1',
'--efi-minor=1',
'--subsystem=10',
'--minimum-sections=' + minimum_sections,
'@INPUT@',
'@OUTPUT@',
])
+9
View File
@@ -97,3 +97,12 @@ EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path
return ret;
}
void shim_retain_protocol(void) {
uint8_t value = 1;
/* Ask Shim to avoid uninstalling its security protocol, so that we can use it from sd-stub to
* validate PE addons. By default, Shim uninstalls its protocol when calling StartImage().
* Requires Shim 15.8. */
(void) efivar_set_raw(MAKE_GUID_PTR(SHIM_LOCK), u"ShimRetainProtocol", &value, sizeof(value), 0);
}
+1
View File
@@ -13,3 +13,4 @@
bool shim_loaded(void);
EFI_STATUS shim_load_image(EFI_HANDLE parent, const EFI_DEVICE_PATH *device_path, EFI_HANDLE *ret_image);
void shim_retain_protocol(void);
+223 -1
View File
@@ -11,6 +11,7 @@
#include "proto/shell-parameters.h"
#include "random-seed.h"
#include "secure-boot.h"
#include "shim.h"
#include "splash.h"
#include "tpm-pcr.h"
#include "util.h"
@@ -180,6 +181,189 @@ static bool use_load_options(
return true;
}
static EFI_STATUS load_addons_from_dir(
EFI_FILE *root,
const char16_t *prefix,
char16_t ***items,
size_t *n_items,
size_t *n_allocated) {
_cleanup_(file_closep) EFI_FILE *extra_dir = NULL;
_cleanup_free_ EFI_FILE_INFO *dirent = NULL;
size_t dirent_size = 0;
EFI_STATUS err;
assert(root);
assert(prefix);
assert(items);
assert(n_items);
assert(n_allocated);
err = open_directory(root, prefix, &extra_dir);
if (err == EFI_NOT_FOUND)
/* No extra subdir, that's totally OK */
return EFI_SUCCESS;
if (err != EFI_SUCCESS)
return log_error_status(err, "Failed to open addons directory '%ls': %m", prefix);
for (;;) {
_cleanup_free_ char16_t *d = NULL;
err = readdir(extra_dir, &dirent, &dirent_size);
if (err != EFI_SUCCESS)
return log_error_status(err, "Failed to read addons directory of loaded image: %m");
if (!dirent) /* End of directory */
break;
if (dirent->FileName[0] == '.')
continue;
if (FLAGS_SET(dirent->Attribute, EFI_FILE_DIRECTORY))
continue;
if (!is_ascii(dirent->FileName))
continue;
if (strlen16(dirent->FileName) > 255) /* Max filename size on Linux */
continue;
if (!endswith_no_case(dirent->FileName, u".addon.efi"))
continue;
d = xstrdup16(dirent->FileName);
if (*n_items + 2 > *n_allocated) {
/* We allocate 16 entries at a time, as a matter of optimization */
if (*n_items > (SIZE_MAX / sizeof(uint16_t)) - 16) /* Overflow check, just in case */
return log_oom();
size_t m = *n_items + 16;
*items = xrealloc(*items, *n_allocated * sizeof(uint16_t *), m * sizeof(uint16_t *));
*n_allocated = m;
}
(*items)[(*n_items)++] = TAKE_PTR(d);
(*items)[*n_items] = NULL; /* Let's always NUL terminate, to make freeing via strv_free() easy */
}
return EFI_SUCCESS;
}
static EFI_STATUS cmdline_append_and_measure_addons(
EFI_HANDLE stub_image,
EFI_LOADED_IMAGE_PROTOCOL *loaded_image,
const char16_t *prefix,
const char *uname,
bool *ret_parameters_measured,
char16_t **cmdline_append) {
_cleanup_(strv_freep) char16_t **items = NULL;
_cleanup_(file_closep) EFI_FILE *root = NULL;
_cleanup_free_ char16_t *buffer = NULL;
size_t n_items = 0, n_allocated = 0;
EFI_STATUS err;
assert(stub_image);
assert(loaded_image);
assert(prefix);
assert(ret_parameters_measured);
assert(cmdline_append);
if (!loaded_image->DeviceHandle)
return EFI_SUCCESS;
err = open_volume(loaded_image->DeviceHandle, &root);
if (err == EFI_UNSUPPORTED)
/* Error will be unsupported if the bootloader doesn't implement the file system protocol on
* its file handles. */
return EFI_SUCCESS;
if (err != EFI_SUCCESS)
return log_error_status(err, "Unable to open root directory: %m");
err = load_addons_from_dir(root, prefix, &items, &n_items, &n_allocated);
if (err != EFI_SUCCESS)
return err;
if (n_items == 0)
return EFI_SUCCESS; /* Empty directory */
/* Now, sort the files we found, to make this uniform and stable (and to ensure the TPM measurements
* are not dependent on read order) */
sort_pointer_array((void**) items, n_items, (compare_pointer_func_t) strcmp16);
for (size_t i = 0; i < n_items; i++) {
size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {};
_cleanup_free_ EFI_DEVICE_PATH *addon_path = NULL;
_cleanup_(unload_imagep) EFI_HANDLE addon = NULL;
EFI_LOADED_IMAGE_PROTOCOL *loaded_addon = NULL;
_cleanup_free_ char16_t *addon_spath = NULL;
addon_spath = xasprintf("%ls\\%ls", prefix, items[i]);
err = make_file_device_path(loaded_image->DeviceHandle, addon_spath, &addon_path);
if (err != EFI_SUCCESS)
return log_error_status(err, "Error making device path for %ls: %m", addon_spath);
/* By using shim_load_image, we cover both the case where the PE files are signed with MoK
* and with DB, and running with or without shim. */
err = shim_load_image(stub_image, addon_path, &addon);
if (err != EFI_SUCCESS) {
log_error_status(err,
"Failed to read '%ls' from '%ls', ignoring: %m",
items[i],
addon_spath);
continue;
}
err = BS->HandleProtocol(addon,
MAKE_GUID_PTR(EFI_LOADED_IMAGE_PROTOCOL),
(void **) &loaded_addon);
if (err != EFI_SUCCESS)
return log_error_status(err, "Failed to find protocol in %ls: %m", items[i]);
err = pe_memory_locate_sections(loaded_addon->ImageBase, unified_sections, addrs, szs);
if (err != EFI_SUCCESS || szs[UNIFIED_SECTION_CMDLINE] == 0) {
if (err == EFI_SUCCESS)
err = EFI_NOT_FOUND;
log_error_status(err,
"Unable to locate embedded .cmdline section in %ls, ignoring: %m",
items[i]);
continue;
}
/* We want to enforce that addons are not UKIs, i.e.: they must not embed a kernel. */
if (szs[UNIFIED_SECTION_LINUX] > 0) {
log_error_status(EFI_INVALID_PARAMETER, "%ls is a UKI, not an addon, ignoring: %m", items[i]);
continue;
}
/* Also enforce that, in case it is specified, .uname matches as a quick way to allow
* enforcing compatibility with a specific UKI only */
if (uname && szs[UNIFIED_SECTION_UNAME] > 0 &&
!strneq8(uname,
(char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_UNAME],
szs[UNIFIED_SECTION_UNAME])) {
log_error(".uname mismatch between %ls and UKI, ignoring", items[i]);
continue;
}
_cleanup_free_ char16_t *tmp = TAKE_PTR(buffer),
*extra16 = xstrn8_to_16((char *)loaded_addon->ImageBase + addrs[UNIFIED_SECTION_CMDLINE],
szs[UNIFIED_SECTION_CMDLINE]);
buffer = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", extra16);
}
mangle_stub_cmdline(buffer);
if (!isempty(buffer)) {
_cleanup_free_ char16_t *tmp = TAKE_PTR(*cmdline_append);
bool m = false;
(void) tpm_log_load_options(buffer, &m);
*ret_parameters_measured = m;
*cmdline_append = xasprintf("%ls%ls%ls", strempty(tmp), isempty(tmp) ? u"" : u" ", buffer);
}
return EFI_SUCCESS;
}
static EFI_STATUS run(EFI_HANDLE image) {
_cleanup_free_ void *credential_initrd = NULL, *global_credential_initrd = NULL, *sysext_initrd = NULL, *pcrsig_initrd = NULL, *pcrpkey_initrd = NULL;
size_t credential_initrd_size = 0, global_credential_initrd_size = 0, sysext_initrd_size = 0, pcrsig_initrd_size = 0, pcrpkey_initrd_size = 0;
@@ -190,6 +374,7 @@ static EFI_STATUS run(EFI_HANDLE image) {
size_t addrs[_UNIFIED_SECTION_MAX] = {}, szs[_UNIFIED_SECTION_MAX] = {};
_cleanup_free_ char16_t *cmdline = NULL;
int sections_measured = -1, parameters_measured = -1;
_cleanup_free_ char *uname = NULL;
bool sysext_measured = false, m;
uint64_t loader_features = 0;
EFI_STATUS err;
@@ -262,6 +447,10 @@ static EFI_STATUS run(EFI_HANDLE image) {
/* Show splash screen as early as possible */
graphics_splash((const uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_SPLASH], szs[UNIFIED_SECTION_SPLASH]);
if (szs[UNIFIED_SECTION_UNAME] > 0)
uname = xstrndup8((char *)loaded_image->ImageBase + addrs[UNIFIED_SECTION_UNAME],
szs[UNIFIED_SECTION_UNAME]);
if (use_load_options(image, loaded_image, szs[UNIFIED_SECTION_CMDLINE] > 0, &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
@@ -277,11 +466,44 @@ static EFI_STATUS run(EFI_HANDLE image) {
mangle_stub_cmdline(cmdline);
}
/* SMBIOS strings are measured in PCR1, so we do not re-measure these command line extensions. */
/* If we have any extra command line to add via PE addons, load them now and append, and
* measure the additions separately, after the embedded options, but before the smbios ones,
* so that the order is reversed from "most hardcoded" to "most dynamic". The global addons are
* loaded first, and the image-specific ones later, for the same reason. */
err = cmdline_append_and_measure_addons(
image,
loaded_image,
u"\\loader\\addons",
uname,
&m,
&cmdline);
if (err != EFI_SUCCESS)
log_error_status(err, "Error loading global addons, ignoring: %m");
parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
_cleanup_free_ char16_t *dropin_dir = get_extra_dir(loaded_image->FilePath);
err = cmdline_append_and_measure_addons(
image,
loaded_image,
dropin_dir,
uname,
&m,
&cmdline);
if (err != EFI_SUCCESS)
log_error_status(err, "Error loading UKI-specific addons, ignoring: %m");
parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
const char *extra = smbios_find_oem_string("io.systemd.stub.kernel-cmdline-extra");
if (extra) {
_cleanup_free_ char16_t *tmp = TAKE_PTR(cmdline), *extra16 = xstr8_to_16(extra);
cmdline = xasprintf("%ls %ls", tmp, extra16);
/* SMBIOS strings are measured in PCR1, but we also want to measure them in our specific
* PCR12, as firmware-owned PCRs are very difficult to use as they'll contain unpredictable
* measurements that are not under control of the machine owner. */
m = false;
(void) tpm_log_load_options(extra16, &m);
parameters_measured = parameters_measured < 0 ? m : (parameters_measured && m);
}
export_variables(loaded_image);
+27
View File
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "device-path-util.h"
#include "proto/device-path.h"
#include "proto/simple-text-io.h"
#include "ticks.h"
@@ -267,6 +268,9 @@ char16_t *xstr8_to_path(const char *str8) {
void mangle_stub_cmdline(char16_t *cmdline) {
char16_t *p = cmdline;
if (!cmdline)
return;
for (; *cmdline != '\0'; cmdline++)
/* Convert ASCII control characters to spaces. */
if (*cmdline <= 0x1F)
@@ -665,3 +669,26 @@ void *find_configuration_table(const EFI_GUID *guid) {
return NULL;
}
char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path) {
if (!file_path)
return NULL;
/* A device path is allowed to have more than one file path node. If that is the case they are
* supposed to be concatenated. Unfortunately, the device path to text protocol simply converts the
* nodes individually and then combines those with the usual '/' for device path nodes. But this does
* not create a legal EFI file path that the file protocol can use. */
/* Make sure we really only got file paths. */
for (const EFI_DEVICE_PATH *node = file_path; !device_path_is_end(node);
node = device_path_next_node(node))
if (node->Type != MEDIA_DEVICE_PATH || node->SubType != MEDIA_FILEPATH_DP)
return NULL;
_cleanup_free_ char16_t *file_path_str = NULL;
if (device_path_to_str(file_path, &file_path_str) != EFI_SUCCESS)
return NULL;
convert_efi_path(file_path_str);
return xasprintf("%ls.extra.d", file_path_str);
}
+2
View File
@@ -212,3 +212,5 @@ static inline bool efi_guid_equal(const EFI_GUID *a, const EFI_GUID *b) {
}
void *find_configuration_table(const EFI_GUID *guid);
char16_t *get_extra_dir(const EFI_DEVICE_PATH *file_path);
+3
View File
@@ -83,6 +83,7 @@ static int help(int argc, char *argv[], void *userdata) {
" --initrd=PATH Path to initrd image file %7$s .initrd\n"
" --splash=PATH Path to splash bitmap file %7$s .splash\n"
" --dtb=PATH Path to Devicetree file %7$s .dtb\n"
" --uname=PATH Path to 'uname -r' file %7$s .uname\n"
" --pcrpkey=PATH Path to public key for PCR signatures %7$s .pcrpkey\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
@@ -122,6 +123,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_INITRD,
ARG_SPLASH,
ARG_DTB,
ARG_UNAME,
_ARG_PCRSIG, /* the .pcrsig section is not input for signing, hence not actually an argument here */
_ARG_SECTION_LAST,
ARG_PCRPKEY = _ARG_SECTION_LAST,
@@ -144,6 +146,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "initrd", required_argument, NULL, ARG_INITRD },
{ "splash", required_argument, NULL, ARG_SPLASH },
{ "dtb", required_argument, NULL, ARG_DTB },
{ "uname", required_argument, NULL, ARG_UNAME },
{ "pcrpkey", required_argument, NULL, ARG_PCRPKEY },
{ "current", no_argument, NULL, 'c' },
{ "bank", required_argument, NULL, ARG_BANK },
+1
View File
@@ -11,6 +11,7 @@ const char* const unified_sections[_UNIFIED_SECTION_MAX + 1] = {
[UNIFIED_SECTION_INITRD] = ".initrd",
[UNIFIED_SECTION_SPLASH] = ".splash",
[UNIFIED_SECTION_DTB] = ".dtb",
[UNIFIED_SECTION_UNAME] = ".uname",
[UNIFIED_SECTION_PCRSIG] = ".pcrsig",
[UNIFIED_SECTION_PCRPKEY] = ".pcrpkey",
NULL,
+1
View File
@@ -29,6 +29,7 @@ typedef enum UnifiedSection {
UNIFIED_SECTION_INITRD,
UNIFIED_SECTION_SPLASH,
UNIFIED_SECTION_DTB,
UNIFIED_SECTION_UNAME,
UNIFIED_SECTION_PCRSIG,
UNIFIED_SECTION_PCRPKEY,
_UNIFIED_SECTION_MAX,
+18 -3
View File
@@ -658,10 +658,10 @@ def make_uki(opts):
('.osrel', opts.os_release, True ),
('.cmdline', opts.cmdline, True ),
('.dtb', opts.devicetree, True ),
('.uname', opts.uname, True ),
('.splash', opts.splash, True ),
('.pcrpkey', pcrpkey, True ),
('.initrd', initrd, True ),
('.uname', opts.uname, False),
# linux shall be last to leave breathing room for decompression.
# We'll add it later.
@@ -679,10 +679,12 @@ def make_uki(opts):
call_systemd_measure(uki, linux, opts=opts)
# UKI creation
# UKI or addon creation - addons don't use the stub so we add SBAT manually
if linux is not None:
uki.add_section(Section.create('.linux', linux, measure=True))
elif opts.sbat:
uki.add_section(Section.create('.sbat', opts.sbat, measure=False))
if sign_args_present:
unsigned = tempfile.NamedTemporaryFile(prefix='uki')
@@ -927,6 +929,16 @@ CONFIG_ITEMS = [
config_key = 'UKI/Stub',
),
ConfigItem(
'--sbat',
metavar = 'TEXT|@PATH',
help = 'SBAT policy [.sbat section] for addons',
default = """sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
uki.addon,1,UKI Addon,uki.addon,1,https://www.freedesktop.org/software/systemd/man/systemd-stub.html
""",
config_key = 'Addon/SBAT',
),
ConfigItem(
'--section',
dest = 'sections',
@@ -1141,7 +1153,10 @@ def finalize_options(opts):
opts.efi_arch = guess_efi_arch()
if opts.stub is None:
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
if opts.linux is not None:
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/linux{opts.efi_arch}.efi.stub')
else:
opts.stub = pathlib.Path(f'/usr/lib/systemd/boot/efi/addon{opts.efi_arch}.efi.stub')
if opts.signing_engine is None:
if opts.sb_key: