diff --git a/man/bootctl.xml b/man/bootctl.xml index fb5f4b2b2a..39ddfdd58c 100644 --- a/man/bootctl.xml +++ b/man/bootctl.xml @@ -117,6 +117,36 @@ + + KERNEL + + Creates one or more Type #1 boot loader entries pointing to the specified UKI. Takes + the path to a Unified Kernel Image (UKI) as argument. The UKI is copied into the ESP (or XBOOTLDR + partition if present) below the configured entry token directory, and one or more Boot Loader + Specification Type #1 entries are generated referring to it (one per UKI profile, if multiple + profiles are embedded). + + The title, version, commit number and initial try counter of the generated entries + may be overridden with , , + and . Additional sidecar resources + (system extension images, configuration extension images, credential files) to pass to the UKI at + boot may be specified with . + + If the ESP/XBOOTLDR do not have enough free space for the new boot loader entry and its + referenced resources the oldest existing boot loader entry matching the selected entry token is + removed (along with any resources referenced by it that are no longer referenced by any other + entry). This step is repeated until the new boot loader entry fits. For robustness reasons the + currently booted boot loader entry is never removed, nor is the last existing boot loader + entry. + + By default, the operation refuses to proceed if the resulting ESP/XBOOTLDR free space would + drop below a safety threshold after automatic removal of older entries completes; use + to adjust. + + + + @@ -577,6 +607,77 @@ + + + + When used with link, controls the minimum amount of free space + (in bytes) that must remain on the target partition (ESP or XBOOTLDR) after the new entry has been + materialized. The operation fails if installing the entry would drop the free space below this + threshold. Accepts the usual size suffixes (K, M, G, …). If empty, the built-in default is + restored. If set to zero no minimum amount of free space is kept. + + + + + + + + When used with link, specifies the title of the generated boot + loader entry (the title field of the Type #1 entry). If not specified, a title is + derived from the UKI's embedded metadata. + + + + + + + + When used with link, specifies the version string of the + generated boot loader entry (the version field of the Type #1 entry). If not + specified, the version is derived from the UKI's embedded metadata. Used by the boot loader to sort + and select entries. + + + + + + + + When used with link, specifies the commit number for the generated + boot loader entry. + + + + + + + + + When used with link, registers an additional sidecar resource + file that shall be passed to the UKI at boot. This may be a system extension image + (*.sysext.raw), configuration extension image + (*.confext.raw), or credential file + (*.cred). The file is copied into the ESP/XBOOTLDR alongside the UKI and the + boot loader will load and pass it to the kernel via initrd. This option may be used multiple times + to register more than one extra resource. If passed an empty argument, all previously specified + extras are cleared. + + + + + + + + When used with link, initializes the boot counting + tries-left counter for the generated entry. If set, the resulting boot entry file + is named according to the boot counting scheme described in the Automatic Boot Assessment documentation, + so that the boot loader decreases the counter on each attempted boot and eventually marks the entry + as bad. If not specified, boot counting is not enabled for the generated entry. + + + + diff --git a/shell-completion/bash/bootctl b/shell-completion/bash/bootctl index 792fc0c0ac..3b7290c230 100644 --- a/shell-completion/bash/bootctl +++ b/shell-completion/bash/bootctl @@ -43,7 +43,8 @@ _bootctl() { --efi-boot-option-description --efi-boot-option-description-with-device --secure-boot-auto-enroll --private-key --private-key-source --certificate --certificate-source - --oldest' + --oldest --keep-free --entry-title --entry-version --entry-commit + -X --extra --tries-left' ) if __contains_word "$prev" ${OPTS[ARG]}; then @@ -62,7 +63,7 @@ _bootctl() { --entry-token) comps="machine-id os-id os-image-id auto literal:" ;; - --image|--root) + --image|--root|-X|--extra) compopt -o nospace comps=$( compgen -A file -- "$cur" ) ;; @@ -89,7 +90,7 @@ _bootctl() { [STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup' [BOOTENTRY]='set-default set-oneshot set-sysfail set-preferred unlink' [BOOLEAN]='reboot-to-firmware' - [FILE]='kernel-identify kernel-inspect' + [FILE]='kernel-identify kernel-inspect link' ) for ((i=0; i < COMP_CWORD; i++)); do diff --git a/shell-completion/zsh/_bootctl b/shell-completion/zsh/_bootctl index c23c1c888d..423828ba33 100644 --- a/shell-completion/zsh/_bootctl +++ b/shell-completion/zsh/_bootctl @@ -36,6 +36,10 @@ _bootctl_unlink() { _bootctl_comp_ids } +_bootctl_link() { + _files +} + _bootctl_kernel-identify() { _files } @@ -70,6 +74,7 @@ _bootctl_reboot-to-firmware() { "set-timeout:Set the menu timeout" "set-timeout-oneshot:Set the menu timeout for the next boot only" "unlink:Remove boot loader entry" + "link:Add boot loader entry" "cleanup:Remove files in ESP not referenced in any boot entry" "kernel-identify:Identify kernel image type" "kernel-inspect:Print details about the kernel image" diff --git a/src/bootctl/bootctl-link.c b/src/bootctl/bootctl-link.c new file mode 100644 index 0000000000..6358189e7d --- /dev/null +++ b/src/bootctl/bootctl-link.c @@ -0,0 +1,1206 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "sd-json.h" +#include "sd-varlink.h" + +#include "boot-entry.h" +#include "bootctl.h" +#include "bootctl-link.h" +#include "bootctl-unlink.h" +#include "bootspec.h" +#include "bootspec-util.h" +#include "chase.h" +#include "copy.h" +#include "dirent-util.h" +#include "efi-loader.h" +#include "env-file.h" +#include "errno-util.h" +#include "fd-util.h" +#include "find-esp.h" +#include "format-util.h" +#include "fs-util.h" +#include "hashmap.h" +#include "id128-util.h" +#include "io-util.h" +#include "json-util.h" +#include "kernel-image.h" +#include "log.h" +#include "parse-argument.h" +#include "path-util.h" +#include "recurse-dir.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "uki.h" +#include "utf8.h" + +/* Keeps track of an "extra" file to associate with the type 1 entries to generate */ +typedef struct ExtraFile { + /* The source and the temporary file we copy it into */ + int source_fd, temp_fd; + char *filename, *temp_filename; +} ExtraFile; + +#define EXTRA_FILE_NULL \ + (const ExtraFile) { \ + .source_fd = -EBADF, \ + .temp_fd = -EBADF, \ + } + +/* Keeps track of a specific UKI profile we need to generate a type entry for */ +typedef struct Profile { + /* The final and the temporary file for the .conf entry file, while we write it */ + char *entry_filename, *entry_temp_filename; + int entry_temp_fd; +} Profile; + +typedef struct LinkContext { + char *root; + int root_fd; + + sd_id128_t machine_id; + BootEntryTokenType entry_token_type; + char *entry_token; + + char *entry_title; + char *entry_version; + uint64_t entry_commit; + + BootEntrySource dollar_boot_source; + char *dollar_boot_path; + int dollar_boot_fd; + int entry_token_dir_fd; + int loader_entries_dir_fd; + + /* The UKI source and temporary target while we write it. Note that for now we exclusively support + * UKIs, but let's keep things somewhat generic to keep options open for the future. */ + char *kernel_filename, *kernel_temp_filename; + int kernel_fd, kernel_temp_fd; + + ExtraFile *extra; + size_t n_extra; + + Profile *profiles; + size_t n_profiles; + + unsigned tries_left; + + uint64_t keep_free; + + char **linked_ids; +} LinkContext; + +#define LINK_CONTEXT_NULL \ + (LinkContext) { \ + .root_fd = -EBADF, \ + .entry_token_type = _BOOT_ENTRY_TOKEN_TYPE_INVALID, \ + .dollar_boot_fd = -EBADF, \ + .loader_entries_dir_fd = -EBADF, \ + .entry_token_dir_fd = -EBADF, \ + .kernel_fd = -EBADF, \ + .kernel_temp_fd = -EBADF, \ + .tries_left = UINT_MAX, \ + .keep_free = UINT64_MAX, \ + } + +static void extra_file_done(ExtraFile *x) { + assert(x); + + x->source_fd = safe_close(x->source_fd); + x->temp_fd = safe_close(x->temp_fd); + x->filename = mfree(x->filename); + x->temp_filename = mfree(x->temp_filename); +} + +static void profile_done(Profile *p) { + assert(p); + + p->entry_filename = mfree(p->entry_filename); + p->entry_temp_filename = mfree(p->entry_temp_filename); + p->entry_temp_fd = safe_close(p->entry_temp_fd); +} + +static void link_context_unlink_temporary(LinkContext *c) { + assert(c); + + if (c->kernel_temp_filename) { + if (c->entry_token_dir_fd >= 0) + (void) unlinkat(c->entry_token_dir_fd, c->kernel_temp_filename, /* flags= */ 0); + + c->kernel_temp_fd = safe_close(c->kernel_temp_fd); + c->kernel_temp_filename = mfree(c->kernel_temp_filename); + } + + FOREACH_ARRAY(x, c->extra, c->n_extra) { + if (!x->temp_filename) + continue; + + if (c->entry_token_dir_fd >= 0) + (void) unlinkat(c->entry_token_dir_fd, x->temp_filename, /* flags= */ 0); + + x->temp_fd = safe_close(x->temp_fd); + x->temp_filename = mfree(x->temp_filename); + } + + FOREACH_ARRAY(p, c->profiles, c->n_profiles) { + if (!p->entry_temp_filename) + continue; + + if (c->loader_entries_dir_fd >= 0) + (void) unlinkat(c->loader_entries_dir_fd, p->entry_temp_filename, /* flags= */ 0); + + p->entry_temp_fd = safe_close(p->entry_temp_fd); + p->entry_temp_filename = mfree(p->entry_temp_filename); + } +} + +static void link_context_clear_profiles(LinkContext *c) { + assert(c); + + FOREACH_ARRAY(p, c->profiles, c->n_profiles) + profile_done(p); + + c->profiles = mfree(c->profiles); + c->n_profiles = 0; +} + +static void link_context_done(LinkContext *c) { + assert(c); + + link_context_unlink_temporary(c); + + FOREACH_ARRAY(x, c->extra, c->n_extra) + extra_file_done(x); + + c->extra = mfree(c->extra); + c->n_extra = 0; + + link_context_clear_profiles(c); + + c->kernel_filename = mfree(c->kernel_filename); + c->kernel_fd = safe_close(c->kernel_fd); + c->kernel_temp_filename = mfree(c->kernel_temp_filename); + c->kernel_temp_fd = safe_close(c->kernel_temp_fd); + + c->root = mfree(c->root); + c->root_fd = safe_close(c->root_fd); + + c->entry_token = mfree(c->entry_token); + c->entry_title = mfree(c->entry_title); + c->entry_version = mfree(c->entry_version); + + c->dollar_boot_path = mfree(c->dollar_boot_path); + c->dollar_boot_fd = safe_close(c->dollar_boot_fd); + c->entry_token_dir_fd = safe_close(c->entry_token_dir_fd); + c->loader_entries_dir_fd = safe_close(c->loader_entries_dir_fd); + + c->linked_ids = strv_free(c->linked_ids); +} + +static int link_context_from_cmdline(LinkContext *ret, const char *kernel) { + int r; + + assert(ret); + assert(kernel); + + _cleanup_(link_context_done) LinkContext b = LINK_CONTEXT_NULL; + b.entry_token_type = arg_entry_token_type; + b.tries_left = arg_tries_left; + b.entry_commit = arg_entry_commit; + b.keep_free = arg_keep_free; + + if (strdup_to(&b.entry_token, arg_entry_token) < 0 || + strdup_to(&b.entry_title, arg_entry_title) < 0 || + strdup_to(&b.entry_version, arg_entry_version) < 0) + return log_oom(); + + if (arg_root) { + b.root_fd = open(arg_root, O_CLOEXEC|O_DIRECTORY|O_PATH); + if (b.root_fd < 0) + return log_error_errno(errno, "Failed to open root directory '%s': %m", arg_root); + + if (strdup_to(&b.root, arg_root) < 0) + return log_oom(); + } else + b.root_fd = XAT_FDROOT; + + r = path_extract_filename(kernel, &b.kernel_filename); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel); + if (!efi_loader_entry_resource_filename_valid(b.kernel_filename)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Kernel '%s' is not suitable for reference in a boot menu entry.", kernel); + b.kernel_fd = xopenat_full(AT_FDCWD, kernel, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID); + if (b.kernel_fd < 0) + return log_error_errno(b.kernel_fd, "Failed to open kernel path '%s': %m", kernel); + + KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID; + r = inspect_kernel(b.kernel_fd, /* filename= */ NULL, &kit); + if (r == -EBADMSG) + return log_error_errno(r, "Kernel image '%s' is not valid.", kernel); + if (r < 0) + return log_error_errno(r, "Failed to determine kernel image type of '%s': %m", kernel); + if (kit != KERNEL_IMAGE_TYPE_UKI) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Kernel image '%s' is not a UKI.", kernel); + + STRV_FOREACH(x, arg_extras) { + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(*x, &fn); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from path '%s': %m", *x); + if (r == O_DIRECTORY) + return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Extra file path '%s' does not refer to regular file.", *x); + + _cleanup_close_ int fd = -EBADF; + fd = xopenat_full(AT_FDCWD, *x, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID); + if (fd < 0) + return log_error_errno(fd, "Failed to open '%s': %m", *x); + + if (!GREEDY_REALLOC(b.extra, b.n_extra+1)) + return log_oom(); + + b.extra[b.n_extra++] = (ExtraFile) { + .source_fd = TAKE_FD(fd), + .filename = TAKE_PTR(fn), + .temp_fd = -EBADF, + }; + } + + r = acquire_xbootldr( + /* unprivileged_mode= */ false, + &b.dollar_boot_fd, + /* ret_uuid= */ NULL, + /* ret_devid= */ NULL); + if (r < 0) + return r; + if (r > 0) { /* XBOOTLDR has been found */ + assert(arg_xbootldr_path); + + if (arg_root) { + const char *e = path_startswith(arg_xbootldr_path, arg_root); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "XBOOTLDR path '%s' not below specified root '%s', refusing.", arg_xbootldr_path, arg_root); + + r = strdup_to(&b.dollar_boot_path, e); + } else + r = strdup_to(&b.dollar_boot_path, arg_xbootldr_path); + if (r < 0) + return log_oom(); + + b.dollar_boot_source = BOOT_ENTRY_XBOOTLDR; + } else { + /* No XBOOTLDR has been found, look for ESP */ + + r = acquire_esp(/* unprivileged_mode= */ false, + /* graceful= */ false, + &b.dollar_boot_fd, + /* ret_part= */ NULL, + /* ret_pstart= */ NULL, + /* ret_psize= */ NULL, + /* ret_uuid= */ NULL, + /* ret_devid= */ NULL); + if (r < 0) + return r; + + assert(arg_esp_path); + + if (arg_root) { + const char *e = path_startswith(arg_esp_path, arg_root); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "ESP path '%s' not below specified root '%s', refusing.", arg_esp_path, arg_root); + + r = strdup_to(&b.dollar_boot_path, e); + } else + r = strdup_to(&b.dollar_boot_path, arg_esp_path); + if (r < 0) + return log_oom(); + + b.dollar_boot_source = BOOT_ENTRY_ESP; + } + + *ret = TAKE_GENERIC(b, LinkContext, LINK_CONTEXT_NULL); + return 0; +} + +static int link_context_load_etc_machine_id(LinkContext *c) { + int r; + + assert(c); + + r = id128_get_machine_at(c->root_fd, &c->machine_id); + if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r)) /* Not set or empty */ + return 0; + if (r < 0) + return log_error_errno(r, "Failed to get machine-id: %m"); + + log_debug("Loaded machine ID %s from '%s/etc/machine-id'.", SD_ID128_TO_STRING(c->machine_id), strempty(c->root)); + return 0; +} + +static int link_context_pick_entry_token(LinkContext *c) { + int r; + + assert(c); + + r = link_context_load_etc_machine_id(c); + if (r < 0) + return r; + + const char *e = secure_getenv("KERNEL_INSTALL_CONF_ROOT"); + r = boot_entry_token_ensure_at( + e ? XAT_FDROOT : c->root_fd, + e, + c->machine_id, + /* machine_id_is_random= */ false, + &c->entry_token_type, + &c->entry_token); + if (r < 0) + return r; + + log_debug("Using entry token: %s", c->entry_token); + return 0; +} + +static int begin_copy_file( + int source_fd, + const char *filename, + int target_dir_fd, + int *ret_tmpfile_fd, + char **ret_tmpfile_filename) { + + int r; + + assert(source_fd >= 0); + assert(filename); + assert(target_dir_fd >= 0); + assert(ret_tmpfile_fd); + assert(ret_tmpfile_filename); + + if (faccessat(target_dir_fd, filename, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '%s' exists already: %m", filename); + } else { + log_info("'%s' already in place, not copying.", filename); + + *ret_tmpfile_fd = -EBADF; + *ret_tmpfile_filename = NULL; + return 0; + } + + _cleanup_free_ char *t = NULL; + _cleanup_close_ int write_fd = open_tmpfile_linkable_at(target_dir_fd, filename, O_WRONLY|O_CLOEXEC, &t); + if (write_fd < 0) + return log_error_errno(write_fd, "Failed to create '%s': %m", filename); + + CLEANUP_TMPFILE_AT(target_dir_fd, t); + + r = copy_bytes(source_fd, write_fd, UINT64_MAX, COPY_REFLINK|COPY_SEEK0_SOURCE); + if (r < 0) + return log_error_errno(r, "Failed to copy data into '%s': %m", filename); + + (void) copy_times(source_fd, write_fd, /* flags= */ 0); + (void) fchmod(write_fd, 0644); + + *ret_tmpfile_fd = TAKE_FD(write_fd); + *ret_tmpfile_filename = TAKE_PTR(t); + + return 1; +} + +static int begin_write_entry_file( + LinkContext *c, + unsigned profile_nr, + const char *osrelease_text, + const char *profile_text, + Profile *ret) { + + int r; + + assert(c); + assert(osrelease_text); + assert(ret); + + assert(c->entry_token); + assert(c->kernel_filename); + assert(c->loader_entries_dir_fd >= 0); + + _cleanup_free_ char *good_name = NULL, *good_sort_key = NULL, *os_version_id = NULL, *image_version = NULL; + r = bootspec_extract_osrelease( + osrelease_text, + /* These three fields are used by systemd-stub for showing entries + sorting them */ + &good_name, /* human readable */ + /* ret_good_version= */ NULL, + &good_sort_key, + /* These four fields are the raw fields provided in os-release */ + /* ret_os_id= */ NULL, + &os_version_id, + /* ret_image_id= */ NULL, + &image_version); + if (r < 0) + return log_error_errno(r, "Failed to extract name/version/sort-key from os-release data from unified kernel image, refusing."); + + assert(good_name); /* This one is the only field guaranteed to be defined once the above succeeds */ + + _cleanup_free_ char *profile_id = NULL, *profile_title = NULL; + if (profile_text) { + r = parse_env_data( + profile_text, /* size= */ SIZE_MAX, + ".profile", + "ID", &profile_id, + "TITLE", &profile_title); + if (r < 0) + return log_error_errno(r, "Failed to parse profile data from unified kernel image: %m"); + } + + const char *version = c->entry_version ?: image_version ?: os_version_id; + + _cleanup_free_ char *filename = NULL; + r = boot_entry_make_commit_filename( + c->entry_token, + c->entry_commit, + version, + profile_nr, + c->tries_left, + &filename); + if (r < 0) + return log_error_errno(r, "Failed to generate filename for entry file: %m"); + + if (faccessat(c->loader_entries_dir_fd, filename, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '%s' exists: %m", filename); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Boot menu entry '%s' exists already, refusing.", filename); + + log_info("Writing new boot menu entry '%s/loader/entries/%s' for profile %u.", c->dollar_boot_path, filename, profile_nr); + + _cleanup_free_ char *t = NULL; + _cleanup_close_ int write_fd = open_tmpfile_linkable_at(c->loader_entries_dir_fd, filename, O_WRONLY|O_CLOEXEC, &t); + if (write_fd < 0) + return log_error_errno(write_fd, "Failed to create '%s': %m", filename); + + CLEANUP_TMPFILE_AT(c->loader_entries_dir_fd, t); + + _cleanup_free_ char *_title = NULL; + const char *title; + if (profile_title || profile_id) { + _title = strjoin(c->entry_title ?: good_name, " (", profile_title ?: profile_id, ")"); + if (!_title) + return log_oom(); + + title = _title; + } else if (profile_nr > 0) { + _title = asprintf_safe("%s (Profile #%u)", c->entry_title ?: good_name, profile_nr); + if (!_title) + return log_oom(); + + title = _title; + } else + title = c->entry_title ?: good_name; + + /* Do some validation that this will result in a valid type #1 entry before we write this out */ + if (string_has_cc(title, /* ok= */ NULL) || !utf8_is_valid(title)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to generate valid title for new commit: %s", title); + if (string_has_cc(c->kernel_filename, /* ok= */ NULL) || !utf8_is_valid(c->kernel_filename)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UKI filename is not suitable for inclusion in new commit: %s", c->kernel_filename); + + _cleanup_free_ char *text = NULL; + if (asprintf(&text, + "title %s\n" + "uki /%s/%s\n" + "version %" PRIu64 "%s%s\n", + title, + c->entry_token, c->kernel_filename, + c->entry_commit, isempty(version) ? "" : ".", strempty(version)) < 0) + return log_oom(); + + if (good_sort_key && strextendf(&text, "sort-key %s\n", good_sort_key) < 0) + return log_oom(); + + if (profile_nr > 0 && strextendf(&text, "profile %u\n", profile_nr) < 0) + return log_oom(); + + if (!sd_id128_is_null(c->machine_id) && strextendf(&text, "machine-id " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(c->machine_id)) < 0) + return log_oom(); + + FOREACH_ARRAY(x, c->extra, c->n_extra) { + if (string_has_cc(x->filename, /* ok= */ NULL) || !utf8_is_valid(x->filename)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra filename is not suitable for inclusion in new commit: %s", x->filename); + + if (strextendf(&text, + "extra /%s/%s\n", + c->entry_token, + x->filename) < 0) + return log_oom(); + } + + r = loop_write(write_fd, text, /* nbytes= */ SIZE_MAX); + if (r < 0) + return log_error_errno(r, "Failed to write entry file: %m"); + + *ret = (Profile) { + .entry_filename = TAKE_PTR(filename), + .entry_temp_filename = TAKE_PTR(t), + .entry_temp_fd = TAKE_FD(write_fd), + }; + + return 0; +} + +static int finalize_file( + const char *filename, + int target_dir_fd, + int tmpfile_fd, + const char *tmpfile_filename) { + + int r; + + assert(filename); + assert(target_dir_fd >= 0); + + if (tmpfile_fd < 0) /* If the file already existed, we don't move anything into place. */ + return 0; + + r = link_tmpfile_at(tmpfile_fd, target_dir_fd, tmpfile_filename, filename, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC); + if (r < 0) + return log_error_errno(r, "Failed to move from '%s' into place: %m", filename); + + log_info("Installed '%s' into place.", filename); + return 1; +} + +static int link_context_pick_entry_commit(LinkContext *c) { + int r; + + assert(c); + assert(c->loader_entries_dir_fd >= 0); + assert(c->entry_token); + + /* Already have a commit nr? */ + if (c->entry_commit != 0) + return 0; + + _cleanup_close_ int opened_fd = fd_reopen(c->loader_entries_dir_fd, O_DIRECTORY|O_CLOEXEC); + if (opened_fd < 0) + return log_error_errno(opened_fd, "Failed to reopen loader entries dir: %m"); + + _cleanup_free_ DirectoryEntries *dentries = NULL; + r = readdir_all(opened_fd, RECURSE_DIR_IGNORE_DOT, &dentries); + if (r < 0) + return log_error_errno(r, "Failed to read loader entries directory: %m"); + + uint64_t m = 0; /* largest commit number seen */ + FOREACH_ARRAY(i, dentries->entries, dentries->n_entries) { + const struct dirent *de = *i; + + /* We look for files named -commit_[.][.p].conf */ + + if (!dirent_is_file(de)) + continue; + + if (!efi_loader_entry_name_valid(de->d_name)) + continue; + + _cleanup_free_ char *et = NULL; + uint64_t ec; + r = boot_entry_parse_commit_filename(de->d_name, &et, &ec); + if (r < 0) { + log_debug_errno(r, "Cannot extract entry token/commit number from '%s', ignoring.", de->d_name); + continue; + } + + if (!streq(c->entry_token, et)) + continue; + + log_debug("Found existing commit %" PRIu64 ".", ec); + if (ec > m) + m = ec; + } + + assert(m < UINT64_MAX); + uint64_t next = m + 1; + + if (!entry_commit_valid(next)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many commits already in place, refusing."); + + log_debug("Picking commit %" PRIu64 " for new commit.", next); + c->entry_commit = next; + return 0; +} + +static int clean_temporary_files(int fd) { + int r; + + assert(fd >= 0); + + /* Before we create any new files let's clear any possible left-overs from a previous run. We look + * specifically for all temporary files whose name starts with .# because that's what we create, via + * open_tmpfile_linkable_at(). + * + * Ideally, this would not be necessary because O_TMPFILE would ensure that files are not + * materialized before they are fully written. However, vfat currently does not support O_TMPFILE, + * hence we need to clean things up manually. */ + + _cleanup_close_ int dfd = fd_reopen(fd, O_CLOEXEC|O_DIRECTORY); + if (dfd < 0) + return log_error_errno(dfd, "Failed to open directory: %m"); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(dfd, RECURSE_DIR_ENSURE_TYPE, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate contents of directory: %m"); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *e = *i; + + if (e->d_type != DT_REG) + continue; + + if (!startswith_no_case(e->d_name, ".#")) + continue; + + if (unlinkat(dfd, e->d_name, /* flags= */ 0) < 0 && errno != ENOENT) + log_warning_errno(errno, "Failed to remove temporary file '%s', ignoring: %m", e->d_name); + } + + return 0; +} + +static int link_context_unlink_oldest(LinkContext *c) { + int r; + + assert(c); + + /* We only load the entries from the partition we want to make space on (!) */ + _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL; + r = boot_config_load_and_select( + &config, + c->root, + c->dollar_boot_source == BOOT_ENTRY_ESP ? c->dollar_boot_path : NULL, + /* esp_devid= */ 0, + c->dollar_boot_source == BOOT_ENTRY_XBOOTLDR ? c->dollar_boot_path : NULL, + /* xbootldr_devid= */ 0); + if (r < 0) + return r; + + _cleanup_(strv_freep) char **ids = NULL; + r = boot_config_find_oldest_commit( + &config, + c->entry_token, + &ids); + if (r == -ENXIO) + return log_error_errno(r, "No suitable boot menu entry to delete found."); + if (r == -EBUSY) + return log_error_errno(r, "Refusing to remove currently booted boot menu entry."); + if (r < 0) + return log_error_errno(r, "Failed to find suitable oldest boot menu entry: %m"); + + _cleanup_(hashmap_freep) Hashmap *known_files = NULL; + r = boot_config_count_known_files(&config, c->dollar_boot_source, &known_files); + if (r < 0) + return r; + + int ret = 0; + STRV_FOREACH(id, ids) { + const BootEntry *entry = boot_config_find_entry(&config, *id); + if (!entry) + continue; + + RET_GATHER(ret, boot_entry_unlink(entry, c->dollar_boot_path, c->dollar_boot_fd, known_files, /* dry_run= */ false)); + } + + if (ret < 0) + return ret; + + return 1; +} + +static int verify_keep_free(LinkContext *c) { + int r; + + assert(c); + + if (c->keep_free == 0) + return 0; + + uint64_t f; + r = vfs_free_bytes(ASSERT_FD(c->dollar_boot_fd), &f); + if (r < 0) + return log_error_errno(r, "Failed to statvfs() the $BOOT partition: %m"); + + if (f < c->keep_free) + return log_error_errno( + SYNTHETIC_ERRNO(EDQUOT), + "Not installing boot menu entry, free space after installation of %s would be below configured keep free size %s.", + FORMAT_BYTES(f), FORMAT_BYTES(c->keep_free)); + + return 0; +} + +static int run_link_now(LinkContext *c) { + int r; + + assert(c); + assert(c->dollar_boot_fd >= 0); + + _cleanup_free_ char *j = path_join(empty_to_root(c->root), c->dollar_boot_path); + if (!j) + return log_oom(); + + if (c->loader_entries_dir_fd < 0) { + r = chaseat(/* root_fd= */ c->dollar_boot_fd, + /* dir_fd= */ c->dollar_boot_fd, + "loader/entries", + CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &c->loader_entries_dir_fd); + if (r < 0) + return log_error_errno(r, "Failed to pin '/loader/entries' directory below '%s': %m", j); + } + + /* Remove any left-overs from an earlier run before we write new stuff */ + (void) clean_temporary_files(c->loader_entries_dir_fd); + + r = link_context_pick_entry_commit(c); + if (r < 0) + return r; + + log_info("Will create commit %" PRIu64 ".", c->entry_commit); + + if (c->entry_token_dir_fd < 0) { + r = chaseat(/* root_fd= */ c->dollar_boot_fd, + /* dir_fd= */ c->dollar_boot_fd, + c->entry_token, + CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &c->entry_token_dir_fd); + if (r < 0) + return log_error_errno(r, "Failed to pin '/%s' directory below '%s': %m", c->entry_token, j); + } + + /* As above */ + (void) clean_temporary_files(c->entry_token_dir_fd); + + /* Synchronize everything to disk before we verify the disk space, to ensure the counters are + * accurate (some file systems delay accurate counters) */ + (void) syncfs(c->dollar_boot_fd); + + /* Before we start copying things, let's see if there's even a remote chance to get this copied + * in. Note that we do not try to be overly smart here, i.e. we do not try to calculate how much + * extra space we'll need here. Doing that is not trivial since after all the same resources can be + * referenced by multiple entries, which makes copying them multiple times unnecessary. */ + r = verify_keep_free(c); + if (r < 0) + return r; + + for (unsigned p = 0; p < UNIFIED_PROFILES_MAX; p++) { + _cleanup_free_ char *osrelease = NULL, *profile = NULL; + r = pe_find_uki_sections(c->kernel_fd, j, p, &osrelease, &profile, /* ret_cmdline= */ NULL); + if (r < 0) + return r; + if (r == 0) /* this profile does not exist, we are done */ + break; + + if (!GREEDY_REALLOC(c->profiles, c->n_profiles+1)) + return log_oom(); + + r = begin_write_entry_file( + c, + p, + osrelease, + profile, + c->profiles + c->n_profiles); + if (r < 0) + return r; + + c->n_profiles++; + } + + if (c->n_profiles == 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "UKI with no valid profile, refusing."); + + r = begin_copy_file( + c->kernel_fd, + c->kernel_filename, + c->entry_token_dir_fd, + &c->kernel_temp_fd, + &c->kernel_temp_filename); + if (r < 0) + return r; + + FOREACH_ARRAY(x, c->extra, c->n_extra) { + r = begin_copy_file( + x->source_fd, + x->filename, + c->entry_token_dir_fd, + &x->temp_fd, + &x->temp_filename); + if (r < 0) + return r; + } + + /* We copied all files into place, but they are not materialized yet. Let's ensure the data hits the + * disk before we proceed */ + (void) syncfs(c->dollar_boot_fd); + + /* Before we materialize things, let's ensure the space to keep free is not taken */ + r = verify_keep_free(c); + if (r < 0) + return r; + + /* We successfully managed to put all resources we need into the $BOOT partition. Now, let's + * "materialize" them by linking them into the file system. Before this point we'd get rid of every + * file we created on error again. But from now on we switch modes: what we manage to move into place + * we leave in place even on error. These are not lost resources after all, the GC logic implemented + * by "bootctl cleanup" will take care of removing things again if necessary. */ + + r = finalize_file( + c->kernel_filename, + c->entry_token_dir_fd, + c->kernel_temp_fd, + c->kernel_temp_filename); + if (r < 0) + return r; + + c->kernel_temp_fd = safe_close(c->kernel_temp_fd); + c->kernel_temp_filename = mfree(c->kernel_temp_filename); + + FOREACH_ARRAY(x, c->extra, c->n_extra) { + r = finalize_file( + x->filename, + c->entry_token_dir_fd, + x->temp_fd, + x->temp_filename); + if (r < 0) + return r; + + x->temp_fd = safe_close(x->temp_fd); + x->temp_filename = mfree(x->temp_filename); + } + + /* Finally, after all our resources are in place, also materialize the menu entry files themselves */ + FOREACH_ARRAY(profile, c->profiles, c->n_profiles) { + r = finalize_file( + profile->entry_filename, + c->loader_entries_dir_fd, + profile->entry_temp_fd, + profile->entry_temp_filename); + if (r < 0) + return r; + + profile->entry_temp_fd = safe_close(profile->entry_temp_fd); + profile->entry_temp_filename = mfree(profile->entry_temp_filename); + + _cleanup_free_ char *stripped = NULL; + r = boot_filename_extract_tries( + profile->entry_filename, + &stripped, + /* ret_tries_left= */ NULL, + /* ret_tries_done= */ NULL); + if (r < 0) + return log_warning_errno(r, "Failed to extract tries counters from id '%s'", profile->entry_filename); + + if (strv_consume(&c->linked_ids, TAKE_PTR(stripped)) < 0) + return log_oom(); + } + + (void) syncfs(c->dollar_boot_fd); + return 0; +} + +static int run_link(LinkContext *c) { + int r; + + assert(c); + assert(c->dollar_boot_path); + assert(c->dollar_boot_fd >= 0); + + if (c->keep_free == UINT64_MAX) + c->keep_free = KEEP_FREE_BYTES_DEFAULT; + + r = link_context_pick_entry_token(c); + if (r < 0) + return r; + + unsigned n_removals = 0; + for (;;) { + r = run_link_now(c); + if (r < 0) { + if (!ERRNO_IS_NEG_DISK_SPACE(r)) + return r; + } else + break; + + log_notice("Attempt to link entry failed due to exhausted disk space, trying to remove oldest boot menu entry."); + + link_context_unlink_temporary(c); + link_context_clear_profiles(c); + + if (link_context_unlink_oldest(c) <= 0) { + log_warning("Attempted to make space on $BOOT, but this failed, attempt to link entry failed."); + return r; /* propagate original error */ + } + + /* Close entry token dir here, quite possible the unlinking above might have removed it too, in case it was empty */ + c->entry_token_dir_fd = safe_close(c->entry_token_dir_fd); + + log_info("Removing oldest boot menu entry succeeded, will retry to create boot loader menu entry."); + n_removals++; + } + + _cleanup_free_ char *j = strv_join(c->linked_ids, "', '"); + if (!j) + return log_oom(); + + if (n_removals > 0) + log_info("Successfully installed boot loader entries '%s', after removing %u old entries.", j, n_removals); + else + log_info("Successfully installed boot loader entries '%s'.", j); + + return 0; +} + +int verb_link(int argc, char *argv[], uintptr_t data, void *userdata) { + int r; + + assert(argc == 2); + + _cleanup_free_ char *x = NULL; + r = parse_path_argument(argv[1], /* suppress_root= */ false, &x); + if (r < 0) + return r; + + _cleanup_(link_context_done) LinkContext c = LINK_CONTEXT_NULL; + r = link_context_from_cmdline(&c, x); + if (r < 0) + return r; + + return run_link(&c); +} + +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_boot_entry_token_type, BootEntryTokenType, boot_entry_token_type_from_string); + +typedef struct LinkParameters { + LinkContext context; + unsigned root_fd_index; + unsigned kernel_fd_index; + sd_varlink *link; +} LinkParameters; + +static void link_parameters_done(LinkParameters *p) { + assert(p); + + link_context_done(&p->context); +} + +typedef struct ExtraParameters { + ExtraFile extra_file; + unsigned fd_index; +} ExtraParameters; + +static void extra_parameters_done(ExtraParameters *p) { + assert(p); + + extra_file_done(&p->extra_file); +} + +static int json_dispatch_loader_entry_resource_filename(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + char **n = ASSERT_PTR(userdata); + const char *filename; + int r; + + assert(variant); + + r = json_dispatch_const_filename(name, variant, flags, &filename); + if (r < 0) + return r; + + if (filename && !efi_loader_entry_resource_filename_valid(filename)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid boot entry resource filename.", strna(name)); + + if (free_and_strdup(n, filename) < 0) + return json_log_oom(variant, flags); + + return 0; +} + +static int dispatch_extras(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) { + LinkParameters *c = ASSERT_PTR(userdata); + int r; + + if (!sd_json_variant_is_array(v)) + return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + sd_json_variant *i; + JSON_VARIANT_ARRAY_FOREACH(i, v) { + _cleanup_(extra_parameters_done) ExtraParameters xp = { + .extra_file = EXTRA_FILE_NULL, + .fd_index = UINT_MAX, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "filename", SD_JSON_VARIANT_STRING, json_dispatch_loader_entry_resource_filename, offsetof(ExtraParameters, extra_file.filename), SD_JSON_MANDATORY }, + { "fileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(ExtraParameters, fd_index), SD_JSON_MANDATORY }, + {}, + }; + + r = sd_json_dispatch(i, dispatch_table, /* flags= */ 0, &xp); + if (r < 0) + return r; + + xp.extra_file.source_fd = sd_varlink_peek_dup_fd(c->link, xp.fd_index); + if (xp.extra_file.source_fd < 0) + return log_debug_errno(xp.extra_file.source_fd, "Failed to acquire extra fd from Varlink: %m"); + + r = fd_verify_safe_flags(xp.extra_file.source_fd); + if (r < 0) + return sd_varlink_error_invalid_parameter_name(c->link, name); + + r = fd_verify_regular(xp.extra_file.source_fd); + if (r < 0) + return log_debug_errno(r, "Failed to validate that the extra file is a regular file descriptor: %m"); + + if (!GREEDY_REALLOC(c->context.extra, c->context.n_extra+1)) + return log_oom(); + + c->context.extra[c->context.n_extra++] = TAKE_GENERIC(xp.extra_file, ExtraFile, EXTRA_FILE_NULL); + } + + return 0; +} + +int vl_method_link( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(link); + + _cleanup_(link_parameters_done) LinkParameters p = { + .context = LINK_CONTEXT_NULL, + .root_fd_index = UINT_MAX, + .kernel_fd_index = UINT_MAX, + .link = link, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "rootFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, root_fd_index), 0 }, + { "rootDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, voffsetof(p, context.root), 0 }, + { "bootEntryTokenType", SD_JSON_VARIANT_STRING, json_dispatch_boot_entry_token_type, voffsetof(p, context.entry_token_type), 0 }, + { "entryTitle", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(p, context.entry_title), 0 }, + { "entryVersion", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(p, context.entry_version), 0 }, + { "entryCommit", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint64, voffsetof(p, context.entry_commit), 0 }, + { "kernelFilename", SD_JSON_VARIANT_STRING, json_dispatch_loader_entry_resource_filename, voffsetof(p, context.kernel_filename), SD_JSON_MANDATORY }, + { "kernelFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, kernel_fd_index), SD_JSON_MANDATORY }, + { "extraFiles", SD_JSON_VARIANT_ARRAY, dispatch_extras, 0, 0 }, + { "triesLeft", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, context.tries_left), 0 }, + { "keepFree", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, context.keep_free), 0 }, + {}, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.root_fd_index != UINT_MAX) { + p.context.root_fd = sd_varlink_peek_dup_fd(link, p.root_fd_index); + if (p.context.root_fd < 0) + return log_debug_errno(p.context.root_fd, "Failed to acquire root fd from Varlink: %m"); + + r = fd_verify_safe_flags_full(p.context.root_fd, O_DIRECTORY); + if (r < 0) + return sd_varlink_error_invalid_parameter_name(link, "rootFileDescriptor"); + + r = fd_verify_directory(p.context.root_fd); + if (r < 0) + return log_debug_errno(r, "Specified file descriptor does not refer to a directory: %m"); + + if (!p.context.root) { + r = fd_get_path(p.context.root_fd, &p.context.root); + if (r < 0) + return log_debug_errno(r, "Failed to get path of file descriptor: %m"); + + if (empty_or_root(p.context.root)) + p.context.root = mfree(p.context.root); + } + } else if (p.context.root) { + p.context.root_fd = open(p.context.root, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (p.context.root_fd < 0) + return log_debug_errno(errno, "Failed to open '%s': %m", p.context.root); + } else + p.context.root_fd = XAT_FDROOT; + + if (p.context.entry_token_type < 0) + p.context.entry_token_type = BOOT_ENTRY_TOKEN_AUTO; + + if (p.context.entry_title && !efi_loader_entry_title_valid(p.context.entry_title)) + return sd_varlink_error_invalid_parameter_name(link, "entryTitle"); + + if (p.context.entry_version && !version_is_valid_versionspec(p.context.entry_version)) + return sd_varlink_error_invalid_parameter_name(link, "entryVersion"); + + if (p.context.entry_commit != 0 && !entry_commit_valid(p.context.entry_commit)) + return sd_varlink_error_invalid_parameter_name(link, "entryCommit"); + + p.context.kernel_fd = sd_varlink_peek_dup_fd(link, p.kernel_fd_index); + if (p.context.kernel_fd < 0) + return log_debug_errno(p.context.kernel_fd, "Failed to acquire kernel fd from Varlink: %m"); + + r = fd_verify_safe_flags(p.context.kernel_fd); + if (r < 0) + return sd_varlink_error_invalid_parameter_name(link, "kernelFileDescriptor"); + r = fd_verify_regular(p.context.kernel_fd); + if (r < 0) + return log_debug_errno(r, "Failed to validate that kernel image file is a regular file descriptor: %m"); + + /* Refuse non-UKIs for now. */ + KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID; + r = inspect_kernel(p.context.kernel_fd, /* filename= */ NULL, &kit); + if (r == -EBADMSG) + return sd_varlink_error(link, "io.systemd.BootControl.InvalidKernelImage", NULL); + if (r < 0) + return r; + if (kit != KERNEL_IMAGE_TYPE_UKI) + return sd_varlink_error(link, "io.systemd.BootControl.InvalidKernelImage", NULL); + + r = find_xbootldr_and_warn_at( + p.context.root_fd, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &p.context.dollar_boot_path, + &p.context.dollar_boot_fd); + if (r < 0) { + if (r != -ENOKEY) + return r; + + /* No XBOOTLDR found, let's look for ESP then. */ + + r = find_esp_and_warn_at( + p.context.root_fd, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &p.context.dollar_boot_path, + &p.context.dollar_boot_fd); + if (r == -ENOKEY) + return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL); + if (r < 0) + return r; + + p.context.dollar_boot_source = BOOT_ENTRY_ESP; + } else + p.context.dollar_boot_source = BOOT_ENTRY_XBOOTLDR; + + r = run_link(&p.context); + if (r == -EUNATCH) /* no boot entry token is set */ + return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL); + if (r < 0) + return r; + + return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", p.context.linked_ids)); +} diff --git a/src/bootctl/bootctl-link.h b/src/bootctl/bootctl-link.h new file mode 100644 index 0000000000..de64563b87 --- /dev/null +++ b/src/bootctl/bootctl-link.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +int verb_link(int argc, char *argv[], uintptr_t data, void *userdata); + +int vl_method_link(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 67a20814da..c627a7dd07 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -9,6 +9,7 @@ #include "bootctl.h" #include "bootctl-cleanup.h" #include "bootctl-install.h" +#include "bootctl-link.h" #include "bootctl-random-seed.h" #include "bootctl-reboot-to-firmware.h" #include "bootctl-set-efivar.h" @@ -16,6 +17,7 @@ #include "bootctl-uki.h" #include "bootctl-unlink.h" #include "bootctl-util.h" +#include "bootspec-util.h" #include "build.h" #include "crypto-util.h" #include "devnum-util.h" @@ -82,6 +84,12 @@ char *arg_private_key = NULL; KeySourceType arg_private_key_source_type = OPENSSL_KEY_SOURCE_FILE; char *arg_private_key_source = NULL; bool arg_oldest = false; +uint64_t arg_keep_free = KEEP_FREE_BYTES_DEFAULT; +char *arg_entry_title = NULL; +char *arg_entry_version = NULL; +uint64_t arg_entry_commit = 0; +char **arg_extras = NULL; +unsigned arg_tries_left = UINT_MAX; STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep); @@ -95,6 +103,9 @@ STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep); STATIC_DESTRUCTOR_REGISTER(arg_certificate_source, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep); +STATIC_DESTRUCTOR_REGISTER(arg_entry_title, freep); +STATIC_DESTRUCTOR_REGISTER(arg_entry_version, freep); +STATIC_DESTRUCTOR_REGISTER(arg_extras, strv_freep); static const char* const install_source_table[_INSTALL_SOURCE_MAX] = { [INSTALL_SOURCE_IMAGE] = "image", @@ -366,6 +377,9 @@ VERB_SCOPE_NOARG(, verb_list, "list", VERB_SCOPE(, verb_unlink, "unlink", "ID", VERB_ANY, 2, 0, "Remove boot loader entry"); +VERB_SCOPE(, verb_link, "link", "KERNEL", 2, 2, 0, + "Create boot loader entry for specified kernel"); + VERB_SCOPE_NOARG(, verb_cleanup, "cleanup", "Remove files in ESP not referenced in any boot entry"); @@ -641,8 +655,115 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { return r; break; + + OPTION_LONG("keep-free", "BYTES", + "How much space to keep free on ESP/XBOOTLDR"): + + if (isempty(opts.arg)) + arg_keep_free = KEEP_FREE_BYTES_DEFAULT; + else { + r = parse_size(opts.arg, 1024, &arg_keep_free); + if (r < 0) + return log_error_errno(r, "Failed to parse --keep-free=: %s", opts.arg); + } + + break; + + OPTION_LONG("entry-title", "TITLE", + "Selects the entry title for the new boot menu entry"): + + if (isempty(opts.arg)) { + arg_entry_title = mfree(arg_entry_title); + break; + } + + if (!efi_loader_entry_title_valid(opts.arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid boot menu entry title: %s", opts.arg); + + r = free_and_strdup_warn(&arg_entry_title, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("entry-version", "VERSION", + "Selects the entry version for the new boot menu entry"): + if (isempty(opts.arg)) { + arg_entry_version = mfree(arg_entry_version); + break; + } + + if (!version_is_valid_versionspec(opts.arg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid boot menu entry version: %s", opts.arg); + + r = free_and_strdup_warn(&arg_entry_version, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("entry-commit", "NR", + "Selects the entry commit version for the new boot menu entry"): { + if (isempty(opts.arg)) { + arg_entry_commit = 0; + break; + } + + uint64_t n; + r = safe_atou64(opts.arg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse --entry-commit= parameter: %s", opts.arg); + if (!entry_commit_valid(n)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid entry commit number."); + + arg_entry_commit = n; + break; } + OPTION('X', "extra", "PATH", + "Pass extra resource (confext, sysext, credential) to the invoked UKI of the boot menu entry"): { + + if (isempty(opts.arg)) { + arg_extras = strv_free(arg_extras); + break; + } + + _cleanup_free_ char *x = NULL; + r = parse_path_argument(opts.arg, /* suppress_root= */ false, &x); + if (r < 0) + return r; + + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(x, &fn); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from '%s': %m", x); + if (!efi_loader_entry_resource_filename_valid(fn)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra filename '%s' is not suitable for reference in a boot menu entry.", fn); + + r = strv_consume(&arg_extras, TAKE_PTR(x)); + if (r < 0) + return log_oom(); + + strv_uniq(arg_extras); + break; + } + + OPTION_LONG("tries-left", "NR", + "Set boot menu entries tries-left counter to the specified value"): { + if (isempty(opts.arg)) { + arg_tries_left = UINT_MAX; + break; + } + + unsigned u; + r = safe_atou(opts.arg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse tries left counter: %s", opts.arg); + if (u >= UINT_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Tries left counter too large, refusing: %u", u); + + arg_tries_left = u; + break; + }} + char **args = option_parser_get_args(&opts); if (!!arg_print_esp_path + !!arg_print_dollar_boot_path + (arg_print_root_device > 0) + arg_print_loader_path + arg_print_stub_path + arg_print_efi_architecture > 1) @@ -711,6 +832,7 @@ static int vl_server(void) { "io.systemd.BootControl.SetRebootToFirmware", vl_method_set_reboot_to_firmware, "io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware, "io.systemd.BootControl.Install", vl_method_install, + "io.systemd.BootControl.Link", vl_method_link, "io.systemd.BootControl.Unlink", vl_method_unlink); if (r < 0) return log_error_errno(r, "Failed to bind Varlink methods: %m"); diff --git a/src/bootctl/bootctl.h b/src/bootctl/bootctl.h index ea097ba329..b478ae0ec1 100644 --- a/src/bootctl/bootctl.h +++ b/src/bootctl/bootctl.h @@ -52,6 +52,12 @@ extern char *arg_private_key; extern KeySourceType arg_private_key_source_type; extern char *arg_private_key_source; extern bool arg_oldest; +extern uint64_t arg_keep_free; +extern char *arg_entry_title; +extern char *arg_entry_version; +extern uint64_t arg_entry_commit; +extern char **arg_extras; +extern unsigned arg_tries_left; static inline const char* arg_dollar_boot_path(void) { /* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */ @@ -68,3 +74,7 @@ int acquire_xbootldr(int unprivileged_mode, int *ret_fd, sd_id128_t *ret_uuid, d * string, but we limit the length to something reasonable to prevent from the firmware * having to deal with a potentially too long string. */ #define EFI_BOOT_OPTION_DESCRIPTION_MAX ((size_t) 255) + +/* Before we "materialize" a new entry, let's ensure we have this much space free still on the partition, by + * default */ +#define KEEP_FREE_BYTES_DEFAULT (5U * U64_MB) diff --git a/src/bootctl/meson.build b/src/bootctl/meson.build index ff33cde3f6..06137bdae0 100644 --- a/src/bootctl/meson.build +++ b/src/bootctl/meson.build @@ -3,6 +3,7 @@ bootctl_sources = files( 'bootctl.c', 'bootctl-install.c', + 'bootctl-link.c', 'bootctl-random-seed.c', 'bootctl-reboot-to-firmware.c', 'bootctl-set-efivar.c', diff --git a/src/shared/efi-loader.c b/src/shared/efi-loader.c index ce10a44d34..20e4719bb0 100644 --- a/src/shared/efi-loader.c +++ b/src/shared/efi-loader.c @@ -433,3 +433,14 @@ bool efi_loader_entry_name_valid(const char *s) { return in_charset(s, ALPHANUMERICAL "+-_.@"); } + +bool efi_loader_entry_title_valid(const char *s) { + return string_is_safe(s, /* flags= */ 0); +} + +bool efi_loader_entry_resource_filename_valid(const char *s) { + /* Validates file names so that they are safe for their inclusion in boot loader type #1 + * entries. i.e. may not contain CCs, and should be ASCII */ + + return string_is_safe(s, STRING_ASCII|STRING_FILENAME); +} diff --git a/src/shared/efi-loader.h b/src/shared/efi-loader.h index abf8bdc49e..c51c2dbb2f 100644 --- a/src/shared/efi-loader.h +++ b/src/shared/efi-loader.h @@ -23,3 +23,5 @@ int efi_loader_update_entry_one_shot_cache(char **cache, struct stat *cache_stat int efi_get_variable_id128(const char *variable, sd_id128_t *ret); bool efi_loader_entry_name_valid(const char *s); +bool efi_loader_entry_title_valid(const char *s); +bool efi_loader_entry_resource_filename_valid(const char *s); diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c index 42f7b53492..c16a295d89 100644 --- a/src/shared/varlink-io.systemd.BootControl.c +++ b/src/shared/varlink-io.systemd.BootControl.c @@ -27,7 +27,7 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_FIELD_COMMENT("The location of the local addon."), SD_VARLINK_DEFINE_FIELD(localAddon, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("The command line options by the addon."), - SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, 0)); + SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_STRUCT_TYPE( BootEntry, @@ -147,6 +147,40 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_FIELD_COMMENT("If true, remove the oldest entry."), SD_VARLINK_DEFINE_INPUT(oldest, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); +static SD_VARLINK_DEFINE_STRUCT_TYPE( + BootEntryExtraFile, + SD_VARLINK_FIELD_COMMENT("The name of the extra file"), + SD_VARLINK_DEFINE_FIELD(filename, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors, pointing to a file descriptor referencing the extra file."), + SD_VARLINK_DEFINE_FIELD(fileDescriptor, SD_VARLINK_INT, 0)); + +static SD_VARLINK_DEFINE_METHOD( + Link, + SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors passed along with this message, pointing to file descriptor to root file system to operate on"), + SD_VARLINK_DEFINE_INPUT(rootFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Root directory to operate relative to. If both this and rootFileDescriptor is specified, this is purely informational. If only this is specified, it is what will be used."), + SD_VARLINK_DEFINE_INPUT(rootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Selects how to identify boot entries"), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(bootEntryTokenType, BootEntryTokenType, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The entry title for the newly created boot menu entry"), + SD_VARLINK_DEFINE_INPUT(entryTitle, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The entry version for the newly created boot menu entry"), + SD_VARLINK_DEFINE_INPUT(entryVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The commit number for the newly created boot menu entry"), + SD_VARLINK_DEFINE_INPUT(entryCommit, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Target filename for the kernel image (UKI) in the $BOOT partition"), + SD_VARLINK_DEFINE_INPUT(kernelFilename, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors passed along with this message, pointing to file descriptor to the kernel image to copy"), + SD_VARLINK_DEFINE_INPUT(kernelFileDescriptor, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("An array of 'extra' files for this entry, i.e. credentials, confexts, sysexts, addons."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(extraFiles, BootEntryExtraFile, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("What to set the triesLeft counter of the boot menu entry to initially."), + SD_VARLINK_DEFINE_INPUT(triesLeft, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("How much space to always keep free on ESP/XBOOTLDR. Defaults to 1 MiB"), + SD_VARLINK_DEFINE_INPUT(keepFree, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The IDs of the created boot loader entries."), + SD_VARLINK_DEFINE_OUTPUT(ids, SD_VARLINK_STRING, SD_VARLINK_ARRAY)); + static SD_VARLINK_DEFINE_ERROR( RebootToFirmwareNotSupported); @@ -162,6 +196,9 @@ static SD_VARLINK_DEFINE_ERROR( static SD_VARLINK_DEFINE_ERROR( BootEntryTokenUnavailable); +static SD_VARLINK_DEFINE_ERROR( + InvalidKernelImage); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_BootControl, "io.systemd.BootControl", @@ -172,6 +209,8 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_type_BootEntrySource, SD_VARLINK_SYMBOL_COMMENT("A structure encapsulating an addon of a boot entry"), &vl_type_BootEntryAddon, + SD_VARLINK_SYMBOL_COMMENT("An additional file to install"), + &vl_type_BootEntryExtraFile, SD_VARLINK_SYMBOL_COMMENT("A structure encapsulating a boot entry"), &vl_type_BootEntry, SD_VARLINK_SYMBOL_COMMENT("The operation to execute"), @@ -188,6 +227,8 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_method_Install, SD_VARLINK_SYMBOL_COMMENT("Unlink a boot menu item"), &vl_method_Unlink, + SD_VARLINK_SYMBOL_COMMENT("Install a kernel as boot menu item"), + &vl_method_Link, SD_VARLINK_SYMBOL_COMMENT("SetRebootToFirmware() and GetRebootToFirmware() return this if the firmware does not actually support the reboot-to-firmware-UI concept."), &vl_error_RebootToFirmwareNotSupported, SD_VARLINK_SYMBOL_COMMENT("No boot entry defined."), @@ -197,4 +238,6 @@ SD_VARLINK_DEFINE_INTERFACE( SD_VARLINK_SYMBOL_COMMENT("Neither ESP nor XBOOTLDR found, hence no $BOOT location identified."), &vl_error_NoDollarBootFound, SD_VARLINK_SYMBOL_COMMENT("The selected boot entry token could not be determined."), - &vl_error_BootEntryTokenUnavailable); + &vl_error_BootEntryTokenUnavailable, + SD_VARLINK_SYMBOL_COMMENT("The specified kernel image is not valid."), + &vl_error_InvalidKernelImage);