From: Lennart Poettering Date: Fri, 5 Jul 2024 10:11:48 +0000 (+0200) Subject: bootspec: process multi-profile UKIs X-Git-Tag: v257-rc1~441^2~1 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=59b3df9;p=thirdparty%2Fsystemd.git bootspec: process multi-profile UKIs --- diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index b902fe54803..a74204f1191 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -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) - return 0; + if (le16toh(pe_header->pe.NumberOfSections) == 0) + return NULL; - 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; - return 0; + 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 (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 (!start) + return NULL; - /* 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); + if (ret_n_sections) + *ret_n_sections = end - start; - return 0; + return start; } -static int find_osrel_section( - int fd, - const char *path, - IMAGE_SECTION_HEADER *sections, - PeHeader *pe_header, - char **ret_osrelease) { +static int trim_cmdline(char **cmdline) { + assert(cmdline); - int r; + /* Strips leading and trailing whitespace from command line */ - if (!ret_osrelease) + if (!*cmdline) 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); + const char *skipped = skip_leading_chars(*cmdline, WHITESPACE); - return 0; + if (isempty(skipped)) { + *cmdline = mfree(*cmdline); + return 0; + } + + if (skipped != *cmdline) { + _cleanup_free_ char *c = strdup(skipped); + if (!c) + return -ENOMEM; + + free_and_replace(*cmdline, c); + } + + delete_trailing_chars(*cmdline, WHITESPACE); + return 1; } -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, §ions, &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, §ions, &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, §ions, &pe_header); + assert(fd >= 0); + assert(path); + + r = pe_load_headers_and_sections(fd, path, §ions, &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); + + /* Define early, before the goto below */ + _cleanup_free_ char *cmdline_text = NULL; - return r; + 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(); + + if (boot_entry_load_unified(root, j, p, osrelease, profile, cmdline, config->entries + config->n_entries) < 0) + continue; - config->n_entries++; + 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) diff --git a/src/shared/bootspec.h b/src/shared/bootspec.h index 0be254e1065..58c676fbeca 100644 --- a/src/shared/bootspec.h +++ b/src/shared/bootspec.h @@ -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) \