]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
bootspec: process multi-profile UKIs
authorLennart Poettering <lennart@poettering.net>
Fri, 5 Jul 2024 10:11:48 +0000 (12:11 +0200)
committerLennart Poettering <lennart@poettering.net>
Thu, 12 Sep 2024 08:02:15 +0000 (10:02 +0200)
src/shared/bootspec.c
src/shared/bootspec.h

index b902fe54803188068d01101174d1c03e7419bf47..a74204f11916ae8a556fbef2264b391f7d4cdc99 100644 (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)
-                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, &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);
+
+        /* 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)
index 0be254e1065e61106058ad6cc7770b5fe29b1f29..58c676fbeca874f4e8faf33b9ea9523c2b76c62b 100644 (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)                      \