1
0
mirror of https://github.com/systemd/systemd.git synced 2025-08-26 17:49:52 +03:00

bootspec: process multi-profile UKIs

This commit is contained in:
Lennart Poettering
2024-07-05 12:11:48 +02:00
parent 9de565dd5d
commit 59b3df9bae
2 changed files with 253 additions and 101 deletions

View File

@ -23,6 +23,7 @@
#include "string-table.h"
#include "strv.h"
#include "terminal-util.h"
#include "uki.h"
#include "unaligned.h"
static const char* const boot_entry_type_table[_BOOT_ENTRY_TYPE_MAX] = {
@ -48,6 +49,7 @@ static void boot_entry_free(BootEntry *entry) {
free(entry->id);
free(entry->id_old);
free(entry->id_without_profile);
free(entry->path);
free(entry->root);
free(entry->title);
@ -529,10 +531,18 @@ static int boot_entry_compare(const BootEntry *a, const BootEntry *b) {
return r;
}
r = -strverscmp_improved(a->id, b->id);
r = -strverscmp_improved(a->id_without_profile ?: a->id, b->id_without_profile ?: b->id);
if (r != 0)
return r;
if (a->id_without_profile && b->id_without_profile) {
/* The strverscmp_improved() call above already established that we are talking about the
* same image here, hence order by profile, if there is one */
r = CMP(a->profile, b->profile);
if (r != 0)
return r;
}
if (a->tries_left != UINT_MAX || b->tries_left != UINT_MAX)
return 0;
@ -636,28 +646,30 @@ static int boot_entries_find_type1(
static int boot_entry_load_unified(
const char *root,
const char *path,
const char *osrelease,
const char *cmdline,
unsigned profile,
const char *osrelease_text,
const char *profile_text,
const char *cmdline_text,
BootEntry *ret) {
_cleanup_free_ char *fname = NULL, *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
*os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
_cleanup_(boot_entry_free) BootEntry tmp = BOOT_ENTRY_INIT(BOOT_ENTRY_UNIFIED);
const char *k, *good_name, *good_version, *good_sort_key;
_cleanup_fclose_ FILE *f = NULL;
int r;
assert(root);
assert(path);
assert(osrelease);
assert(osrelease_text);
assert(ret);
k = path_startswith(path, root);
if (!k)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path is not below root: %s", path);
f = fmemopen_unlocked((void*) osrelease, strlen(osrelease), "r");
f = fmemopen_unlocked((void*) osrelease_text, strlen(osrelease_text), "r");
if (!f)
return log_error_errno(errno, "Failed to open os-release buffer: %m");
return log_oom();
r = parse_env_file(f, "os-release",
"PRETTY_NAME", &os_pretty_name,
@ -685,10 +697,28 @@ static int boot_entry_load_unified(
&good_sort_key))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing fields in os-release data from unified kernel image %s, refusing.", path);
_cleanup_free_ char *profile_id = NULL, *profile_title = NULL;
if (profile_text) {
fclose(f);
f = fmemopen_unlocked((void*) profile_text, strlen(profile_text), "r");
if (!f)
return log_oom();
r = parse_env_file(
f, "profile",
"ID", &profile_id,
"TITLE", &profile_title);
if (r < 0)
return log_error_errno(r, "Failed to parse profile data from unified kernel image '%s': %m", path);
}
r = path_extract_filename(path, &fname);
if (r < 0)
return log_error_errno(r, "Failed to extract file name from '%s': %m", path);
_cleanup_(boot_entry_free) BootEntry tmp = BOOT_ENTRY_INIT(BOOT_ENTRY_UNIFIED);
r = boot_filename_extract_tries(fname, &tmp.id, &tmp.tries_left, &tmp.tries_done);
if (r < 0)
return r;
@ -696,6 +726,19 @@ static int boot_entry_load_unified(
if (!efi_loader_entry_name_valid(tmp.id))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid loader entry name: %s", tmp.id);
tmp.profile = profile;
if (profile_id || profile > 0) {
tmp.id_without_profile = TAKE_PTR(tmp.id);
if (profile_id)
tmp.id = strjoin(tmp.id_without_profile, "@", profile_id);
else
(void) asprintf(&tmp.id, "%s@%u", tmp.id_without_profile, profile);
if (!tmp.id)
return log_oom();
}
if (os_id && os_version_id) {
tmp.id_old = strjoin(os_id, "-", os_version_id);
if (!tmp.id_old)
@ -714,13 +757,18 @@ static int boot_entry_load_unified(
if (!tmp.kernel)
return log_oom();
tmp.options = strv_new(skip_leading_chars(cmdline, WHITESPACE));
tmp.options = strv_new(cmdline_text);
if (!tmp.options)
return log_oom();
delete_trailing_chars(tmp.options[0], WHITESPACE);
tmp.title = strdup(good_name);
if (profile_title)
tmp.title = strjoin(good_name, " (", profile_title, ")");
else if (profile_id)
tmp.title = strjoin(good_name, " (", profile_id, ")");
else if (profile > 0)
(void) asprintf(&tmp.title, "%s (@%u)", good_name, profile);
else
tmp.title = strdup(good_name);
if (!tmp.title)
return log_oom();
@ -740,11 +788,7 @@ static int boot_entry_load_unified(
return 0;
}
/* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
* the ones we do care about and we are willing to load into memory have this size limit.) */
#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
static int find_sections(
static int pe_load_headers_and_sections(
int fd,
const char *path,
IMAGE_SECTION_HEADER **ret_sections,
@ -774,92 +818,174 @@ static int find_sections(
return 0;
}
static int find_cmdline_section(
int fd,
const char *path,
IMAGE_SECTION_HEADER *sections,
PeHeader *pe_header,
char **ret_cmdline) {
static const IMAGE_SECTION_HEADER* pe_find_profile_section_table(
const PeHeader *pe_header,
const IMAGE_SECTION_HEADER *sections,
unsigned profile,
size_t *ret_n_sections) {
int r;
char *cmdline = NULL, *t = NULL;
_cleanup_free_ char *word = NULL;
assert(pe_header);
assert(path);
/* Looks for the part of the section table that defines the specified profile. If 'profile' is
* specified as UINT_MAX this will look for the base profile. */
if (!ret_cmdline)
if (le16toh(pe_header->pe.NumberOfSections) == 0)
return NULL;
assert(sections);
const IMAGE_SECTION_HEADER
*p = sections,
*e = sections + le16toh(pe_header->pe.NumberOfSections),
*start = profile == UINT_MAX ? sections : NULL,
*end;
unsigned current_profile = UINT_MAX;
for (;;) {
p = pe_section_table_find(p, e - p, ".profile");
if (!p) {
end = e;
break;
}
if (current_profile == profile) {
end = p;
break;
}
if (current_profile == UINT_MAX)
current_profile = 0;
else
current_profile++;
if (current_profile == profile)
start = p;
p++; /* Continue scanning after the .profile entry we just found */
}
if (!start)
return NULL;
if (ret_n_sections)
*ret_n_sections = end - start;
return start;
}
static int trim_cmdline(char **cmdline) {
assert(cmdline);
/* Strips leading and trailing whitespace from command line */
if (!*cmdline)
return 0;
r = pe_read_section_data_by_name(fd, pe_header, sections, ".cmdline", PE_SECTION_SIZE_MAX, (void**) &cmdline, NULL);
if (r == -ENXIO) { /* cmdline is optional */
*ret_cmdline = NULL;
const char *skipped = skip_leading_chars(*cmdline, WHITESPACE);
if (isempty(skipped)) {
*cmdline = mfree(*cmdline);
return 0;
}
if (r < 0)
return log_warning_errno(r, "Failed to read .cmdline section of '%s': %m", path);
word = strdup(cmdline);
if (!word)
return log_oom();
if (skipped != *cmdline) {
_cleanup_free_ char *c = strdup(skipped);
if (!c)
return -ENOMEM;
/* Quick test to check if there is actual content in the addon cmdline */
t = delete_chars(word, NULL);
if (isempty(t))
*ret_cmdline = NULL;
else
*ret_cmdline = TAKE_PTR(cmdline);
free_and_replace(*cmdline, c);
}
return 0;
delete_trailing_chars(*cmdline, WHITESPACE);
return 1;
}
static int find_osrel_section(
int fd,
const char *path,
IMAGE_SECTION_HEADER *sections,
PeHeader *pe_header,
char **ret_osrelease) {
int r;
if (!ret_osrelease)
return 0;
r = pe_read_section_data_by_name(fd, pe_header, sections, ".osrel", PE_SECTION_SIZE_MAX, (void**) ret_osrelease, NULL);
if (r < 0)
return log_error_errno(r, "Failed to read .osrel section of '%s': %m", path);
return 0;
}
static int find_uki_sections(
/* Maximum PE section we are willing to load (Note that sections we are not interested in may be larger, but
* the ones we do care about and we are willing to load into memory have this size limit.) */
#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
static int pe_find_uki_sections(
int fd,
const char *path,
unsigned profile,
char **ret_osrelease,
char **ret_profile,
char **ret_cmdline) {
_cleanup_free_ char *osrelease_text = NULL, *profile_text = NULL, *cmdline_text = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
_cleanup_free_ PeHeader *pe_header = NULL;
int r;
r = find_sections(fd, path, &sections, &pe_header);
assert(fd >= 0);
assert(path);
assert(profile != UINT_MAX);
assert(ret_osrelease);
assert(ret_profile);
assert(ret_cmdline);
r = pe_load_headers_and_sections(fd, path, &sections, &pe_header);
if (r < 0)
return r;
if (!pe_is_uki(pe_header, sections))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Parsed PE file '%s' is not a UKI.", path);
r = find_osrel_section(fd, path, sections, pe_header, ret_osrelease);
if (r < 0)
return r;
/* Find part of the section table for this profile */
size_t n_psections = 0;
const IMAGE_SECTION_HEADER *psections = pe_find_profile_section_table(pe_header, sections, profile, &n_psections);
if (!psections && profile != 0) /* Profile not found? (Profile @0 needs no explicit .profile!) */
goto nothing;
r = find_cmdline_section(fd, path, sections, pe_header, ret_cmdline);
if (r < 0)
return r;
/* Find base profile part of section table */
size_t n_bsections;
const IMAGE_SECTION_HEADER *bsections = ASSERT_PTR(pe_find_profile_section_table(pe_header, sections, UINT_MAX, &n_bsections));
struct {
const char *name;
char **data;
} table[] = {
{ ".osrel", &osrelease_text },
{ ".profile", &profile_text },
{ ".cmdline", &cmdline_text },
};
FOREACH_ELEMENT(t, table) {
const IMAGE_SECTION_HEADER *found;
/* First look in the profile part of the section table, and if we don't find anything there, look into the base part */
found = pe_section_table_find(psections, n_psections, t->name);
if (!found) {
found = pe_section_table_find(bsections, n_bsections, t->name);
if (!found)
continue;
}
/* Permit "masking" of sections in the base profile */
if (found->VirtualSize == 0)
continue;
r = pe_read_section_data(fd, found, PE_SECTION_SIZE_MAX, (void**) t->data, /* ret_data= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to load contents of section '%s': %m", t->name);
}
if (!osrelease_text)
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Unified kernel image lacks .osrel data for profile @%u, refusing.", profile);
if (trim_cmdline(&cmdline_text) < 0)
return log_oom();
*ret_osrelease = TAKE_PTR(osrelease_text);
*ret_profile = TAKE_PTR(profile_text);
*ret_cmdline = TAKE_PTR(cmdline_text);
return 1;
nothing:
*ret_osrelease = *ret_profile = *ret_cmdline = NULL;
return 0;
}
static int find_addon_sections(
static int pe_find_addon_sections(
int fd,
const char *path,
char **ret_cmdline) {
@ -868,19 +994,36 @@ static int find_addon_sections(
_cleanup_free_ PeHeader *pe_header = NULL;
int r;
r = find_sections(fd, path, &sections, &pe_header);
assert(fd >= 0);
assert(path);
r = pe_load_headers_and_sections(fd, path, &sections, &pe_header);
if (r < 0)
return r;
r = find_cmdline_section(fd, path, sections, pe_header, ret_cmdline);
/* If addon cmdline is empty or contains just separators,
* don't bother tracking it.
* Don't check r because it cannot return <0 if cmdline is empty,
* as cmdline is always optional. */
if (!ret_cmdline)
return log_warning_errno(SYNTHETIC_ERRNO(ENOENT), "Addon %s contains empty cmdline and will be therefore ignored.", path);
if (!pe_is_addon(pe_header, sections))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Parse PE file '%s' is not an add-on.", path);
return r;
/* Define early, before the goto below */
_cleanup_free_ char *cmdline_text = NULL;
const IMAGE_SECTION_HEADER *found = pe_section_table_find(sections, le16toh(pe_header->pe.NumberOfSections), ".cmdline");
if (!found)
goto nothing;
r = pe_read_section_data(fd, found, PE_SECTION_SIZE_MAX, (void**) &cmdline_text, /* ret_size= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to load contents of section '.cmdline': %m");
if (trim_cmdline(&cmdline_text) < 0)
return log_oom();
*ret_cmdline = TAKE_PTR(cmdline_text);
return 1;
nothing:
*ret_cmdline = NULL;
return 0;
}
static int insert_boot_entry_addon(
@ -959,7 +1102,7 @@ static int boot_entries_find_unified_addons(
if (!j)
return log_oom();
if (find_addon_sections(fd, j, &cmdline) < 0)
if (pe_find_addon_sections(fd, j, &cmdline) <= 0)
continue;
location = strdup(j);
@ -1032,19 +1175,13 @@ static int boot_entries_find_unified(
return log_error_errno(r, "Failed to open '%s/%s': %m", root, dir);
FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", full)) {
_cleanup_free_ char *j = NULL, *osrelease = NULL, *cmdline = NULL;
_cleanup_close_ int fd = -EBADF;
if (!dirent_is_file(de))
continue;
if (!endswith_no_case(de->d_name, ".efi"))
continue;
if (!GREEDY_REALLOC0(config->entries, config->n_entries + 1))
return log_oom();
fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
_cleanup_close_ int fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
if (fd < 0) {
log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", full, de->d_name);
continue;
@ -1056,23 +1193,30 @@ static int boot_entries_find_unified(
if (r == 0) /* inode already seen or otherwise not relevant */
continue;
j = path_join(full, de->d_name);
_cleanup_free_ char *j = path_join(full, de->d_name);
if (!j)
return log_oom();
if (find_uki_sections(fd, j, &osrelease, &cmdline) < 0)
continue;
for (unsigned p = 0; p < UNIFIED_PROFILES_MAX; p++) {
_cleanup_free_ char *osrelease = NULL, *profile = NULL, *cmdline = NULL;
r = boot_entry_load_unified(root, j, osrelease, cmdline, config->entries + config->n_entries);
if (r < 0)
continue;
r = pe_find_uki_sections(fd, j, p, &osrelease, &profile, &cmdline);
if (r == 0) /* this profile does not exist, we are done */
break;
if (r < 0)
continue;
/* look for .efi.extra.d */
r = boot_entries_find_unified_local_addons(config, dirfd(d), de->d_name, full, config->entries + config->n_entries);
if (r < 0)
continue;
if (!GREEDY_REALLOC0(config->entries, config->n_entries + 2))
return log_oom();
config->n_entries++;
if (boot_entry_load_unified(root, j, p, osrelease, profile, cmdline, config->entries + config->n_entries) < 0)
continue;
config->n_entries++;
/* look for .efi.extra.d */
(void) boot_entries_find_unified_local_addons(config, dirfd(d), de->d_name, full, config->entries + config->n_entries);
}
}
return 0;
@ -1648,8 +1792,14 @@ int show_boot_entry(
putchar('\n');
if (e->id)
printf(" id: %s\n", e->id);
if (e->id) {
printf(" id: %s", e->id);
if (e->id_without_profile && !streq_ptr(e->id, e->id_without_profile))
printf(" (without profile: %s)\n", e->id_without_profile);
else
putchar('\n');
}
if (e->path) {
_cleanup_free_ char *text = NULL, *link = NULL;
@ -1673,7 +1823,7 @@ int show_boot_entry(
if (e->tries_done != UINT_MAX)
printf("; %u done\n", e->tries_done);
else
printf("\n");
putchar('\n');
}
if (e->sort_key)

View File

@ -38,6 +38,7 @@ typedef struct BootEntry {
bool reported_by_loader;
char *id; /* This is the file basename (including extension!) */
char *id_old; /* Old-style ID, for deduplication purposes. */
char *id_without_profile; /* id without profile suffixed */
char *path; /* This is the full path to the drop-in file */
char *root; /* The root path in which the drop-in was found, i.e. to which 'kernel', 'efi' and 'initrd' are relative */
char *title;
@ -55,6 +56,7 @@ typedef struct BootEntry {
char **device_tree_overlay;
unsigned tries_left;
unsigned tries_done;
unsigned profile;
} BootEntry;
#define BOOT_ENTRY_INIT(t) \