From 38433a6d06ef4b3fa1a8e0f92fc1a1e22cdbc499 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Sep 2025 12:11:00 +0200 Subject: [PATCH 1/7] bootctl: rework bootctl-install.c in preparation of varlinkification This primarily introduces a context object for each operation, so that we later can instantiate one for each varlink op we execute, and can safely lifecycle all operation parameters for each subequent call. This also reworks the root dir handling to be fd based. This drops explicit CHASE_TRIGGER_AUTOFS from a bunch of chase() calls that operate within the ESP/XBOOTLDR, while it keeps them in place for the chase() calls that find the top-level ESP/XBOOTLDR inode. This reflects the fact that we explicitly support autofs for the ESP/XBOOTLDR itself, but below it expect no further mounts, just plain VFAT. This changes behaviour of the interaction of $KERNEL_INSTALL_CONF_ROOT and --root=: the former will now be taken relative to the host root, and will no longer be affected by --root=. This follows similar behaviour in kernel-install, where it is very explicitly documented in the man page (the bootclt man page does not document this). This is strictly speaking a compat breakage, but i think a very minor, niche one, and I think the pain afflicted by this change is probably neglible compare to the unsystematic behaviour comapred to kernel-install. --- src/bootctl/bootctl-install.c | 1351 +++++++++++++++++++++++---------- 1 file changed, 944 insertions(+), 407 deletions(-) diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index ac15ce8020e..4abaa1aa83e 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -31,138 +31,411 @@ #include "pe-binary.h" #include "rm-rf.h" #include "stat-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" -#include "sync-util.h" #include "time-util.h" #include "tmpfile-util.h" #include "umask-util.h" #include "utf8.h" -static int load_etc_machine_id(void) { +typedef enum InstallOperation { + INSTALL_NEW, + INSTALL_UPDATE, + INSTALL_REMOVE, + INSTALL_TEST, + _INSTALL_OPERATION_MAX, + _INSTALL_OPERATION_INVALID = -1, +} InstallOperation; + +typedef struct InstallContext { + InstallOperation operation; + bool graceful; + char *root; + int root_fd; + sd_id128_t machine_id; + char *install_layout; + BootEntryTokenType entry_token_type; + char *entry_token; + int make_entry_directory; /* tri-state */ + InstallSource install_source; + char *esp_path; + int esp_fd; + uint32_t esp_part; + uint64_t esp_pstart; + uint64_t esp_psize; + sd_id128_t esp_uuid; + char *xbootldr_path; + int xbootldr_fd; +#if HAVE_OPENSSL + X509 *secure_boot_certificate; + EVP_PKEY *secure_boot_private_key; +#endif + int touch_variables; /* tri-state */ +} InstallContext; + +#define INSTALL_CONTEXT_NULL \ + (InstallContext) { \ + .operation = _INSTALL_OPERATION_INVALID, \ + .root_fd = -EBADF, \ + .entry_token_type = _BOOT_ENTRY_TOKEN_TYPE_INVALID, \ + .make_entry_directory = -1, \ + .install_source = _INSTALL_SOURCE_INVALID, \ + .esp_part = UINT32_MAX, \ + .esp_pstart = UINT64_MAX, \ + .esp_psize = UINT64_MAX, \ + .esp_fd = -EBADF, \ + .xbootldr_fd = -EBADF, \ + .touch_variables = -1, \ + } + +static void install_context_done(InstallContext *c) { + assert(c); + + c->root = mfree(c->root); + c->root_fd = safe_close(c->root_fd); + c->install_layout = mfree(c->install_layout); + c->entry_token = mfree(c->entry_token); + c->esp_path = mfree(c->esp_path); + c->esp_fd = safe_close(c->esp_fd); + c->xbootldr_path = mfree(c->xbootldr_path); + c->xbootldr_fd = safe_close(c->xbootldr_fd); +#if HAVE_OPENSSL + if (c->secure_boot_private_key) { + EVP_PKEY_free(c->secure_boot_private_key); + c->secure_boot_private_key = NULL; + } + if (c->secure_boot_certificate) { + X509_free(c->secure_boot_certificate); + c->secure_boot_certificate = NULL; + } +#endif +} + +static int install_context_from_cmdline( + InstallContext *ret, + InstallOperation operation) { + int r; - r = sd_id128_get_machine(&arg_machine_id); + assert(ret); + assert(operation >= 0); + assert(operation < _INSTALL_OPERATION_MAX); + + _cleanup_(install_context_done) InstallContext b = INSTALL_CONTEXT_NULL; + b.operation = operation; + b.graceful = arg_graceful() == ARG_GRACEFUL_FORCE || + (operation == INSTALL_UPDATE && arg_graceful() != ARG_GRACEFUL_NO); + b.machine_id = arg_machine_id; + b.entry_token_type = arg_entry_token_type; + b.make_entry_directory = arg_make_entry_directory; + b.install_source = arg_install_source; + + if (strdup_to(&b.entry_token, arg_entry_token) < 0 || + strdup_to(&b.install_layout, arg_install_layout) < 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); + + r = strdup_to(&b.root, arg_root); + if (r < 0) + return log_oom(); + } else + b.root_fd = XAT_FDROOT; + + r = acquire_esp(/* unprivileged_mode= */ false, + b.graceful, + &b.esp_part, + &b.esp_pstart, + &b.esp_psize, + &b.esp_uuid, + /* ret_devid= */ NULL); + /* If --graceful is specified and we can't find an ESP, handle this cleanly */ + if (r < 0 && (!b.graceful || r != -ENOKEY)) + return r; + + if (r >= 0) { /* An ESP has been found */ + 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.esp_path, e); + } else + r = strdup_to(&b.esp_path, arg_esp_path); + if (r < 0) + return log_oom(); + } + + r = acquire_xbootldr( + /* unprivileged_mode= */ false, + /* 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.xbootldr_path, e); + } else + r = strdup_to(&b.xbootldr_path, arg_xbootldr_path); + if (r < 0) + return log_oom(); + } + + *ret = TAKE_GENERIC(b, InstallContext, INSTALL_CONTEXT_NULL); + + return !!ret->esp_path; /* return positive if we found an ESP */ +} + +static int acquire_esp_fd(InstallContext *c) { + int r; + + assert(c); + + if (c->esp_fd >= 0) + return c->esp_fd; + + assert(c->esp_path); + + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) + return log_oom(); + + r = chaseat(c->root_fd, + c->esp_path, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &c->esp_fd); + if (r < 0) + return log_error_errno(r, "Failed to open ESP '%s': %m", j); + + return c->esp_fd; +} + +static int acquire_dollar_boot_fd(InstallContext *c) { + int r; + + assert(c); + + if (c->xbootldr_fd >= 0) + return c->xbootldr_fd; + + if (!c->xbootldr_path) + return acquire_esp_fd(c); + + _cleanup_free_ char *j = path_join(c->root, c->xbootldr_path); + if (!j) + return log_oom(); + + r = chaseat(c->root_fd, + c->xbootldr_path, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &c->xbootldr_fd); + if (r < 0) + return log_error_errno(r, "Failed to open XBOOTLDR '%s': %m", j); + + return c->xbootldr_fd; +} + +static const char* dollar_boot_path(InstallContext *c) { + assert(c); + + return c->xbootldr_path ?: c->esp_path; +} + +static bool should_touch_install_variables(InstallContext *c) { + assert(c); + + if (c->touch_variables >= 0) + return c->touch_variables; + + if (!is_efi_boot()) /* NB: this internally checks if we run in a container */ + return false; + + return empty_or_root(c->root); +} + +static int load_etc_machine_id(InstallContext *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 /etc/machine-id.", SD_ID128_TO_STRING(arg_machine_id)); + log_debug("Loaded machine ID %s from '%s/etc/machine-id'.", strempty(c->root), SD_ID128_TO_STRING(c->machine_id)); return 0; } -static int load_etc_machine_info(void) { +static int load_etc_machine_info(InstallContext *c) { /* systemd v250 added support to store the kernel-install layout setting and the machine ID to use * for setting up the ESP in /etc/machine-info. The newer /etc/kernel/entry-token file, as well as * the $layout field in /etc/kernel/install.conf are better replacements for this though, hence this * has been deprecated and is only returned for compatibility. */ - _cleanup_free_ char *p = NULL, *s = NULL, *layout = NULL; + _cleanup_free_ char *s = NULL, *layout = NULL; int r; - p = path_join(arg_root, "/etc/machine-info"); - if (!p) + assert(c); + + _cleanup_free_ char *j = path_join(c->root, "/etc/machine-info"); + if (!j) return log_oom(); - r = parse_env_file(NULL, p, - "KERNEL_INSTALL_LAYOUT", &layout, - "KERNEL_INSTALL_MACHINE_ID", &s); - if (r == -ENOENT) + _cleanup_close_ int fd = + chase_and_openat( + c->root_fd, + "/etc/machine-info", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, + O_RDONLY|O_CLOEXEC, + /* ret_path= */ NULL); + if (fd == -ENOENT) return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open '%s': %m", j); + + r = parse_env_file_fd( + fd, "/etc/machine-info", + "KERNEL_INSTALL_LAYOUT", &layout, + "KERNEL_INSTALL_MACHINE_ID", &s); if (r < 0) - return log_error_errno(r, "Failed to parse /etc/machine-info: %m"); + return log_error_errno(r, "Failed to parse '%s': %m", j); if (!isempty(s)) { if (!arg_quiet) - log_notice("Read $KERNEL_INSTALL_MACHINE_ID from /etc/machine-info. " - "Please move it to /etc/kernel/entry-token."); + log_notice("Read $KERNEL_INSTALL_MACHINE_ID from '%s'. " + "Please move it to '%s/etc/kernel/entry-token'.", j, strempty(c->root)); - r = sd_id128_from_string(s, &arg_machine_id); + r = sd_id128_from_string(s, &c->machine_id); if (r < 0) - return log_error_errno(r, "Failed to parse KERNEL_INSTALL_MACHINE_ID=%s in /etc/machine-info: %m", s); + return log_error_errno(r, "Failed to parse KERNEL_INSTALL_MACHINE_ID=\"%s\" in '%s': %m", s, j); - log_debug("Loaded KERNEL_INSTALL_MACHINE_ID=%s from /etc/machine-info.", - SD_ID128_TO_STRING(arg_machine_id)); + log_debug("Loaded KERNEL_INSTALL_MACHINE_ID=\"%s\" from '%s'.", + SD_ID128_TO_STRING(c->machine_id), j); } if (!isempty(layout)) { if (!arg_quiet) - log_notice("Read $KERNEL_INSTALL_LAYOUT from /etc/machine-info. " - "Please move it to the layout= setting of /etc/kernel/install.conf."); + log_notice("Read $KERNEL_INSTALL_LAYOUT from '%s'. " + "Please move it to the layout= setting of '%s/etc/kernel/install.conf'.", j, strempty(c->root)); - log_debug("KERNEL_INSTALL_LAYOUT=%s is specified in /etc/machine-info.", layout); - free_and_replace(arg_install_layout, layout); + log_debug("KERNEL_INSTALL_LAYOUT=\"%s\" is specified in '%s'.", layout, j); + free_and_replace(c->install_layout, layout); } return 0; } -static int load_kernel_install_layout(void) { +static int load_kernel_install_layout(InstallContext *c) { _cleanup_free_ char *layout = NULL; int r; - r = load_kernel_install_conf(arg_root, - secure_getenv("KERNEL_INSTALL_CONF_ROOT"), - /* ret_machine_id= */ NULL, - /* ret_boot_root= */ NULL, - &layout, - /* ret_initrd_generator= */ NULL, - /* ret_uki_generator= */ NULL); + assert(c); + + const char *e = secure_getenv("KERNEL_INSTALL_CONF_ROOT"); + r = load_kernel_install_conf_at( + e ? NULL : c->root, + e ? XAT_FDROOT : c->root_fd, + e, + /* ret_machine_id= */ NULL, + /* ret_boot_root= */ NULL, + &layout, + /* ret_initrd_generator= */ NULL, + /* ret_uki_generator= */ NULL); if (r <= 0) return r; if (!isempty(layout)) { - log_debug("layout=%s is specified in config.", layout); - free_and_replace(arg_install_layout, layout); + log_debug("layout=\"%s\" is specified in config.", layout); + free_and_replace(c->install_layout, layout); } return 0; } -static bool use_boot_loader_spec_type1(void) { +static bool use_boot_loader_spec_type1(InstallContext *c) { + assert(c); /* If the layout is not specified, or if it is set explicitly to "bls" we assume Boot Loader * Specification Type #1 is the chosen format for our boot loader entries */ - return !arg_install_layout || streq(arg_install_layout, "bls"); + return !c->install_layout || streq(c->install_layout, "bls"); } -static int settle_make_entry_directory(void) { +static int settle_make_entry_directory(InstallContext *c) { int r; - r = load_etc_machine_id(); + assert(c); + + r = load_etc_machine_id(c); if (r < 0) return r; - r = load_etc_machine_info(); + r = load_etc_machine_info(c); if (r < 0) return r; - r = load_kernel_install_layout(); + r = load_kernel_install_layout(c); if (r < 0) return r; - r = settle_entry_token(); + 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; - bool layout_type1 = use_boot_loader_spec_type1(); - if (arg_make_entry_directory < 0) { /* Automatic mode */ + log_debug("Using entry token: %s", c->entry_token); + + bool layout_type1 = use_boot_loader_spec_type1(c); + if (c->make_entry_directory < 0) { /* Automatic mode */ if (layout_type1) { - if (arg_entry_token_type == BOOT_ENTRY_TOKEN_MACHINE_ID) { - r = path_is_temporary_fs("/etc/machine-id"); - if (r < 0) - return log_debug_errno(r, "Couldn't determine whether /etc/machine-id is on a temporary file system: %m"); + if (c->entry_token_type == BOOT_ENTRY_TOKEN_MACHINE_ID) { + _cleanup_free_ char *j = path_join(c->root, "/etc/machine-id"); + if (!j) + return log_oom(); - arg_make_entry_directory = r == 0; + _cleanup_close_ int fd = -EBADF; + r = chaseat(c->root_fd, + "/etc/machine-id", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, + /* ret_path= */ NULL, + &fd); + if (r < 0) + return log_debug_errno(r, "Unable to open '%s': %m", j); + + r = fd_is_temporary_fs(fd); + if (r < 0) + return log_debug_errno(r, "Couldn't determine whether '%s' is on a temporary file system: %m", j); + + c->make_entry_directory = r == 0; } else - arg_make_entry_directory = true; + c->make_entry_directory = true; } else - arg_make_entry_directory = false; + c->make_entry_directory = false; } - if (arg_make_entry_directory > 0 && !layout_type1) + if (c->make_entry_directory > 0 && !layout_type1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "KERNEL_INSTALL_LAYOUT=%s is configured, but Boot Loader Specification Type #1 entry directory creation was requested.", - arg_install_layout); + "KERNEL_INSTALL_LAYOUT=\"%s\" is configured, but Boot Loader Specification Type #1 entry directory creation was requested.", + c->install_layout); return 0; } @@ -229,77 +502,81 @@ static int version_check(int fd_from, const char *from, int fd_to, const char *t return 0; } -static int copy_file_with_version_check(const char *from, const char *to, bool force) { - _cleanup_close_ int fd_from = -EBADF, fd_to = -EBADF; - _cleanup_free_ char *t = NULL; +static int copy_file_with_version_check( + const char *source_path, + int source_fd, + const char *dest_path, + int dest_parent_fd, + const char *dest_filename, + int dest_fd, + bool force) { + int r; - fd_from = open(from, O_RDONLY|O_CLOEXEC|O_NOCTTY); - if (fd_from < 0) - return log_error_errno(errno, "Failed to open \"%s\" for reading: %m", from); + assert(source_path); + assert(source_fd >= 0); + assert(dest_path); + assert(dest_parent_fd >= 0); + assert(dest_filename); - if (!force) { - fd_to = open(to, O_RDONLY|O_CLOEXEC|O_NOCTTY); - if (fd_to < 0) { - if (errno != ENOENT) - return log_error_errno(errno, "Failed to open \"%s\" for reading: %m", to); - } else { - r = version_check(fd_from, from, fd_to, to); - if (r < 0) - return r; - - if (lseek(fd_from, 0, SEEK_SET) < 0) - return log_error_errno(errno, "Failed to seek in \"%s\": %m", from); - - fd_to = safe_close(fd_to); - } + if (!force && dest_fd >= 0) { + r = version_check(source_fd, source_path, dest_fd, dest_path); + if (r < 0) + return r; } - r = tempfn_random(to, NULL, &t); + _cleanup_free_ char *t = NULL; + _cleanup_close_ int write_fd = -EBADF; + write_fd = open_tmpfile_linkable_at(dest_parent_fd, dest_filename, O_WRONLY|O_CLOEXEC, &t); + if (write_fd < 0) + return log_error_errno(write_fd, "Failed to open \"%s\" for writing: %m", dest_path); + + CLEANUP_TMPFILE_AT(dest_parent_fd, t); + + /* Reset file offset before we start copying, since we copy this file multiple times, and the offset + * might be left at the end of the file. (Resetting before rather than after a copy attempt is safer + * because a previous attempt might have failed half-way, leaving the file offset at some undefined + * place.) */ + if (lseek(source_fd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek in \"%s\": %m", source_path); + + r = copy_bytes(source_fd, write_fd, UINT64_MAX, COPY_REFLINK); if (r < 0) - return log_oom(); + return log_error_errno(r, "Failed to copy data from \"%s\" to \"%s\": %m", source_path, dest_path); - WITH_UMASK(0000) { - fd_to = open(t, O_WRONLY|O_CREAT|O_CLOEXEC|O_EXCL|O_NOFOLLOW, 0644); - if (fd_to < 0) - return log_error_errno(errno, "Failed to open \"%s\" for writing: %m", t); - } + (void) copy_times(source_fd, write_fd, /* flags= */ 0); + (void) fchmod(write_fd, 0644); - r = copy_bytes(fd_from, fd_to, UINT64_MAX, COPY_REFLINK); - if (r < 0) { - (void) unlink(t); - return log_error_errno(r, "Failed to copy data from \"%s\" to \"%s\": %m", from, t); - } + r = link_tmpfile_at(write_fd, dest_parent_fd, t, dest_filename, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC); + if (r < 0) + return log_error_errno(r, "Failed to move data from \"%s\" to \"%s\": %m", source_path, dest_path); - (void) copy_times(fd_from, fd_to, 0); - - r = fsync_full(fd_to); - if (r < 0) { - (void) unlink(t); - return log_error_errno(r, "Failed to copy data from \"%s\" to \"%s\": %m", from, t); - } - - r = RET_NERRNO(renameat(AT_FDCWD, t, AT_FDCWD, to)); - if (r < 0) { - (void) unlink(t); - return log_error_errno(r, "Failed to rename \"%s\" to \"%s\": %m", t, to); - } - - log_info("Copied \"%s\" to \"%s\".", from, to); + t = mfree(t); /* disarm CLEANUP_TMPFILE_AT() */ + log_info("Copied \"%s\" to \"%s\".", source_path, dest_path); return 0; } -static int mkdir_one(const char *prefix, const char *suffix) { - _cleanup_free_ char *p = NULL; +static int mkdir_one(const char *root, int root_fd, const char *path) { + int r; - p = path_join(prefix, suffix); - if (mkdir(p, 0700) < 0) { - if (errno != EEXIST) - return log_error_errno(errno, "Failed to create \"%s\": %m", p); - } else - log_info("Created \"%s\".", p); + assert(root); + assert(root_fd >= 0); + assert(path); + _cleanup_free_ char *p = path_join(empty_to_root(root), path); + if (!p) + return log_oom(); + + r = chaseat(root_fd, + path, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + /* ret_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to create \"%s\": %m", p); + + log_info("Created directory \"%s\".", p); return 0; } @@ -322,11 +599,14 @@ static const char *const dollar_boot_subdirs[] = { NULL }; -static int create_subdirs(const char *root, const char * const *subdirs) { +static int create_subdirs(const char *root, int root_fd, const char * const *subdirs) { int r; + assert(root); + assert(root_fd >= 0); + STRV_FOREACH(i, subdirs) { - r = mkdir_one(root, *i); + r = mkdir_one(root, root_fd, *i); if (r < 0) return r; } @@ -335,22 +615,35 @@ static int create_subdirs(const char *root, const char * const *subdirs) { } static int update_efi_boot_binaries( - const char *esp_path, + InstallContext *c, const char *source_path, + int source_fd, const char *ignore_filename) { - _cleanup_closedir_ DIR *d = NULL; - _cleanup_free_ char *p = NULL; int r, ret = 0; - assert(esp_path); + assert(c); assert(source_path); - r = chase_and_opendir("/EFI/BOOT", esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, &p, &d); + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) + return log_oom(); + + _cleanup_closedir_ DIR *d = NULL; + r = chase_and_opendirat( + esp_fd, + "/EFI/BOOT", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &d); if (r == -ENOENT) return 0; if (r < 0) - return log_error_errno(r, "Failed to open directory \"%s/EFI/BOOT\": %m", esp_path); + return log_error_errno(r, "Failed to open directory \"%s/EFI/BOOT\": %m", j); FOREACH_DIRENT(de, d, break) { _cleanup_close_ int fd = -EBADF; @@ -361,23 +654,23 @@ static int update_efi_boot_binaries( if (strcaseeq_ptr(ignore_filename, de->d_name)) continue; - fd = xopenat_full(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW, XO_REGULAR, /* mode= */ 0); + fd = xopenat_full(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW, XO_REGULAR, /* mode= */ MODE_INVALID); if (fd < 0) - return log_error_errno(fd, "Failed to open \"%s/%s\" for reading: %m", p, de->d_name); + return log_error_errno(fd, "Failed to open \"%s/%s\" for reading: %m", j, de->d_name); r = pe_is_native_fd(fd); if (r < 0) { - log_warning_errno(r, "Failed to detect if \"%s/%s\" is native architecture, ignoring: %m", p, de->d_name); + log_warning_errno(r, "Failed to detect if \"%s/%s\" is for native architecture, ignoring: %m", j, de->d_name); continue; } if (r == 0) continue; - _cleanup_free_ char *dest_path = path_join(p, de->d_name); + _cleanup_free_ char *dest_path = path_join(j, "/EFI/BOOT", de->d_name); if (!dest_path) return log_oom(); - r = copy_file_with_version_check(source_path, dest_path, /* force= */ false); + r = copy_file_with_version_check(source_path, source_fd, dest_path, dirfd(d), de->d_name, fd, /* force= */ false); if (IN_SET(r, -ESTALE, -ESRCH)) continue; RET_GATHER(ret, r); @@ -386,108 +679,175 @@ static int update_efi_boot_binaries( return ret; } -static int copy_one_file(const char *esp_path, const char *name, bool force) { - char *root = IN_SET(arg_install_source, INSTALL_SOURCE_AUTO, INSTALL_SOURCE_IMAGE) ? arg_root : NULL; - _cleanup_free_ char *source_path = NULL, *dest_path = NULL, *p = NULL, *q = NULL; - const char *e; - char *dest_name, *s; - int r, ret; +static int copy_one_file( + InstallContext *c, + const char *name, + bool force) { - dest_name = strdupa_safe(name); - s = endswith_no_case(dest_name, ".signed"); + int r, ret = 0; + + assert(c); + + _cleanup_free_ char *dest_name = strdup(name); + if (!dest_name) + return log_oom(); + char *s = endswith_no_case(dest_name, ".signed"); if (s) *s = 0; - p = path_join(BOOTLIBDIR, name); - if (!p) + _cleanup_free_ char *sp = path_join(BOOTLIBDIR, name); + if (!sp) return log_oom(); - r = chase(p, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, &source_path, NULL); - /* If we had a root directory to try, we didn't find it and we are in auto mode, retry on the host */ - if (r == -ENOENT && root && arg_install_source == INSTALL_SOURCE_AUTO) - r = chase(p, NULL, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, &source_path, NULL); - if (r < 0) - return log_error_errno(r, - "Failed to resolve path %s%s%s: %m", - p, - root ? " under directory " : "", - strempty(root)); + _cleanup_free_ char *source_path = NULL; + _cleanup_close_ int source_fd = -EBADF; + if (IN_SET(c->install_source, INSTALL_SOURCE_AUTO, INSTALL_SOURCE_IMAGE)) { + source_fd = chase_and_openat( + c->root_fd, + sp, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_REGULAR, + O_RDONLY|O_CLOEXEC, + &source_path); + if (source_fd < 0 && (source_fd != -ENOENT || c->install_source != INSTALL_SOURCE_AUTO)) + return log_error_errno(source_fd, "Failed to resolve path '%s' under directory '%s': %m", sp, c->root); - q = path_join("/EFI/systemd/", dest_name); - if (!q) + /* If we had a root directory to try, we didn't find it and we are in auto mode, retry on the host */ + } + if (source_fd < 0) { + source_fd = chase_and_open( + sp, + /* root= */ NULL, + CHASE_MUST_BE_REGULAR, + O_RDONLY|O_CLOEXEC, + &source_path); + if (source_fd < 0) + return log_error_errno(source_fd, "Failed to resolve path '%s': %m", sp); + } + + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) return log_oom(); - r = chase(q, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_NONEXISTENT|CHASE_TRIGGER_AUTOFS, &dest_path, NULL); + _cleanup_close_ int dest_parent_fd = -EBADF; + r = chaseat(esp_fd, + "/EFI/systemd", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &dest_parent_fd); if (r < 0) - return log_error_errno(r, "Failed to resolve path %s under directory %s: %m", q, esp_path); + return log_error_errno(r, "Failed to resolve path '/EFI/systemd' under directory '%s': %m", j); + + _cleanup_free_ char *dest_path = path_join(j, "/EFI/systemd", dest_name); + if (!dest_path) + return log_oom(); + + _cleanup_close_ int dest_fd = xopenat_full(dest_parent_fd, dest_name, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); + if (dest_fd < 0 && dest_fd != -ENOENT) + return log_error_errno(dest_fd, "Failed to open '%s' under '%s/EFI/systemd' directory: %m", dest_name, j); /* Note that if this fails we do the second copy anyway, but return this error code, * so we stash it away in a separate variable. */ - ret = copy_file_with_version_check(source_path, dest_path, force); + ret = copy_file_with_version_check(source_path, source_fd, dest_path, dest_parent_fd, dest_name, dest_fd, force); - e = startswith(dest_name, "systemd-boot"); + const char *e = startswith(dest_name, "systemd-boot"); if (e) { - _cleanup_free_ char *default_dest_path = NULL; - char *v; /* Create the EFI default boot loader name (specified for removable devices) */ - v = strjoina("/EFI/BOOT/BOOT", e); - const char *boot_dot_efi = ascii_strupper(strrchr(v, '/') + 1); + _cleanup_free_ char *boot_dot_efi = strjoin("BOOT", e); + if (!boot_dot_efi) + return log_oom(); - r = chase(v, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_NONEXISTENT|CHASE_TRIGGER_AUTOFS, &default_dest_path, NULL); + ascii_strupper(boot_dot_efi); + + _cleanup_close_ int default_dest_parent_fd = -EBADF; + r = chaseat(esp_fd, + "/EFI/BOOT", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &default_dest_parent_fd); if (r < 0) - return log_error_errno(r, "Failed to resolve path %s under directory %s: %m", v, esp_path); + return log_error_errno(r, "Failed to resolve path '/EFI/BOOT/' under directory '%s': %m", j); - RET_GATHER(ret, copy_file_with_version_check(source_path, default_dest_path, force)); + _cleanup_free_ char *default_dest_path = path_join(j, "/EFI/BOOT", boot_dot_efi); + if (!default_dest_path) + return log_oom(); - /* If we were installed under any other name in /EFI/BOOT/, make sure we update those binaries - * as well. */ + _cleanup_close_ int default_dest_fd = xopenat_full(default_dest_parent_fd, boot_dot_efi, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); + if (default_dest_fd < 0 && default_dest_fd != -ENOENT) + return log_error_errno(default_dest_fd, "Failed to open '%s' under '%s/EFI/BOOT' directory: %m", boot_dot_efi, j); + + RET_GATHER(ret, copy_file_with_version_check(source_path, source_fd, default_dest_path, default_dest_parent_fd, boot_dot_efi, default_dest_fd, force)); + + /* If we were installed under any other name in /EFI/BOOT/, make sure we update those + * binaries as well. */ if (!force) - RET_GATHER(ret, update_efi_boot_binaries(esp_path, source_path, boot_dot_efi)); + RET_GATHER(ret, update_efi_boot_binaries(c, source_path, source_fd, boot_dot_efi)); } return ret; } -static int install_binaries(const char *esp_path, const char *arch, bool force) { - char *root = IN_SET(arg_install_source, INSTALL_SOURCE_AUTO, INSTALL_SOURCE_IMAGE) ? arg_root : NULL; - _cleanup_closedir_ DIR *d = NULL; - _cleanup_free_ char *path = NULL; +static int install_binaries( + InstallContext *c, + const char *arch) { + int r; - r = chase_and_opendir(BOOTLIBDIR, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, &path, &d); - /* If we had a root directory to try, we didn't find it and we are in auto mode, retry on the host */ - if (r == -ENOENT && root && arg_install_source == INSTALL_SOURCE_AUTO) - r = chase_and_opendir(BOOTLIBDIR, NULL, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, &path, &d); - if (r == -ENOENT && arg_graceful() != ARG_GRACEFUL_NO) { - log_debug("Source directory does not exist, ignoring."); - return 0; + assert(c); + + _cleanup_free_ char *source_path = NULL; + _cleanup_closedir_ DIR *d = NULL; + if (IN_SET(c->install_source, INSTALL_SOURCE_AUTO, INSTALL_SOURCE_IMAGE)) { + r = chase_and_opendirat( + c->root_fd, + BOOTLIBDIR, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_MUST_BE_DIRECTORY, + &source_path, + &d); + if (r < 0 && (r != -ENOENT || c->install_source != INSTALL_SOURCE_AUTO)) + return log_error_errno(r, "Failed to resolve path '%s' under directory '%s': %m", BOOTLIBDIR, c->root); + + /* If we had a root directory to try, we didn't find it and we are in auto mode, retry on the host */ + } + if (!d) { + r = chase_and_opendir( + BOOTLIBDIR, + /* root= */ NULL, + CHASE_MUST_BE_DIRECTORY, + &source_path, + &d); + if (r == -ENOENT && c->graceful) { + log_debug("Source directory '%s' does not exist, ignoring.", BOOTLIBDIR); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to resolve path '%s': %m", BOOTLIBDIR); } - if (r < 0) - return log_error_errno(r, "Failed to open boot loader directory %s%s: %m", strempty(root), BOOTLIBDIR); const char *suffix = strjoina(arch, ".efi"); const char *suffix_signed = strjoina(arch, ".efi.signed"); - FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read \"%s\": %m", path)) { + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read \"%s\": %m", source_path)) { int k; - if (!endswith_no_case(de->d_name, suffix) && !endswith_no_case(de->d_name, suffix_signed)) - continue; - - /* skip the .efi file, if there's a .signed version of it */ - if (endswith_no_case(de->d_name, ".efi")) { + if (endswith_no_case(de->d_name, suffix)) { + /* skip the .efi file, if there's a .signed version of it */ _cleanup_free_ const char *s = strjoin(de->d_name, ".signed"); if (!s) return log_oom(); if (faccessat(dirfd(d), s, F_OK, 0) >= 0) continue; - } + } else if (!endswith_no_case(de->d_name, suffix_signed)) + continue; - k = copy_one_file(esp_path, de->d_name, force); + k = copy_one_file(c, de->d_name, c->operation == INSTALL_NEW); /* Don't propagate an error code if no update necessary, installed version already equal or * newer version, or other boot loader in place. */ - if (arg_graceful() != ARG_GRACEFUL_NO && IN_SET(k, -ESTALE, -ESRCH)) + if (c->graceful && IN_SET(k, -ESTALE, -ESRCH)) continue; RET_GATHER(r, k); } @@ -495,102 +855,160 @@ static int install_binaries(const char *esp_path, const char *arch, bool force) return r; } -static int install_loader_config(const char *esp_path) { - _cleanup_(unlink_and_freep) char *t = NULL; - _cleanup_fclose_ FILE *f = NULL; - _cleanup_free_ char *p = NULL; +static int install_loader_config(InstallContext *c) { int r; - assert(arg_make_entry_directory >= 0); + assert(c); + assert(c->make_entry_directory >= 0); - p = path_join(esp_path, "/loader/loader.conf"); - if (!p) + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) return log_oom(); - if (access(p, F_OK) >= 0) /* Silently skip creation if the file already exists (early check) */ + + _cleanup_close_ int loader_dir_fd = -EBADF; + r = chaseat(esp_fd, + "loader", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &loader_dir_fd); + if (r < 0) + return log_error_errno(r, "Failed to open '/loader/' directory below '%s': %m", j); + + if (faccessat(loader_dir_fd, "loader.conf", F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '/loader/loader.conf' exists below '%s': %m", j); + } else /* Silently skip creation if the file already exists (early check) */ return 0; - r = fopen_tmpfile_linkable(p, O_WRONLY|O_CLOEXEC, &t, &f); + _cleanup_free_ char *t = NULL; + _cleanup_fclose_ FILE *f = NULL; + r = fopen_tmpfile_linkable_at(loader_dir_fd, "loader.conf", O_WRONLY|O_CLOEXEC, &t, &f); if (r < 0) - return log_error_errno(r, "Failed to open \"%s\" for writing: %m", p); + return log_error_errno(r, "Failed to open '%s/loader/loader.conf' for writing: %m", j); + + CLEANUP_TMPFILE_AT(loader_dir_fd, t); fprintf(f, "#timeout 3\n" "#console-mode keep\n"); - if (arg_make_entry_directory) { - assert(arg_entry_token); - fprintf(f, "default %s-*\n", arg_entry_token); + if (c->make_entry_directory) { + assert(c->entry_token); + fprintf(f, "default %s-*\n", c->entry_token); } - r = flink_tmpfile(f, t, p, LINK_TMPFILE_SYNC); + r = flink_tmpfile_at(f, loader_dir_fd, t, "loader.conf", LINK_TMPFILE_SYNC); if (r == -EEXIST) return 0; /* Silently skip creation if the file exists now (recheck) */ if (r < 0) - return log_error_errno(r, "Failed to move \"%s\" into place: %m", p); + return log_error_errno(r, "Failed to move '%s/loader/loader.conf' into place: %m", j); - t = mfree(t); + t = mfree(t); /* disarm CLEANUP_TMPFILE_AT() */ return 1; } -static int install_loader_specification(const char *root) { - _cleanup_(unlink_and_freep) char *t = NULL; - _cleanup_fclose_ FILE *f = NULL; - _cleanup_free_ char *p = NULL; +static int install_loader_specification(InstallContext *c) { int r; - p = path_join(root, "/loader/entries.srel"); - if (!p) + assert(c); + + int dollar_boot_fd = acquire_dollar_boot_fd(c); + if (dollar_boot_fd < 0) + return dollar_boot_fd; + + _cleanup_free_ char *j = path_join(c->root, dollar_boot_path(c)); + if (!j) return log_oom(); - if (access(p, F_OK) >= 0) /* Silently skip creation if the file already exists (early check) */ + _cleanup_close_ int loader_dir_fd = -EBADF; + r = chaseat(dollar_boot_fd, + "loader", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &loader_dir_fd); + if (r < 0) + return log_error_errno(r, "Failed to pin '/loader' directory below '%s': %m", j); + + if (faccessat(loader_dir_fd, "entries.srel", F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check if '/loader/entries.srel' exists below '%s': %m", j); + } else /* Silently skip creation if the file already exists (early check) */ return 0; - r = fopen_tmpfile_linkable(p, O_WRONLY|O_CLOEXEC, &t, &f); + _cleanup_free_ char *t = NULL; + _cleanup_fclose_ FILE *f = NULL; + r = fopen_tmpfile_linkable_at(loader_dir_fd, "entries.srel", O_WRONLY|O_CLOEXEC, &t, &f); if (r < 0) - return log_error_errno(r, "Failed to open \"%s\" for writing: %m", p); + return log_error_errno(r, "Failed to open '%s/loader/entries.srel' for writing: %m", j); + + CLEANUP_TMPFILE_AT(loader_dir_fd, t); fprintf(f, "type1\n"); - r = flink_tmpfile(f, t, p, LINK_TMPFILE_SYNC); + r = flink_tmpfile_at(f, loader_dir_fd, t, "entries.srel", LINK_TMPFILE_SYNC); if (r == -EEXIST) return 0; /* Silently skip creation if the file exists now (recheck) */ if (r < 0) - return log_error_errno(r, "Failed to move \"%s\" into place: %m", p); + return log_error_errno(r, "Failed to move '%s/loader/entries.srel' into place: %m", j); - t = mfree(t); + t = mfree(t); /* disarm CLEANUP_TMPFILE_AT() */ return 1; } -static int install_entry_directory(const char *root) { - assert(root); - assert(arg_make_entry_directory >= 0); +static int install_entry_directory(InstallContext *c) { + assert(c); + assert(c->make_entry_directory >= 0); - if (!arg_make_entry_directory) + if (!c->make_entry_directory) return 0; - assert(arg_entry_token); - return mkdir_one(root, arg_entry_token); + assert(c->entry_token); + + int dollar_boot_fd = acquire_dollar_boot_fd(c); + if (dollar_boot_fd < 0) + return dollar_boot_fd; + + _cleanup_free_ char *j = path_join(c->root, dollar_boot_path(c)); + if (!j) + return log_oom(); + + return mkdir_one(j, dollar_boot_fd, c->entry_token); } -static int install_entry_token(void) { - _cleanup_free_ char* p = NULL; +static int install_entry_token(InstallContext *c) { int r; - assert(arg_make_entry_directory >= 0); - assert(arg_entry_token); + assert(c); + assert(c->make_entry_directory >= 0); + assert(c->entry_token); /* Let's save the used entry token in /etc/kernel/entry-token if we used it to create the entry * directory, or if anything else but the machine ID */ - if (!arg_make_entry_directory && arg_entry_token_type == BOOT_ENTRY_TOKEN_MACHINE_ID) + if (!c->make_entry_directory && c->entry_token_type == BOOT_ENTRY_TOKEN_MACHINE_ID) return 0; - p = path_join(arg_root, secure_getenv("KERNEL_INSTALL_CONF_ROOT") ?: "/etc/kernel/", "entry-token"); - if (!p) + const char *confdir = secure_getenv("KERNEL_INSTALL_CONF_ROOT") ?: "/etc/kernel/"; + + _cleanup_free_ char *j = path_join(c->root, confdir); + if (!j) return log_oom(); - r = write_string_file(p, arg_entry_token, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755); + _cleanup_close_ int dfd = -EBADF; + r = chaseat(c->root_fd, + confdir, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &dfd); if (r < 0) - return log_error_errno(r, "Failed to write entry token '%s' to %s: %m", arg_entry_token, p); + return log_error_errno(r, "Failed to open '%s': %m", j); + + r = write_string_file_at(dfd, "entry-token", c->entry_token, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755); + if (r < 0) + return log_error_errno(r, "Failed to write entry token '%s' to '%s/entry-token': %m", c->entry_token, j); return 0; } @@ -618,27 +1036,43 @@ static int efi_timestamp(EFI_TIME *ret) { return 0; } +#endif -static int install_secure_boot_auto_enroll(const char *esp, X509 *certificate, EVP_PKEY *private_key) { +static int install_secure_boot_auto_enroll(InstallContext *c) { +#if HAVE_OPENSSL int r; +#endif if (!arg_secure_boot_auto_enroll) return 0; +#if HAVE_OPENSSL + if (!c->secure_boot_certificate || !c->secure_boot_private_key) + return 0; + _cleanup_free_ uint8_t *dercert = NULL; int dercertsz; - dercertsz = i2d_X509(certificate, &dercert); + dercertsz = i2d_X509(c->secure_boot_certificate, &dercert); if (dercertsz < 0) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to convert X.509 certificate to DER: %s", ERR_error_string(ERR_get_error(), NULL)); - r = mkdir_one(esp, "loader/keys/auto"); - if (r < 0) - return r; + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; - _cleanup_close_ int keys_fd = chase_and_open("loader/keys/auto", esp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, O_DIRECTORY, NULL); - if (keys_fd < 0) - return log_error_errno(keys_fd, "Failed to chase loader/keys/auto in the ESP: %m"); + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) + return log_oom(); + + _cleanup_close_ int keys_fd = -EBADF; + r = chaseat(esp_fd, + "loader/keys/auto", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &keys_fd); + if (r < 0) + return log_error_errno(r, "Failed to chase /loader/keys/auto/ below '%s': %m", j); uint32_t siglistsz = offsetof(EFI_SIGNATURE_LIST, Signatures) + offsetof(EFI_SIGNATURE_DATA, SignatureData) + dercertsz; /* We use malloc0() to zero-initialize the SignatureOwner field of Signatures[0]. */ @@ -695,7 +1129,7 @@ static int install_secure_boot_auto_enroll(const char *esp, X509 *certificate, E return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write signature list to bio"); _cleanup_(PKCS7_freep) PKCS7 *p7 = NULL; - p7 = PKCS7_sign(certificate, private_key, /* certs= */ NULL, bio, PKCS7_DETACHED|PKCS7_NOATTR|PKCS7_BINARY|PKCS7_NOSMIMECAP); + p7 = PKCS7_sign(c->secure_boot_certificate, c->secure_boot_private_key, /* certs= */ NULL, bio, PKCS7_DETACHED|PKCS7_NOATTR|PKCS7_BINARY|PKCS7_NOSMIMECAP); if (!p7) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to calculate PKCS7 signature: %s", ERR_error_string(ERR_get_error(), NULL)); @@ -729,10 +1163,13 @@ static int install_secure_boot_auto_enroll(const char *esp, X509 *certificate, E if (!filename) return log_oom(); - _cleanup_close_ int fd = openat(keys_fd, filename, O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY|O_WRONLY|O_CLOEXEC, 0600); + _cleanup_free_ char *t = NULL; + _cleanup_close_ int fd = open_tmpfile_linkable_at(keys_fd, filename, O_WRONLY|O_CLOEXEC, &t); if (fd < 0) return log_error_errno(fd, "Failed to open secure boot auto-enrollment file for writing: %m"); + CLEANUP_TMPFILE_AT(keys_fd, t); + r = loop_write(fd, auth, authsz); if (r < 0) return log_error_errno(r, "Failed to write authentication descriptor to secure boot auto-enrollment file: %m"); @@ -741,15 +1178,20 @@ static int install_secure_boot_auto_enroll(const char *esp, X509 *certificate, E if (r < 0) return log_error_errno(r, "Failed to write signature list to secure boot auto-enrollment file: %m"); - if (fsync(fd) < 0 || fsync(keys_fd) < 0) - return log_error_errno(errno, "Failed to sync secure boot auto-enrollment file: %m"); + r = link_tmpfile_at(fd, keys_fd, t, filename, LINK_TMPFILE_SYNC); + if (r < 0) + return log_error_errno(errno, "Failed to link secure boot auto-enrollment file: %m"); - log_info("Secure boot auto-enrollment file %s/loader/keys/auto/%s successfully written.", esp, filename); + t = mfree(t); /* Disarm CLEANUP_TMPFILE_AT() */ + + log_info("Secure boot auto-enrollment file '%s/loader/keys/auto/%s' successfully written.", j, filename); } return 0; -} +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Built without OpenSSL support, cannot set up auto-enrollment."); #endif +} static bool same_entry(uint16_t id, sd_id128_t uuid, const char *path) { _cleanup_free_ char *opath = NULL; @@ -799,11 +1241,13 @@ static int find_slot(sd_id128_t uuid, const char *path, uint16_t *id) { return 0; } -static int insert_into_order(uint16_t slot, bool first) { +static int insert_into_order(InstallContext *c, uint16_t slot) { _cleanup_free_ uint16_t *order = NULL; uint16_t *t; int n; + assert(c); + n = efi_get_boot_order(&order); if (n <= 0) /* no entry, add us */ @@ -819,7 +1263,7 @@ static int insert_into_order(uint16_t slot, bool first) { continue; /* we do not require to be the first one, all is fine */ - if (!first) + if (c->operation != INSTALL_NEW) return 0; /* move us to the first slot */ @@ -835,7 +1279,7 @@ static int insert_into_order(uint16_t slot, bool first) { order = t; /* add us to the top or end of the list */ - if (first) { + if (c->operation != INSTALL_NEW) { memmove(order + 1, order, n * sizeof(uint16_t)); order[0] = slot; } else @@ -869,55 +1313,64 @@ static const char *pick_efi_boot_option_description(void) { } static int install_variables( - const char *esp_path, - uint32_t part, - uint64_t pstart, - uint64_t psize, - sd_id128_t uuid, - const char *path, - bool first, - bool graceful) { + InstallContext *c, + const char *path) { uint16_t slot; int r; - r = chase_and_access(path, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, F_OK, NULL); + assert(c); + + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) + return log_oom(); + + r = chase_and_accessat( + esp_fd, + path, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_REGULAR, + F_OK, + /* ret_path= */ NULL); if (r == -ENOENT) return 0; if (r < 0) - return log_error_errno(r, "Cannot access \"%s/%s\": %m", esp_path, skip_leading_slash(path)); + return log_error_errno(r, "Cannot access \"%s/%s\": %m", j, skip_leading_slash(path)); - r = find_slot(uuid, path, &slot); + r = find_slot(c->esp_uuid, path, &slot); if (r < 0) { - int level = graceful ? arg_quiet ? LOG_DEBUG : LOG_INFO : LOG_ERR; - const char *skip = graceful ? ", skipping" : ""; + int level = c->graceful ? arg_quiet ? LOG_DEBUG : LOG_INFO : LOG_ERR; + const char *skip = c->graceful ? ", skipping" : ""; log_full_errno(level, r, r == -ENOENT ? "Failed to access EFI variables%s. Is the \"efivarfs\" filesystem mounted?" : "Failed to determine current boot order%s: %m", skip); - return graceful ? 0 : r; + return c->graceful ? 0 : r; } bool existing = r > 0; - if (first || !existing) { + if (c->operation == INSTALL_NEW || !existing) { r = efi_add_boot_option( slot, pick_efi_boot_option_description(), - part, - pstart, - psize, - uuid, + c->esp_part, + c->esp_pstart, + c->esp_psize, + c->esp_uuid, path); if (r < 0) { - int level = graceful ? arg_quiet ? LOG_DEBUG : LOG_INFO : LOG_ERR; - const char *skip = graceful ? ", skipping" : ""; + int level = c->graceful ? arg_quiet ? LOG_DEBUG : LOG_INFO : LOG_ERR; + const char *skip = c->graceful ? ", skipping" : ""; log_full_errno(level, r, "Failed to create EFI Boot variable entry%s: %m", skip); - return graceful ? 0 : r; + return c->graceful ? 0 : r; } log_info("%s EFI boot entry \"%s\".", @@ -925,12 +1378,14 @@ static int install_variables( pick_efi_boot_option_description()); } - return insert_into_order(slot, first); + return insert_into_order(c, slot); } -static int are_we_installed(const char *esp_path) { +static int are_we_installed(InstallContext *c) { int r; + assert(c); + /* Tests whether systemd-boot is installed. It's not obvious what to use as check here: we could * check EFI variables, we could check what binary /EFI/BOOT/BOOT*.EFI points to, or whether the * loader entries directory exists. Here we opted to check whether /EFI/systemd/ is non-empty, which @@ -945,14 +1400,29 @@ static int are_we_installed(const char *esp_path) { * → It specifically checks for systemd-boot, not for other boot loaders (which a check for * /boot/loader/entries would do). */ - _cleanup_free_ char *p = path_join(esp_path, "/EFI/systemd/"); + _cleanup_free_ char *p = path_join(c->esp_path, "/EFI/systemd"); if (!p) return log_oom(); - log_debug("Checking whether %s contains any files%s", p, glyph(GLYPH_ELLIPSIS)); - r = dir_is_empty(p, /* ignore_hidden_or_backup= */ false); + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_close_ int fd = chase_and_openat( + esp_fd, + "/EFI/systemd", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY, + O_RDONLY|O_CLOEXEC|O_DIRECTORY, + /* ret_path= */ NULL); + if (fd == -ENOENT) + return 0; + if (fd < 0) + return log_error_errno(fd, "Failed to open '%s': %m", p); + + log_debug("Checking whether '%s' contains any files%s", p, glyph(GLYPH_ELLIPSIS)); + r = dir_is_empty_at(fd, /* path= */ NULL, /* ignore_hidden_or_backup= */ false); if (r < 0 && r != -ENOENT) - return log_error_errno(r, "Failed to check whether %s contains any files: %m", p); + return log_error_errno(r, "Failed to check whether '%s' contains any files: %m", p); return r == 0; } @@ -1019,38 +1489,15 @@ static int load_secure_boot_auto_enroll( } #endif -int verb_install(int argc, char *argv[], void *userdata) { - sd_id128_t uuid = SD_ID128_NULL; - uint64_t pstart = 0, psize = 0; - uint32_t part = 0; - bool install, graceful; +static int run_install(InstallContext *c) { int r; - /* Invoked for both "update" and "install" */ + assert(c); + assert(c->operation >= 0); - install = streq(argv[0], "install"); - - /* Support graceful mode only for updates, unless forcibly enabled in chroot environments */ - graceful = arg_graceful() == ARG_GRACEFUL_FORCE || (!install && arg_graceful() != ARG_GRACEFUL_NO); - -#if HAVE_OPENSSL - _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; - _cleanup_(EVP_PKEY_freep) EVP_PKEY *private_key = NULL; - _cleanup_(X509_freep) X509 *certificate = NULL; - r = load_secure_boot_auto_enroll(&certificate, &private_key, &ui); - if (r < 0) - return r; -#endif - - r = acquire_esp(/* unprivileged_mode= */ false, graceful, &part, &pstart, &psize, &uuid, NULL); - if (graceful && r == -ENOKEY) - return 0; /* If --graceful is specified and we can't find an ESP, handle this cleanly */ - if (r < 0) - return r; - - if (!install) { + if (c->operation == INSTALL_UPDATE) { /* If we are updating, don't do anything if sd-boot wasn't actually installed. */ - r = are_we_installed(arg_esp_path); + r = are_we_installed(c); if (r < 0) return r; if (r == 0) { @@ -1059,66 +1506,79 @@ int verb_install(int argc, char *argv[], void *userdata) { } } - r = acquire_xbootldr(/* unprivileged_mode= */ false, NULL, NULL); - if (r < 0) - return r; - - r = settle_make_entry_directory(); + r = settle_make_entry_directory(c); if (r < 0) return r; const char *arch = arg_arch_all ? "" : get_efi_arch(); + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_free_ char *j = path_join(c->root, c->esp_path); + if (!j) + return log_oom(); + + int dollar_boot_fd = acquire_dollar_boot_fd(c); + if (dollar_boot_fd < 0) + return dollar_boot_fd; + + _cleanup_free_ char *w = path_join(c->root, dollar_boot_path(c)); + if (!w) + return log_oom(); + WITH_UMASK(0002) { - if (install) { + if (c->operation == INSTALL_NEW) { /* Don't create any of these directories when we are just updating. When we update * we'll drop-in our files (unless there are newer ones already), but we won't create * the directories for them in the first place. */ - r = create_subdirs(arg_esp_path, esp_subdirs); + + r = create_subdirs(j, esp_fd, esp_subdirs); if (r < 0) return r; - r = create_subdirs(arg_dollar_boot_path(), dollar_boot_subdirs); + r = create_subdirs(w, dollar_boot_fd, dollar_boot_subdirs); if (r < 0) return r; } - r = install_binaries(arg_esp_path, arch, install); + r = install_binaries(c, arch); if (r < 0) return r; - if (install) { - r = install_loader_config(arg_esp_path); + if (c->operation == INSTALL_NEW) { + r = install_loader_config(c); if (r < 0) return r; - r = install_entry_directory(arg_dollar_boot_path()); + r = install_entry_directory(c); if (r < 0) return r; - r = install_entry_token(); + r = install_entry_token(c); if (r < 0) return r; - r = install_random_seed(arg_esp_path); - if (r < 0) - return r; + if (arg_install_random_seed && !c->root) { + r = install_random_seed(c->esp_path); + if (r < 0) + return r; + } -#if HAVE_OPENSSL - r = install_secure_boot_auto_enroll(arg_esp_path, certificate, private_key); + r = install_secure_boot_auto_enroll(c); if (r < 0) return r; -#endif } - r = install_loader_specification(arg_dollar_boot_path()); + r = install_loader_specification(c); if (r < 0) return r; } (void) sync_everything(); - if (!touch_variables()) + if (!should_touch_install_variables(c)) return 0; if (arg_arch_all) { @@ -1127,126 +1587,180 @@ int verb_install(int argc, char *argv[], void *userdata) { } char *path = strjoina("/EFI/systemd/systemd-boot", arch, ".efi"); - return install_variables(arg_esp_path, part, pstart, psize, uuid, path, install, graceful); + return install_variables(c, path); } -static int remove_boot_efi(const char *esp_path) { +int verb_install(int argc, char *argv[], void *userdata) { + int r; + + /* Invoked for both "update" and "install" */ + + _cleanup_(install_context_done) InstallContext c = INSTALL_CONTEXT_NULL; + r = install_context_from_cmdline(&c, streq(argv[0], "install") ? INSTALL_NEW : INSTALL_UPDATE); + if (r < 0) + return r; + if (r == 0) { + log_debug("No ESP found and operating in graceful mode, skipping."); + return 0; + } + +#if HAVE_OPENSSL + _cleanup_(openssl_ask_password_ui_freep) OpenSSLAskPasswordUI *ui = NULL; + r = load_secure_boot_auto_enroll(&c.secure_boot_certificate, &c.secure_boot_private_key, &ui); + if (r < 0) + return r; +#endif + + return run_install(&c); +} + +static int remove_boot_efi(InstallContext *c) { + int r, n = 0; + + assert(c); + + int esp_fd = acquire_esp_fd(c); + if (esp_fd < 0) + return esp_fd; + + _cleanup_free_ char *w = path_join(c->root, c->esp_path); + if (!w) + return log_oom(); + _cleanup_closedir_ DIR *d = NULL; _cleanup_free_ char *p = NULL; - int r, c = 0; - - r = chase_and_opendir("/EFI/BOOT", esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, &p, &d); + r = chase_and_opendirat( + esp_fd, + "/EFI/BOOT", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY, + &p, + &d); if (r == -ENOENT) return 0; if (r < 0) - return log_error_errno(r, "Failed to open directory \"%s/EFI/BOOT\": %m", esp_path); + return log_error_errno(r, "Failed to open directory \"%s/EFI/BOOT\": %m", w); + + _cleanup_free_ char *j = path_join(w, p); + if (!j) + return log_oom(); FOREACH_DIRENT(de, d, break) { _cleanup_close_ int fd = -EBADF; - _cleanup_free_ char *v = NULL; if (!endswith_no_case(de->d_name, ".efi")) continue; - fd = xopenat_full(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW, XO_REGULAR, /* mode= */ 0); + _cleanup_free_ char *z = path_join(j, de->d_name); + if (!z) + return log_oom(); + + fd = xopenat_full(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW, XO_REGULAR, /* mode= */ MODE_INVALID); if (fd < 0) - return log_error_errno(fd, "Failed to open \"%s/%s\" for reading: %m", p, de->d_name); + return log_error_errno(fd, "Failed to open '%s' for reading: %m", z); r = pe_is_native_fd(fd); if (r < 0) { - log_warning_errno(r, "Failed to detect if \"%s/%s\" is native architecture, ignoring: %m", p, de->d_name); + log_warning_errno(r, "Failed to detect if '%s' is native architecture, ignoring: %m", z); continue; } if (r == 0) continue; + _cleanup_free_ char *v = NULL; r = get_file_version(fd, &v); if (r == -ESRCH) continue; /* No version information */ if (r < 0) return r; - if (startswith(v, "systemd-boot ")) { - if (unlinkat(dirfd(d), de->d_name, 0) < 0) - return log_error_errno(errno, "Failed to remove \"%s/%s\": %m", p, de->d_name); + if (!startswith(v, "systemd-boot ")) + continue; - log_info("Removed \"%s/%s\".", p, de->d_name); - } + if (unlinkat(dirfd(d), de->d_name, 0) < 0) + return log_error_errno(errno, "Failed to remove '%s': %m", z); - c++; + log_info("Removed '%s'.", z); + + n++; } - return c; + log_debug("Removed %i EFI binaries from '%s'.", n, j); + return n; } -static int rmdir_one(const char *prefix, const char *suffix) { - _cleanup_free_ char *p = path_join(prefix, suffix); +static int unlink_inode(const char *root, int root_fd, const char *path, mode_t type) { + int r; + + assert(root); + assert(root_fd >= 0); + assert(path); + assert(IN_SET(type, S_IFREG, S_IFDIR)); + + _cleanup_free_ char *p = path_join(empty_to_root(root), path); if (!p) return log_oom(); - if (rmdir(p) < 0) { - bool ignore = IN_SET(errno, ENOENT, ENOTEMPTY); - - log_full_errno(ignore ? LOG_DEBUG : LOG_ERR, errno, - "Failed to remove directory \"%s\": %m", p); - if (!ignore) - return -errno; - } else - log_info("Removed \"%s\".", p); + r = chase_and_unlinkat( + root_fd, + path, + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS, + S_ISDIR(type) ? AT_REMOVEDIR : 0, + /* ret_path= */ NULL); + if (r < 0) { + bool ignore = IN_SET(r, -ENOENT, -ENOTEMPTY); + log_full_errno(ignore ? LOG_DEBUG : LOG_ERR, r, "Failed to remove '%s': %m", p); + return ignore ? 0 : r; + } + log_info("Removed %s\"%s\".", S_ISDIR(type) ? "directory " : "", p); return 0; } -static int remove_subdirs(const char *root, const char *const *subdirs) { - int r; +static int remove_subdirs(const char *root, int root_fd, const char *const *subdirs) { + int r = 0; - /* We use recursion here to destroy the directories in reverse order. Which should be safe given how - * short the array is. */ - - if (!subdirs[0]) /* A the end of the list */ - return 0; - - r = remove_subdirs(root, subdirs + 1); - return RET_GATHER(r, rmdir_one(root, subdirs[0])); -} - -static int remove_entry_directory(const char *root) { assert(root); - assert(arg_make_entry_directory >= 0); + assert(root_fd); - if (!arg_make_entry_directory || !arg_entry_token) - return 0; + STRV_FOREACH_BACKWARDS(i, (char**) subdirs) + RET_GATHER(r, unlink_inode(root, root_fd, *i, S_IFDIR)); - return rmdir_one(root, arg_entry_token); + return r; } -static int remove_binaries(const char *esp_path) { +static int remove_entry_directory(InstallContext *c, const char *path, int fd) { + assert(c); + assert(c->make_entry_directory >= 0); + assert(path); + assert(fd >= 0); + + if (!c->make_entry_directory || !c->entry_token) + return 0; + + return unlink_inode(path, fd, c->entry_token, S_IFDIR); +} + +static int remove_binaries(InstallContext *c) { int r; - _cleanup_free_ char *p = path_join(esp_path, "/EFI/systemd"); + _cleanup_free_ char *p = path_join(c->root, "/EFI/systemd"); if (!p) return log_oom(); - r = rm_rf(p, REMOVE_ROOT|REMOVE_PHYSICAL); - return RET_GATHER(r, remove_boot_efi(esp_path)); -} + _cleanup_close_ int efi_fd = -EBADF; + r = chaseat(c->esp_fd, + "EFI", + CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &efi_fd); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to remove '%s': %m", p); -static int remove_file(const char *root, const char *file) { - assert(root); - assert(file); + r = 0; + } else + r = rm_rf_at(efi_fd, "systemd", REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK); - _cleanup_free_ char *p = path_join(root, file); - if (!p) - return log_oom(); - - if (unlink(p) < 0) { - log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, - "Failed to unlink file \"%s\": %m", p); - - return errno == ENOENT ? 0 : -errno; - } - - log_info("Removed \"%s\".", p); - return 1; + return RET_GATHER(r, remove_boot_efi(c)); } static int remove_variables(sd_id128_t uuid, const char *path, bool in_order) { @@ -1300,46 +1814,64 @@ int verb_remove(int argc, char *argv[], void *userdata) { sd_id128_t uuid = SD_ID128_NULL; int r; - r = acquire_esp(/* unprivileged_mode= */ false, /* graceful= */ false, NULL, NULL, NULL, &uuid, NULL); + _cleanup_(install_context_done) InstallContext c = INSTALL_CONTEXT_NULL; + r = install_context_from_cmdline(&c, INSTALL_REMOVE); + if (r < 0) + return r; + if (r == 0) { + log_debug("No ESP found and operating in graceful mode, skipping."); + return 0; + } + + r = settle_make_entry_directory(&c); if (r < 0) return r; - r = acquire_xbootldr(/* unprivileged_mode= */ false, NULL, NULL); - if (r < 0) - return r; + int esp_fd = acquire_esp_fd(&c); + if (esp_fd < 0) + return esp_fd; - r = settle_make_entry_directory(); - if (r < 0) - return r; + _cleanup_free_ char *j = path_join(c.root, c.esp_path); + if (!j) + return log_oom(); - r = remove_binaries(arg_esp_path); - RET_GATHER(r, remove_file(arg_esp_path, "/loader/loader.conf")); - RET_GATHER(r, remove_file(arg_esp_path, "/loader/random-seed")); - RET_GATHER(r, remove_file(arg_esp_path, "/loader/entries.srel")); + int dollar_boot_fd = acquire_dollar_boot_fd(&c); /* this will initialize .xbootldr_fd */ + if (dollar_boot_fd < 0) + return dollar_boot_fd; + + _cleanup_free_ char *w = path_join(c.root, dollar_boot_path(&c)); + if (!w) + return log_oom(); + + r = remove_binaries(&c); + RET_GATHER(r, unlink_inode(j, esp_fd, "/loader/loader.conf", S_IFREG)); + RET_GATHER(r, unlink_inode(j, esp_fd, "/loader/random-seed", S_IFREG)); + RET_GATHER(r, unlink_inode(j, esp_fd, "/loader/entries.srel", S_IFREG)); FOREACH_STRING(db, "PK.auth", "KEK.auth", "db.auth") { _cleanup_free_ char *p = path_join("/loader/keys/auto", db); if (!p) return log_oom(); - RET_GATHER(r, remove_file(arg_esp_path, p)); + RET_GATHER(r, unlink_inode(j, esp_fd, p, S_IFREG)); } + RET_GATHER(r, unlink_inode(j, esp_fd, "/loader/keys/auto", S_IFDIR)); + RET_GATHER(r, unlink_inode(j, esp_fd, "/loader/entries.srel", S_IFREG)); - RET_GATHER(r, rmdir_one(arg_esp_path, "/loader/keys/auto")); - RET_GATHER(r, remove_subdirs(arg_esp_path, esp_subdirs)); - RET_GATHER(r, remove_subdirs(arg_esp_path, dollar_boot_subdirs)); - RET_GATHER(r, remove_entry_directory(arg_esp_path)); + RET_GATHER(r, remove_subdirs(j, esp_fd, esp_subdirs)); + RET_GATHER(r, remove_subdirs(j, esp_fd, dollar_boot_subdirs)); + RET_GATHER(r, remove_entry_directory(&c, j, esp_fd)); - if (arg_xbootldr_path) { + if (c.xbootldr_fd >= 0) { /* Remove a subset of these also from the XBOOTLDR partition if it exists */ - RET_GATHER(r, remove_file(arg_xbootldr_path, "/loader/entries.srel")); - RET_GATHER(r, remove_subdirs(arg_xbootldr_path, dollar_boot_subdirs)); - RET_GATHER(r, remove_entry_directory(arg_xbootldr_path)); + RET_GATHER(r, unlink_inode(w, c.xbootldr_fd, "/loader/entries.srel", S_IFREG)); + RET_GATHER(r, remove_subdirs(w, c.xbootldr_fd, dollar_boot_subdirs)); + RET_GATHER(r, remove_entry_directory(&c, w, c.xbootldr_fd)); } (void) sync_everything(); - if (!touch_variables()) + if (!should_touch_install_variables(&c)) return r; if (arg_arch_all) { @@ -1355,13 +1887,18 @@ int verb_remove(int argc, char *argv[], void *userdata) { int verb_is_installed(int argc, char *argv[], void *userdata) { int r; - r = acquire_esp(/* unprivileged_mode= */ false, - /* graceful= */ arg_graceful() != ARG_GRACEFUL_NO, - NULL, NULL, NULL, NULL, NULL); + _cleanup_(install_context_done) InstallContext c = INSTALL_CONTEXT_NULL; + r = install_context_from_cmdline(&c, INSTALL_TEST); if (r < 0) return r; + if (r == 0) { + log_debug("No ESP found and operating in graceful mode, claiming not installed."); + if (!arg_quiet) + puts("no"); + return EXIT_FAILURE; + } - r = are_we_installed(arg_esp_path); + r = are_we_installed(&c); if (r < 0) return r; From aea76373b29e418101d4a17aacc1fa664af65768 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Sep 2025 12:11:19 +0200 Subject: [PATCH 2/7] bootctl: add Install() varlink API Fixes: #11221 --- src/bootctl/bootctl-install.c | 124 ++++++++++++++++++++ src/bootctl/bootctl-install.h | 4 + src/bootctl/bootctl.c | 5 +- src/shared/boot-entry.c | 2 +- src/shared/boot-entry.h | 2 +- src/shared/varlink-io.systemd.BootControl.c | 64 +++++++++- units/systemd-bootctl.socket | 2 +- units/systemd-bootctl@.service | 2 +- 8 files changed, 198 insertions(+), 7 deletions(-) diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index 4abaa1aa83e..d6ce3471b11 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -3,6 +3,8 @@ #include #include +#include "sd-varlink.h" + #include "alloc-util.h" #include "boot-entry.h" #include "bootctl.h" @@ -18,11 +20,13 @@ #include "env-file.h" #include "fd-util.h" #include "fileio.h" +#include "find-esp.h" #include "fs-util.h" #include "glyph-util.h" #include "id128-util.h" #include "install-file.h" #include "io-util.h" +#include "json-util.h" #include "kernel-config.h" #include "log.h" #include "openssl-util.h" @@ -89,6 +93,15 @@ typedef struct InstallContext { .touch_variables = -1, \ } +static const char* install_operation_table[_INSTALL_OPERATION_MAX] = { + [INSTALL_NEW] = "new", + [INSTALL_UPDATE] = "update", + [INSTALL_REMOVE] = "remove", + [INSTALL_TEST] = "test", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(install_operation, InstallOperation); + static void install_context_done(InstallContext *c) { assert(c); @@ -1912,3 +1925,114 @@ int verb_is_installed(int argc, char *argv[], void *userdata) { return EXIT_FAILURE; } } + +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_install_operation, InstallOperation, install_operation_from_string); +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_boot_entry_token_type, BootEntryTokenType, boot_entry_token_type_from_string); + +typedef struct InstallParameters { + InstallContext context; + unsigned root_fd_index; +} InstallParameters; + +static void install_parameters_done(InstallParameters *p) { + assert(p); + + install_context_done(&p->context); +} + +int vl_method_install( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(link); + + _cleanup_(install_parameters_done) InstallParameters p = { + .context = INSTALL_CONTEXT_NULL, + .root_fd_index = UINT_MAX, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "operation", SD_JSON_VARIANT_STRING, json_dispatch_install_operation, voffsetof(p, context.operation), SD_JSON_MANDATORY }, + { "graceful", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, voffsetof(p, context.graceful), 0 }, + { "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 }, + { "touchVariables", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, context.touch_variables), 0 }, + {}, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!IN_SET(p.context.operation, INSTALL_NEW, INSTALL_UPDATE)) + return sd_varlink_error_invalid_parameter_name(link, "operation"); + + 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_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); + } + } + + if (p.context.root_fd < 0 && 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); + } + + if (p.context.root_fd < 0) + p.context.root_fd = XAT_FDROOT; + + if (p.context.entry_token_type < 0) + p.context.entry_token_type = BOOT_ENTRY_TOKEN_AUTO; + + r = find_esp_and_warn_at( + p.context.root_fd, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &p.context.esp_path, + &p.context.esp_part, + &p.context.esp_pstart, + &p.context.esp_psize, + &p.context.esp_uuid, + /* ret_devid= */ NULL); + if (r == -ENOKEY) + return sd_varlink_error(link, "io.systemd.BootControl.NoESPFound", NULL); + if (r < 0) + return r; + + r = find_xbootldr_and_warn_at( + p.context.root_fd, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &p.context.xbootldr_path, + /* ret_uuid= */ NULL, + /* ret_devid= */ NULL); + if (r == -ENOKEY) + log_debug_errno(r, "Didn't find an XBOOTLDR partition, using ESP as $BOOT."); + else if (r < 0) + return r; + + r = run_install(&p.context); + if (r < 0) + return r; + + return sd_varlink_reply(link, NULL); +} diff --git a/src/bootctl/bootctl-install.h b/src/bootctl/bootctl-install.h index cd4b725112a..f2d7fab5c96 100644 --- a/src/bootctl/bootctl-install.h +++ b/src/bootctl/bootctl-install.h @@ -1,6 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "shared-forward.h" + int verb_install(int argc, char *argv[], void *userdata); int verb_remove(int argc, char *argv[], void *userdata); int verb_is_installed(int argc, char *argv[], void *userdata); + +int vl_method_install(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 dc321a07ca8..889c5f91b67 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -711,7 +711,7 @@ static int vl_server(void) { r = varlink_server_new( &varlink_server, - SD_VARLINK_SERVER_ROOT_ONLY, + SD_VARLINK_SERVER_ROOT_ONLY|SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT, /* userdata= */ NULL); if (r < 0) return log_error_errno(r, "Failed to allocate Varlink server: %m"); @@ -724,7 +724,8 @@ static int vl_server(void) { varlink_server, "io.systemd.BootControl.ListBootEntries", vl_method_list_boot_entries, "io.systemd.BootControl.SetRebootToFirmware", vl_method_set_reboot_to_firmware, - "io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware); + "io.systemd.BootControl.GetRebootToFirmware", vl_method_get_reboot_to_firmware, + "io.systemd.BootControl.Install", vl_method_install); if (r < 0) return log_error_errno(r, "Failed to bind Varlink methods: %m"); diff --git a/src/shared/boot-entry.c b/src/shared/boot-entry.c index 2885d3f92e8..042522951cc 100644 --- a/src/shared/boot-entry.c +++ b/src/shared/boot-entry.c @@ -291,4 +291,4 @@ static const char *const boot_entry_token_type_table[] = { [BOOT_ENTRY_TOKEN_AUTO] = "auto", }; -DEFINE_STRING_TABLE_LOOKUP_TO_STRING(boot_entry_token_type, BootEntryTokenType); +DEFINE_STRING_TABLE_LOOKUP(boot_entry_token_type, BootEntryTokenType); diff --git a/src/shared/boot-entry.h b/src/shared/boot-entry.h index 84ea69a096a..7db1ed0d520 100644 --- a/src/shared/boot-entry.h +++ b/src/shared/boot-entry.h @@ -34,4 +34,4 @@ int boot_entry_token_ensure_at( int parse_boot_entry_token_type(const char *s, BootEntryTokenType *type, char **token); -DECLARE_STRING_TABLE_LOOKUP_TO_STRING(boot_entry_token_type, BootEntryTokenType); +DECLARE_STRING_TABLE_LOOKUP(boot_entry_token_type, BootEntryTokenType); diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c index 11dcb4d0955..453002aaa9f 100644 --- a/src/shared/varlink-io.systemd.BootControl.c +++ b/src/shared/varlink-io.systemd.BootControl.c @@ -36,21 +36,37 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_DEFINE_FIELD_BY_TYPE(source, BootEntrySource, 0), SD_VARLINK_FIELD_COMMENT("The string identifier of the entry"), SD_VARLINK_DEFINE_FIELD(id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to the primary definition file for the entry"), SD_VARLINK_DEFINE_FIELD(path, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Directory path of the file system root the entry was found on"), SD_VARLINK_DEFINE_FIELD(root, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The entry's title string"), SD_VARLINK_DEFINE_FIELD(title, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The possibly mangled/augmented title to show for the entry"), SD_VARLINK_DEFINE_FIELD(showTitle, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("An explicitly configured sorting key for the enry"), SD_VARLINK_DEFINE_FIELD(sortKey, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The version of the entry"), SD_VARLINK_DEFINE_FIELD(version, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Machine ID of the OS installation belonging to the entry, if known"), SD_VARLINK_DEFINE_FIELD(machineId, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("EFI architecture name for this entry"), SD_VARLINK_DEFINE_FIELD(architecture, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Command line options to pass to the invoked kernel or EFI binary"), SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to the Linux kernel to invoke, relative to the root directory of the file system containing the entry file"), SD_VARLINK_DEFINE_FIELD(linux, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to the EFI binary to invoke, relative to the root directory of the file system containing the entry file"), SD_VARLINK_DEFINE_FIELD(efi, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to an UKI EFI binary to invoke, relative to the root directory of the file system containing the entry file"), SD_VARLINK_DEFINE_FIELD(uki, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("An UKI profile index to invoke. If not specified defaults to the first profile."), SD_VARLINK_DEFINE_FIELD(profile, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Path to the initrd image to pass to the invoked kernel, relative to the root directory of the file system containing the entry file"), SD_VARLINK_DEFINE_FIELD(initrd, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Devicetree file to pass to the invoked kernel, relative to the root directory of the file system containing the entry file"), SD_VARLINK_DEFINE_FIELD(devicetree, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Devicetree overlay file to pass to the invoked kernel, relative to the root directory of the file system containing the entry file"), SD_VARLINK_DEFINE_FIELD(devicetreeOverlay, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), SD_VARLINK_FIELD_COMMENT("Indicates whether the boot loader reported this entry on the current boot"), SD_VARLINK_DEFINE_FIELD(isReported, SD_VARLINK_BOOL, 0), @@ -83,12 +99,50 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_FIELD_COMMENT("The current state of the reboot-to-firmware-UI flag"), SD_VARLINK_DEFINE_OUTPUT(state, SD_VARLINK_BOOL, 0)); +static SD_VARLINK_DEFINE_ENUM_TYPE( + Operation, + SD_VARLINK_FIELD_COMMENT("Install the boot loader afresh, creating everything it needs"), + SD_VARLINK_DEFINE_ENUM_VALUE(new), + SD_VARLINK_FIELD_COMMENT("Just update existing boot loader binaries"), + SD_VARLINK_DEFINE_ENUM_VALUE(update)); + +static SD_VARLINK_DEFINE_ENUM_TYPE( + BootEntryTokenType, + SD_VARLINK_FIELD_COMMENT("Pick identifiers for type #1 boot entries based on /etc/machine-id"), + SD_VARLINK_DEFINE_ENUM_VALUE(machine_id), + SD_VARLINK_FIELD_COMMENT("Pick identifiers for type #1 boot entries based on the IMAGE_ID= field from /etc/os-release"), + SD_VARLINK_DEFINE_ENUM_VALUE(os_image_id), + SD_VARLINK_FIELD_COMMENT("Pick identifiers for type #1 boot entries based on the ID= field from /etc/os-release"), + SD_VARLINK_DEFINE_ENUM_VALUE(os_id), + SD_VARLINK_FIELD_COMMENT("Pick identifiers for type #1 boot entries based on a manually chosen string"), + SD_VARLINK_DEFINE_ENUM_VALUE(literal), + SD_VARLINK_FIELD_COMMENT("Choose automatically how to pick identifiers for type #1 boot entries"), + SD_VARLINK_DEFINE_ENUM_VALUE(auto)); + +static SD_VARLINK_DEFINE_METHOD( + Install, + SD_VARLINK_FIELD_COMMENT("Operation, either 'new' or 'update'"), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(operation, Operation, 0), + SD_VARLINK_FIELD_COMMENT("If true, continue on various failures"), + SD_VARLINK_DEFINE_INPUT(graceful, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + 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_INT, 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("If true the boot loader will be registered in an EFI boot entry via EFI variables, otherwise this is omitted"), + SD_VARLINK_DEFINE_INPUT(touchVariables, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); + static SD_VARLINK_DEFINE_ERROR( RebootToFirmwareNotSupported); static SD_VARLINK_DEFINE_ERROR( NoSuchBootEntry); +static SD_VARLINK_DEFINE_ERROR( + NoESPFound); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_BootControl, "io.systemd.BootControl", @@ -101,13 +155,21 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_type_BootEntryAddon, SD_VARLINK_SYMBOL_COMMENT("A structure encapsulating a boot entry"), &vl_type_BootEntry, + SD_VARLINK_SYMBOL_COMMENT("The operation to execute"), + &vl_type_Operation, SD_VARLINK_SYMBOL_COMMENT("Enumerates boot entries. Method call must be called with 'more' flag set. Each response returns one entry. If no entries are defined returns the NoSuchBootEntry error."), &vl_method_ListBootEntries, SD_VARLINK_SYMBOL_COMMENT("Sets the reboot-to-firmware-UI flag of the firmware, if this concept exists. Returns the RebootToFirmwareNotSupported error if not."), &vl_method_SetRebootToFirmware, SD_VARLINK_SYMBOL_COMMENT("Gets the current state of the reboot-to-firmware-UI flag of the firmware, if this concept exists. Returns the RebootToFirmwareNotSupported error if not."), &vl_method_GetRebootToFirmware, + SD_VARLINK_SYMBOL_COMMENT("The boot entry token type to use."), + &vl_type_BootEntryTokenType, + SD_VARLINK_SYMBOL_COMMENT("Install the boot loader on the ESP."), + &vl_method_Install, 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."), - &vl_error_NoSuchBootEntry); + &vl_error_NoSuchBootEntry, + SD_VARLINK_SYMBOL_COMMENT("No EFI System Partition (ESP) found."), + &vl_error_NoESPFound); diff --git a/units/systemd-bootctl.socket b/units/systemd-bootctl.socket index d4071833b6a..f9553b840be 100644 --- a/units/systemd-bootctl.socket +++ b/units/systemd-bootctl.socket @@ -8,7 +8,7 @@ # (at your option) any later version. [Unit] -Description=Boot Entries Service Socket +Description=Boot Loader Control Service Socket Documentation=man:bootctl(1) DefaultDependencies=no After=local-fs.target diff --git a/units/systemd-bootctl@.service b/units/systemd-bootctl@.service index 3f49e104573..7a83d25b38b 100644 --- a/units/systemd-bootctl@.service +++ b/units/systemd-bootctl@.service @@ -8,7 +8,7 @@ # (at your option) any later version. [Unit] -Description=Boot Entries Service +Description=Boot Loader Control Service Documentation=man:bootctl(1) DefaultDependencies=no Conflicts=shutdown.target From 7ee2f6657f8ef5154784d3977b290a38fb25fe26 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 8 Sep 2025 10:16:22 +0200 Subject: [PATCH 3/7] bootctl: parse install source via our usual string table helpers --- src/bootctl/bootctl.c | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 889c5f91b67..dee65d51d7f 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -31,6 +31,7 @@ #include "parse-argument.h" #include "path-util.h" #include "pretty-print.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "utf8.h" @@ -93,6 +94,14 @@ STATIC_DESTRUCTOR_REGISTER(arg_certificate_source, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep); +static const char* const install_source_table[_INSTALL_SOURCE_MAX] = { + [INSTALL_SOURCE_IMAGE] = "image", + [INSTALL_SOURCE_HOST] = "host", + [INSTALL_SOURCE_AUTO] = "auto", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(install_source, InstallSource); + int acquire_esp( int unprivileged_mode, bool graceful, @@ -481,18 +490,14 @@ static int parse_argv(int argc, char *argv[]) { return r; break; - case ARG_INSTALL_SOURCE: - if (streq(optarg, "auto")) - arg_install_source = INSTALL_SOURCE_AUTO; - else if (streq(optarg, "image")) - arg_install_source = INSTALL_SOURCE_IMAGE; - else if (streq(optarg, "host")) - arg_install_source = INSTALL_SOURCE_HOST; - else - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Unexpected parameter for --install-source=: %s", optarg); + case ARG_INSTALL_SOURCE: { + InstallSource is = install_source_from_string(optarg); + if (is < 0) + return log_error_errno(is, "Unexpected parameter for --install-source=: %s", optarg); + arg_install_source = is; break; + } case 'p': arg_print_esp_path = true; From 91b3620b07f29342261a3cbdaaaa3f83f21895e1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sat, 20 Sep 2025 08:38:51 +0200 Subject: [PATCH 4/7] bootctl: optionally include backing disk name in efi boot option description --- man/bootctl.xml | 12 +++++ src/bootctl/bootctl-install.c | 64 +++++++++++++++++++++++-- src/bootctl/bootctl.c | 88 ++++++++++++++++++----------------- src/bootctl/bootctl.h | 7 +++ 4 files changed, 125 insertions(+), 46 deletions(-) diff --git a/man/bootctl.xml b/man/bootctl.xml index b317d840209..1e98b5ca7a6 100644 --- a/man/bootctl.xml +++ b/man/bootctl.xml @@ -524,6 +524,18 @@ + + + Takes a boolean, defaults to false. Controls whether to append disk model information + to the firmware boot option item description (as configured with + above). This is useful when installing multiple + operating systems on separate disks on the same system, as it ensures the firmware boot options are discernable + and give a hint which disk is booted into. Note that this uses hardware model information, and hence + might not be too useful in case multiple disks of an identical model are used. + + + + Dry run for and . diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index d6ce3471b11..976c5380e1f 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -3,9 +3,11 @@ #include #include +#include "sd-device.h" #include "sd-varlink.h" #include "alloc-util.h" +#include "blockdev-util.h" #include "boot-entry.h" #include "bootctl.h" #include "bootctl-install.h" @@ -1321,8 +1323,56 @@ static int remove_from_order(uint16_t slot) { return 0; } -static const char *pick_efi_boot_option_description(void) { - return arg_efi_boot_option_description ?: "Linux Boot Manager"; +static int pick_efi_boot_option_description(int esp_fd, char **ret) { + int r; + + assert(esp_fd >= 0); + assert(ret); + + /* early declarations, so that they are definitely initialized even if we follow any of the gotos */ + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + _cleanup_free_ char *j = NULL; + + const char *b = arg_efi_boot_option_description ?: "Linux Boot Manager"; + if (!arg_efi_boot_option_description_with_device) + goto fallback; + + r = block_device_new_from_fd( + esp_fd, + BLOCK_DEVICE_LOOKUP_WHOLE_DISK|BLOCK_DEVICE_LOOKUP_BACKING, + &d); + if (r < 0) { + log_debug_errno(r, "Failed to find backing device of ESP: %m"); + goto fallback; + } + + const char *serial; + r = sd_device_get_property_value(d, "ID_SERIAL", &serial); + if (r < 0) { + log_debug_errno(r, "Unable to read ID_SERIAL field of backing device of ESP: %m"); + goto fallback; + } + + j = strjoin(b, " (", serial, ")"); + if (!j) + return log_oom(); + + if (strlen(j) > EFI_BOOT_OPTION_DESCRIPTION_MAX) { + log_debug("Boot option string suffixed with device serial would be too long, skipping: %s", j); + j = mfree(j); + goto fallback; + } + + *ret = TAKE_PTR(j); + return 0; + +fallback: + j = strdup(b); + if (!j) + return log_oom(); + + *ret = TAKE_PTR(j); + return 0; } static int install_variables( @@ -1369,9 +1419,15 @@ static int install_variables( bool existing = r > 0; if (c->operation == INSTALL_NEW || !existing) { + _cleanup_free_ char *description = NULL; + + r = pick_efi_boot_option_description(esp_fd, &description); + if (r < 0) + return r; + r = efi_add_boot_option( slot, - pick_efi_boot_option_description(), + description, c->esp_part, c->esp_pstart, c->esp_psize, @@ -1388,7 +1444,7 @@ static int install_variables( log_info("%s EFI boot entry \"%s\".", existing ? "Updated" : "Created", - pick_efi_boot_option_description()); + description); } return insert_into_order(c, slot); diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index dee65d51d7f..8d3cabc8650 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -40,12 +40,6 @@ #include "verbs.h" #include "virt.h" -/* EFI_BOOT_OPTION_DESCRIPTION_MAX sets the maximum length for the boot option description - * stored in NVRAM. The UEFI spec does not specify a minimum or maximum length for this - * 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) - static GracefulMode _arg_graceful = ARG_GRACEFUL_NO; char *arg_esp_path = NULL; @@ -70,6 +64,7 @@ char *arg_root = NULL; char *arg_image = NULL; InstallSource arg_install_source = INSTALL_SOURCE_AUTO; char *arg_efi_boot_option_description = NULL; +bool arg_efi_boot_option_description_with_device = false; bool arg_dry_run = false; ImagePolicy *arg_image_policy = NULL; bool arg_varlink = false; @@ -349,6 +344,8 @@ static int help(int argc, char *argv[], void *userdata) { " Install all supported EFI architectures\n" " --efi-boot-option-description=DESCRIPTION\n" " Description of the entry in the boot option list\n" + " --efi-boot-option-description-with-device=yes\n" + " Suffix description with disk vendor/model/serial\n" " --dry-run Dry run (unlink and cleanup)\n" " --secure-boot-auto-enroll=yes|no\n" " Set up secure boot auto-enrollment\n" @@ -398,6 +395,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_JSON, ARG_ARCH_ALL, ARG_EFI_BOOT_OPTION_DESCRIPTION, + ARG_EFI_BOOT_OPTION_DESCRIPTION_WITH_DEVICE, ARG_DRY_RUN, ARG_PRINT_LOADER_PATH, ARG_PRINT_STUB_PATH, @@ -409,39 +407,40 @@ static int parse_argv(int argc, char *argv[]) { }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "esp-path", required_argument, NULL, ARG_ESP_PATH }, - { "path", required_argument, NULL, ARG_ESP_PATH }, /* Compatibility alias */ - { "boot-path", required_argument, NULL, ARG_BOOT_PATH }, - { "root", required_argument, NULL, ARG_ROOT }, - { "image", required_argument, NULL, ARG_IMAGE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "install-source", required_argument, NULL, ARG_INSTALL_SOURCE }, - { "print-esp-path", no_argument, NULL, 'p' }, - { "print-path", no_argument, NULL, 'p' }, /* Compatibility alias */ - { "print-boot-path", no_argument, NULL, 'x' }, - { "print-loader-path", no_argument, NULL, ARG_PRINT_LOADER_PATH }, - { "print-stub-path", no_argument, NULL, ARG_PRINT_STUB_PATH }, - { "print-root-device", no_argument, NULL, 'R' }, - { "variables", required_argument, NULL, ARG_VARIABLES }, - { "no-variables", no_argument, NULL, ARG_NO_VARIABLES }, /* Compatibility alias */ - { "random-seed", required_argument, NULL, ARG_RANDOM_SEED }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "graceful", no_argument, NULL, ARG_GRACEFUL }, - { "quiet", no_argument, NULL, 'q' }, - { "make-entry-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, - { "make-machine-id-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, /* Compatibility alias */ - { "entry-token", required_argument, NULL, ARG_ENTRY_TOKEN }, - { "json", required_argument, NULL, ARG_JSON }, - { "all-architectures", no_argument, NULL, ARG_ARCH_ALL }, - { "efi-boot-option-description", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION }, - { "dry-run", no_argument, NULL, ARG_DRY_RUN }, - { "secure-boot-auto-enroll", required_argument, NULL, ARG_SECURE_BOOT_AUTO_ENROLL }, - { "certificate", required_argument, NULL, ARG_CERTIFICATE }, - { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE }, - { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, - { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "esp-path", required_argument, NULL, ARG_ESP_PATH }, + { "path", required_argument, NULL, ARG_ESP_PATH }, /* Compatibility alias */ + { "boot-path", required_argument, NULL, ARG_BOOT_PATH }, + { "root", required_argument, NULL, ARG_ROOT }, + { "image", required_argument, NULL, ARG_IMAGE }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "install-source", required_argument, NULL, ARG_INSTALL_SOURCE }, + { "print-esp-path", no_argument, NULL, 'p' }, + { "print-path", no_argument, NULL, 'p' }, /* Compatibility alias */ + { "print-boot-path", no_argument, NULL, 'x' }, + { "print-loader-path", no_argument, NULL, ARG_PRINT_LOADER_PATH }, + { "print-stub-path", no_argument, NULL, ARG_PRINT_STUB_PATH }, + { "print-root-device", no_argument, NULL, 'R' }, + { "variables", required_argument, NULL, ARG_VARIABLES }, + { "no-variables", no_argument, NULL, ARG_NO_VARIABLES }, /* Compatibility alias */ + { "random-seed", required_argument, NULL, ARG_RANDOM_SEED }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "graceful", no_argument, NULL, ARG_GRACEFUL }, + { "quiet", no_argument, NULL, 'q' }, + { "make-entry-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, + { "make-machine-id-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, /* Compatibility alias */ + { "entry-token", required_argument, NULL, ARG_ENTRY_TOKEN }, + { "json", required_argument, NULL, ARG_JSON }, + { "all-architectures", no_argument, NULL, ARG_ARCH_ALL }, + { "efi-boot-option-description", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION }, + { "efi-boot-option-description-with-device", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION_WITH_DEVICE }, + { "dry-run", no_argument, NULL, ARG_DRY_RUN }, + { "secure-boot-auto-enroll", required_argument, NULL, ARG_SECURE_BOOT_AUTO_ENROLL }, + { "certificate", required_argument, NULL, ARG_CERTIFICATE }, + { "certificate-source", required_argument, NULL, ARG_CERTIFICATE_SOURCE }, + { "private-key", required_argument, NULL, ARG_PRIVATE_KEY }, + { "private-key-source", required_argument, NULL, ARG_PRIVATE_KEY_SOURCE }, {} }; @@ -577,9 +576,7 @@ static int parse_argv(int argc, char *argv[]) { case ARG_EFI_BOOT_OPTION_DESCRIPTION: if (isempty(optarg) || !(string_is_safe(optarg) && utf8_is_valid(optarg))) { - _cleanup_free_ char *escaped = NULL; - - escaped = cescape(optarg); + _cleanup_free_ char *escaped = cescape(optarg); return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --efi-boot-option-description=: %s", strna(escaped)); } @@ -592,6 +589,13 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_EFI_BOOT_OPTION_DESCRIPTION_WITH_DEVICE: + r = parse_boolean_argument("--efi-boot-option-description-with-device=", optarg, &arg_efi_boot_option_description_with_device); + if (r < 0) + return r; + + break; + case ARG_DRY_RUN: arg_dry_run = true; break; diff --git a/src/bootctl/bootctl.h b/src/bootctl/bootctl.h index f4d6bb40ba8..93a1ca733b6 100644 --- a/src/bootctl/bootctl.h +++ b/src/bootctl/bootctl.h @@ -37,6 +37,7 @@ extern char *arg_root; extern char *arg_image; extern InstallSource arg_install_source; extern char *arg_efi_boot_option_description; +extern bool arg_efi_boot_option_description_with_device; extern bool arg_dry_run; extern ImagePolicy *arg_image_policy; extern bool arg_varlink; @@ -59,3 +60,9 @@ int acquire_esp(int unprivileged_mode, bool graceful, uint32_t *ret_part, uint64 int acquire_xbootldr(int unprivileged_mode, sd_id128_t *ret_uuid, dev_t *ret_devid); bool touch_variables(void); + +/* EFI_BOOT_OPTION_DESCRIPTION_MAX sets the maximum length for the boot option description + * stored in NVRAM. The UEFI spec does not specify a minimum or maximum length for this + * 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) From 9b29d38c33dcdb56a51d0baf577a8d6d4afde4e4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 23 Jan 2026 21:04:55 +0100 Subject: [PATCH 5/7] bootctl: return recognizable Varlink error when we cannot determine the boot entry token When running "bootctl install" on an empty --root= dir, we don't know which token to use, and the operation will fail. Make sure to return an explicit error about this. This introduces a recognizable low-level error for this (EUNATCH), and then turns this into a recognizable Varlink error. (I made sure that the old low-level error EINVAL wasn't load-bearing, and it is safe to change this.) --- src/bootctl/bootctl-install.c | 2 ++ src/shared/boot-entry.c | 12 +++++++----- src/shared/varlink-io.systemd.BootControl.c | 7 ++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index 976c5380e1f..2d9a151212d 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -2087,6 +2087,8 @@ int vl_method_install( return r; r = run_install(&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; diff --git a/src/shared/boot-entry.c b/src/shared/boot-entry.c index 042522951cc..0f1d8090247 100644 --- a/src/shared/boot-entry.c +++ b/src/shared/boot-entry.c @@ -155,6 +155,8 @@ int boot_entry_token_ensure_at( assert(type); assert(token); + /* Returns -EUNATCH if the selected token is not set */ + if (*token) return 0; /* Already set. */ @@ -181,7 +183,7 @@ int boot_entry_token_ensure_at( return r; } - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + return log_error_errno(SYNTHETIC_ERRNO(EUNATCH), "No machine ID set, and /etc/os-release carries no ID=/IMAGE_ID= fields."); case BOOT_ENTRY_TOKEN_MACHINE_ID: @@ -189,14 +191,14 @@ int boot_entry_token_ensure_at( if (r != 0) return r; - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No machine ID set."); + return log_error_errno(SYNTHETIC_ERRNO(EUNATCH), "No machine ID set."); case BOOT_ENTRY_TOKEN_OS_IMAGE_ID: r = entry_token_from_os_release(rfd, type, token); if (r != 0) return r; - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + return log_error_errno(SYNTHETIC_ERRNO(EUNATCH), "IMAGE_ID= field not set in /etc/os-release."); case BOOT_ENTRY_TOKEN_OS_ID: @@ -204,12 +206,12 @@ int boot_entry_token_ensure_at( if (r != 0) return r; - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + return log_error_errno(SYNTHETIC_ERRNO(EUNATCH), "ID= field not set in /etc/os-release."); case BOOT_ENTRY_TOKEN_LITERAL: /* In this case, the token should be already set by the user input. */ - return -EINVAL; + return log_error_errno(SYNTHETIC_ERRNO(EUNATCH), "Literal token indicated but not specified."); default: assert_not_reached(); diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c index 453002aaa9f..a24b4b076e0 100644 --- a/src/shared/varlink-io.systemd.BootControl.c +++ b/src/shared/varlink-io.systemd.BootControl.c @@ -143,6 +143,9 @@ static SD_VARLINK_DEFINE_ERROR( static SD_VARLINK_DEFINE_ERROR( NoESPFound); +static SD_VARLINK_DEFINE_ERROR( + BootEntryTokenUnavailable); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_BootControl, "io.systemd.BootControl", @@ -172,4 +175,6 @@ SD_VARLINK_DEFINE_INTERFACE( SD_VARLINK_SYMBOL_COMMENT("No boot entry defined."), &vl_error_NoSuchBootEntry, SD_VARLINK_SYMBOL_COMMENT("No EFI System Partition (ESP) found."), - &vl_error_NoESPFound); + &vl_error_NoESPFound, + SD_VARLINK_SYMBOL_COMMENT("The select boot entry token could not be determined."), + &vl_error_BootEntryTokenUnavailable); From 1dad3b67628719959491e8b35724e975ed262705 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 23 Jan 2026 13:09:08 +0100 Subject: [PATCH 6/7] bootctl: add test case for bootctl install via varlink --- test/units/TEST-87-AUX-UTILS-VM.bootctl.sh | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh b/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh index 8a63bb03b53..7d26541ffa6 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh @@ -27,9 +27,11 @@ restore_esp() { fi if [ -d /tmp/esp.bak/EFI/ ]; then + mkdir -p "$(bootctl --print-esp-path)/EFI/" cp -r /tmp/esp.bak/EFI/* "$(bootctl --print-esp-path)/EFI/" fi if [ -d /tmp/esp.bak/loader/ ]; then + mkdir -p "$(bootctl --print-esp-path)/loader/" cp -r /tmp/esp.bak/loader/* "$(bootctl --print-esp-path)/loader/" fi rm -rf /tmp/esp.bak @@ -40,13 +42,19 @@ backup_esp() { return fi + # make a backup of the two key dirs in the ESP, and delete them + if [[ -d "$(bootctl --print-esp-path)/EFI" ]]; then mkdir -p /tmp/esp.bak cp -r "$(bootctl --print-esp-path)/EFI/" /tmp/esp.bak/ + rm -rf "$(bootctl --print-esp-path)/EFI" + mkdir "$(bootctl --print-esp-path)/EFI" fi if [[ -d "$(bootctl --print-esp-path)/loader" ]]; then mkdir -p /tmp/esp.bak cp -r "$(bootctl --print-esp-path)/loader/" /tmp/esp.bak/ + rm -rf "$(bootctl --print-esp-path)/loader" + mkdir "$(bootctl --print-esp-path)/loader" fi } @@ -364,4 +372,22 @@ testcase_00_secureboot() { grep -q addonfoobar /proc/cmdline } +remove_root_dir() { + rm -rf "$ROOTDIR" +} + +testcase_install_varlink() { + + varlinkctl introspect "$(type -p bootctl)" + + if [ $# -eq 0 ]; then + backup_esp + trap restore_esp RETURN ERR + fi + + (! bootctl is-installed ) + SYSTEMD_LOG_TARGET=console varlinkctl call "$(type -p bootctl)" io.systemd.BootControl.Install "{\"operation\":\"new\",\"touchVariables\":false}" + bootctl is-installed +} + run_testcases From a537c5e4b8c424453f629506fb2188b5b6e2e0e4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 6 Feb 2026 22:31:53 +0100 Subject: [PATCH 7/7] bootctl: add comments emphasizing that certain functions do not touch the file read pointer --- src/bootctl/bootctl-install.c | 2 ++ src/bootctl/bootctl-util.c | 2 ++ src/shared/pe-binary.c | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c index 2d9a151212d..8399a2da721 100644 --- a/src/bootctl/bootctl-install.c +++ b/src/bootctl/bootctl-install.c @@ -490,6 +490,8 @@ static int version_check(int fd_from, const char *from, int fd_to, const char *t assert(fd_to >= 0); assert(to); + /* Does not reposition file offset */ + r = get_file_version(fd_from, &a); if (r == -ESRCH) return log_notice_errno(r, "Source file \"%s\" does not carry version information!", from); diff --git a/src/bootctl/bootctl-util.c b/src/bootctl/bootctl-util.c index 23b8786f1d8..071accb77d6 100644 --- a/src/bootctl/bootctl-util.c +++ b/src/bootctl/bootctl-util.c @@ -75,6 +75,8 @@ int get_file_version(int fd, char **ret) { assert(fd >= 0); assert(ret); + /* Does not reposition file offset (as it uses mmap()) */ + if (fstat(fd, &st) < 0) return log_error_errno(errno, "Failed to stat EFI binary: %m"); diff --git a/src/shared/pe-binary.c b/src/shared/pe-binary.c index 97e5ba51c78..0a1b93118c2 100644 --- a/src/shared/pe-binary.c +++ b/src/shared/pe-binary.c @@ -12,6 +12,8 @@ #include "string-table.h" #include "string-util.h" +/* Note: none of these function change the file position of the provided fd, as they use pread() */ + bool pe_header_is_64bit(const PeHeader *h) { assert(h);