From: Lennart Poettering Date: Fri, 10 Apr 2026 12:48:25 +0000 (+0200) Subject: bootctl: rework/modernize "unlink" and add Varlink API for it X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e68bf712be1b348ae8c13fe0e04cc1f42d57ca9e;p=thirdparty%2Fsystemd.git bootctl: rework/modernize "unlink" and add Varlink API for it Among other things this changes tracking of the location of resources during GC from using the BootEntrySource enum rather than a path, since we have that and it is more efficient and easier to grok. --- diff --git a/man/bootctl.xml b/man/bootctl.xml index 558891eaa11..fb5f4b2b2a9 100644 --- a/man/bootctl.xml +++ b/man/bootctl.xml @@ -102,11 +102,17 @@ - ID + ID - Removes a boot loader entry including the files it refers to. Takes a single boot - loader entry ID string or a glob pattern as argument. Referenced files such as kernel or initrd are - only removed if no other entry refers to them. + Removes a boot loader entry including the files it refers to. Takes an optional boot + loader entry ID string or a glob pattern as argument. Referenced files such as the kernel, initrds, + system extensions (sysexts), configuration extensions (confexts) or credential files are only removed + if no other entry refers to them. + + If no ID argument is specified, the option + must be specified, in which case the boot loader entry with the lowest version is removed (for + robustness reasons the currently booted menu entry is never removed, nor is the last existing boot + loader entry). @@ -560,6 +566,17 @@ + + + + When used with unlink, selects the oldest installed boot loader + entry matching the boot entry token for removal (rather than passing an explicit entry ID). This is + useful for pruning older installed boot loader entries. Note that the currently booted entry is never + removed, nor is the last remaining one. + + + + diff --git a/shell-completion/bash/bootctl b/shell-completion/bash/bootctl index d7714731c2a..792fc0c0acc 100644 --- a/shell-completion/bash/bootctl +++ b/shell-completion/bash/bootctl @@ -40,8 +40,10 @@ _bootctl() { --dry-run' [ARG]='--esp-path --boot-path --root --image --image-policy --install-source --variables --random-seed --make-entry-directory --entry-token --json - --efi-boot-option-description --secure-boot-auto-enroll --private-key - --private-key-source --certificate --certificate-source' + --efi-boot-option-description --efi-boot-option-description-with-device + --secure-boot-auto-enroll --private-key + --private-key-source --certificate --certificate-source + --oldest' ) if __contains_word "$prev" ${OPTS[ARG]}; then @@ -67,7 +69,7 @@ _bootctl() { --install-source) comps="image host auto" ;; - --random-seed|--variables|--secure-boot-auto-enroll) + --random-seed|--variables|--secure-boot-auto-enroll|--oldest|--efi-boot-option-description-with-device) comps="yes no" ;; --json) @@ -85,7 +87,7 @@ _bootctl() { local -A VERBS=( [STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup' - [BOOTENTRY]='set-default set-oneshot set-sysfail unlink' + [BOOTENTRY]='set-default set-oneshot set-sysfail set-preferred unlink' [BOOLEAN]='reboot-to-firmware' [FILE]='kernel-identify kernel-inspect' ) diff --git a/shell-completion/zsh/_bootctl b/shell-completion/zsh/_bootctl index f7ed2a8e414..c23c1c888da 100644 --- a/shell-completion/zsh/_bootctl +++ b/shell-completion/zsh/_bootctl @@ -24,10 +24,26 @@ _bootctl_set-oneshot() { _bootctl_comp_ids } +_bootctl_set-sysfail() { + _bootctl_comp_ids +} + +_bootctl_set-preferred() { + _bootctl_comp_ids +} + _bootctl_unlink() { _bootctl_comp_ids } +_bootctl_kernel-identify() { + _files +} + +_bootctl_kernel-inspect() { + _files +} + _bootctl_reboot-to-firmware() { local -a _completions _completions=( yes no ) @@ -49,10 +65,14 @@ _bootctl_reboot-to-firmware() { "list:List boot loader entries" "set-default:Set the default boot loader entry" "set-oneshot:Set the default boot loader entry only for the next boot" + "set-sysfail:Set boot loader entry used in case of a system failure" + "set-preferred:Set the preferred boot loader entry" "set-timeout:Set the menu timeout" "set-timeout-oneshot:Set the menu timeout for the next boot only" "unlink:Remove boot loader entry" "cleanup:Remove files in ESP not referenced in any boot entry" + "kernel-identify:Identify kernel image type" + "kernel-inspect:Print details about the kernel image" ) if (( CURRENT == 1 )); then _describe -t commands 'bootctl command' _bootctl_cmds || compadd "$@" @@ -79,6 +99,7 @@ _arguments \ '--no-pager[Do not pipe output into a pager]' \ '--graceful[Do not fail when locating ESP or writing fails]' \ '--dry-run[Dry run (unlink and cleanup)]' \ + '--oldest=[Delete oldest boot menu entry]:options:(yes no)' \ '--root=[Operate under the specified directory]:PATH' \ '--image=[Operate on the specified image]:PATH' \ '--install-source[Where to pick files when using --root=/--image=]:options:(image host auto)' \ diff --git a/src/bootctl/bootctl-cleanup.c b/src/bootctl/bootctl-cleanup.c index 1e8819bea18..011567d187b 100644 --- a/src/bootctl/bootctl-cleanup.c +++ b/src/bootctl/bootctl-cleanup.c @@ -49,6 +49,7 @@ static int list_remove_orphaned_file( static int cleanup_orphaned_files( const BootConfig *config, + BootEntrySource source, const char *root) { _cleanup_hashmap_free_ Hashmap *known_files = NULL; @@ -65,7 +66,7 @@ static int cleanup_orphaned_files( if (r < 0) return r; - r = boot_config_count_known_files(config, root, &known_files); + r = boot_config_count_known_files(config, source, &known_files); if (r < 0) return log_error_errno(r, "Failed to count files in %s: %m", root); @@ -116,10 +117,10 @@ int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) { return r; r = 0; - RET_GATHER(r, cleanup_orphaned_files(&config, arg_esp_path)); + RET_GATHER(r, cleanup_orphaned_files(&config, BOOT_ENTRY_ESP, arg_esp_path)); if (arg_xbootldr_path && xbootldr_devid != esp_devid) - RET_GATHER(r, cleanup_orphaned_files(&config, arg_xbootldr_path)); + RET_GATHER(r, cleanup_orphaned_files(&config, BOOT_ENTRY_XBOOTLDR, arg_xbootldr_path)); return r; } diff --git a/src/bootctl/bootctl-unlink.c b/src/bootctl/bootctl-unlink.c index 0d0e7ad076b..80e74926c6c 100644 --- a/src/bootctl/bootctl-unlink.c +++ b/src/bootctl/bootctl-unlink.c @@ -3,20 +3,70 @@ #include #include +#include "sd-id128.h" +#include "sd-json.h" +#include "sd-varlink.h" + #include "alloc-util.h" +#include "boot-entry.h" #include "bootctl.h" #include "bootctl-unlink.h" #include "bootspec.h" #include "bootspec-util.h" #include "chase.h" +#include "efi-loader.h" #include "errno-util.h" +#include "fd-util.h" +#include "find-esp.h" #include "hashmap.h" +#include "id128-util.h" +#include "json-util.h" #include "log.h" #include "path-util.h" +#include "stat-util.h" +#include "string-util.h" #include "strv.h" +typedef struct UnlinkContext { + char *root; + int root_fd; + + sd_id128_t machine_id; + BootEntryTokenType entry_token_type; + char *entry_token; + + char *esp_path; + dev_t esp_devid; + int esp_fd; + + char *xbootldr_path; + dev_t xbootldr_devid; + int xbootldr_fd; +} UnlinkContext; + +#define UNLINK_CONTEXT_NULL \ + (UnlinkContext) { \ + .root_fd = -EBADF, \ + .entry_token_type = _BOOT_ENTRY_TOKEN_TYPE_INVALID, \ + .esp_fd = -EBADF, \ + .xbootldr_fd = -EBADF, \ + } + +static void unlink_context_done(UnlinkContext *c) { + assert(c); + + c->root = mfree(c->root); + c->root_fd = safe_close(c->root_fd); + + 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); +} + static int ref_file(Hashmap **known_files, const char *fn, int increment) { - char *k = NULL; int n, r; assert(known_files); @@ -26,13 +76,15 @@ static int ref_file(Hashmap **known_files, const char *fn, int increment) { if (!fn) return 0; + char *k = NULL; n = PTR_TO_INT(hashmap_get2(*known_files, fn, (void**)&k)); - n += increment; + if (!INC_SAFE(&n, increment)) + return -EOVERFLOW; assert(n >= 0); if (n == 0) { - (void) hashmap_remove(*known_files, fn); + (void) hashmap_remove(*known_files, k); free(k); } else if (!k) { _cleanup_free_ char *t = NULL; @@ -40,12 +92,14 @@ static int ref_file(Hashmap **known_files, const char *fn, int increment) { t = strdup(fn); if (!t) return -ENOMEM; + r = hashmap_ensure_put(known_files, &path_hash_ops_free, t, INT_TO_PTR(n)); if (r < 0) return r; + TAKE_PTR(t); } else { - r = hashmap_update(*known_files, fn, INT_TO_PTR(n)); + r = hashmap_update(*known_files, k, INT_TO_PTR(n)); if (r < 0) return r; } @@ -53,195 +107,548 @@ static int ref_file(Hashmap **known_files, const char *fn, int increment) { return n; } +static int boot_entry_ref_files( + const BootEntry *e, + Hashmap **known_files, + int increment) { + + int r; + + assert(e); + assert(known_files); + assert(increment != 0); + + r = ref_file(known_files, e->kernel, increment); + if (r < 0) + return r; + + r = ref_file(known_files, e->efi, increment); + if (r < 0) + return r; + + r = ref_file(known_files, e->uki, increment); + if (r < 0) + return r; + + STRV_FOREACH(s, e->initrd) { + r = ref_file(known_files, *s, increment); + if (r < 0) + return r; + } + + r = ref_file(known_files, e->device_tree, increment); + if (r < 0) + return r; + + STRV_FOREACH(s, e->device_tree_overlay) { + r = ref_file(known_files, *s, increment); + if (r < 0) + return r; + } + + return 0; +} + int boot_config_count_known_files( const BootConfig *config, - const char* root, + BootEntrySource source, Hashmap **ret_known_files) { - _cleanup_hashmap_free_ Hashmap *known_files = NULL; int r; assert(config); assert(ret_known_files); - for (size_t i = 0; i < config->n_entries; i++) { - const BootEntry *e = config->entries + i; + _cleanup_hashmap_free_ Hashmap *known_files = NULL; + FOREACH_ARRAY(e, config->entries, config->n_entries) { - if (!path_equal(e->root, root)) + if (e->source != source) continue; - r = ref_file(&known_files, e->kernel, +1); - if (r < 0) - return r; - r = ref_file(&known_files, e->efi, +1); - if (r < 0) - return r; - r = ref_file(&known_files, e->uki, +1); - if (r < 0) - return r; - STRV_FOREACH(s, e->initrd) { - r = ref_file(&known_files, *s, +1); - if (r < 0) - return r; - } - r = ref_file(&known_files, e->device_tree, +1); + r = boot_entry_ref_files(e, &known_files, +1); if (r < 0) return r; - STRV_FOREACH(s, e->device_tree_overlay) { - r = ref_file(&known_files, *s, +1); - if (r < 0) - return r; - } } *ret_known_files = TAKE_PTR(known_files); - return 0; } -static void deref_unlink_file(Hashmap **known_files, const char *fn, const char *root) { - _cleanup_free_ char *path = NULL; +static int unref_unlink_file( + Hashmap **known_files, + const char *root, + int root_fd, + const char *path, + bool dry_run) { + int r; assert(known_files); /* just gracefully ignore this. This way the caller doesn't have to verify whether the bootloader entry is relevant */ - if (!fn || !root) - return; + if (root_fd < 0 || !root || !path) + return 0; - r = ref_file(known_files, fn, -1); + r = ref_file(known_files, path, -1); if (r < 0) - return (void) log_warning_errno(r, "Failed to deref \"%s\", ignoring: %m", fn); + return log_error_errno(r, "Failed to unref '%s': %m", path); if (r > 0) - return; + return 0; - if (arg_dry_run) { - r = chase_and_access(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, F_OK, &path); - if (r < 0) - log_info_errno(r, "Unable to determine whether \"%s\" exists, ignoring: %m", fn); - else - log_info("Would remove \"%s\"", path); - return; + if (dry_run) { + _cleanup_free_ char *resolved = NULL; + r = chase_and_accessat( + /* root_fd= */ root_fd, + /* dir_fd= */ root_fd, + path, + CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_REGULAR, + F_OK, + &resolved); + if (r < 0) { + log_warning_errno(r, "Unable to determine whether '%s' exists, ignoring: %m", path); + return 0; + } + + log_info("Would remove '%s'", resolved); + return 1; } - r = chase_and_unlink(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, 0, &path); - if (r >= 0) - log_info("Removed \"%s\"", path); - else if (r != -ENOENT) - return (void) log_warning_errno(r, "Failed to remove \"%s\", ignoring: %m", fn); - - _cleanup_free_ char *d = NULL; - if (path_extract_directory(fn, &d) >= 0 && !path_equal(d, "/")) { - r = chase_and_unlink(d, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, AT_REMOVEDIR, NULL); - if (r < 0 && !IN_SET(r, -ENOTEMPTY, -ENOENT)) - log_warning_errno(r, "Failed to remove directory \"%s\", ignoring: %m", d); + _cleanup_free_ char *resolved = NULL; + r = chase_and_unlinkat( + /* root_fd= */ root_fd, + /* dir_fd= */ root_fd, + path, + CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, + /* unlink_flags= */ 0, + &resolved); + if (r == -ENOENT) + log_debug("Resource '%s' is already removed, skipping.", path); + else if (r < 0) { + log_warning_errno(r, "Failed to remove '%s', ignoring: %m", path); + return 0; + } else + log_info("Removed '%s'", resolved); + + _cleanup_free_ char *parent = NULL; + r = path_extract_directory(path, &parent); + if (r < 0) + log_debug_errno(r, "Failed to extract parent directory of '%s', ignoring.", path); + else { + _cleanup_free_ char *resolved_parent = NULL; + r = chase_and_unlinkat( + /* root_fd= */ root_fd, + /* dir_fd= */ root_fd, + parent, + CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, + AT_REMOVEDIR, + &resolved_parent); + if (IN_SET(r, -ENOTEMPTY, -ENOENT)) + log_debug_errno(r, "Failed to remove directory '%s', ignoring: %m", parent); + else if (r < 0) + log_warning_errno(r, "Failed to remove directory '%s', ignoring: %m", parent); + else + log_info("Removed '%s'.", resolved_parent); } + + return 1; } -static int boot_config_find_in(const BootConfig *config, const char *root, const char *id) { +static ssize_t boot_config_find_in( + const BootConfig *config, + BootEntrySource source, + const char *id) { + assert(config); + assert(source >= 0); + assert(source < _BOOT_ENTRY_SOURCE_MAX); - if (!root || !id) + if (!id) return -ENOENT; for (size_t i = 0; i < config->n_entries; i++) - if (path_equal(config->entries[i].root, root) && + if (config->entries[i].source == source && fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0) - return i; + return (ssize_t) i; return -ENOENT; } -static int unlink_entry(const BootConfig *config, const char *root, const char *id) { - _cleanup_hashmap_free_ Hashmap *known_files = NULL; - const BootEntry *e = NULL; - int r; - - assert(config); +int boot_entry_unlink( + const BootEntry *e, + const char *root, + int root_fd, + Hashmap *known_files, + bool dry_run) { - r = boot_config_count_known_files(config, root, &known_files); - if (r < 0) - return log_error_errno(r, "Failed to count files in %s: %m", root); - - r = boot_config_find_in(config, root, id); - if (r < 0) - return 0; /* There is nothing to remove. */ - - if (r == config->default_entry) - log_warning("%s is the default boot entry", id); - if (r == config->selected_entry) - log_warning("%s is the selected boot entry", id); + int r; - e = &config->entries[r]; + assert(e); + assert(root_fd >= 0); - deref_unlink_file(&known_files, e->kernel, e->root); - deref_unlink_file(&known_files, e->efi, e->root); - deref_unlink_file(&known_files, e->uki, e->root); + (void) unref_unlink_file(&known_files, root, root_fd, e->kernel, dry_run); + (void) unref_unlink_file(&known_files, root, root_fd, e->efi, dry_run); + (void) unref_unlink_file(&known_files, root, root_fd, e->uki, dry_run); STRV_FOREACH(s, e->initrd) - deref_unlink_file(&known_files, *s, e->root); - deref_unlink_file(&known_files, e->device_tree, e->root); + (void) unref_unlink_file(&known_files, root, root_fd, *s, dry_run); + (void) unref_unlink_file(&known_files, root, root_fd, e->device_tree, dry_run); STRV_FOREACH(s, e->device_tree_overlay) - deref_unlink_file(&known_files, *s, e->root); + (void) unref_unlink_file(&known_files, root, root_fd, *s, dry_run); - if (arg_dry_run) + if (dry_run) log_info("Would remove \"%s\"", e->path); else { - r = chase_and_unlink(e->path, root, CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, 0, NULL); + const char *p = path_startswith(e->path, root); + if (!p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "File '%s' is not inside root '%s', refusing.", e->path, root); + + _cleanup_free_ char *resolved = NULL; + r = chase_and_unlinkat( + /* root_fd= */ root_fd, + /* dir_fd= */ root_fd, + p, + CHASE_PROHIBIT_SYMLINKS|CHASE_TRIGGER_AUTOFS, + /* unlink_flags= */ 0, + &resolved); if (r == -ENOENT) return 0; /* Already removed? */ if (r < 0) return log_error_errno(r, "Failed to remove \"%s\": %m", e->path); - log_info("Removed %s", e->path); + log_info("Removed '%s'.", resolved); } return 0; } -int verb_unlink(int argc, char *argv[], uintptr_t _data, void *userdata) { - dev_t esp_devid = 0, xbootldr_devid = 0; +static int unlink_entry( + const BootConfig *config, + const char *root, + int root_fd, + BootEntrySource source, + char **ids, + bool dry_run) { + + size_t n_removed = 0; + int r; + + assert(config); + + _cleanup_hashmap_free_ Hashmap *known_files = NULL; + r = boot_config_count_known_files(config, source, &known_files); + if (r < 0) + return log_error_errno(r, "Failed to count files in %s: %m", root); + + int ret = 0; + STRV_FOREACH(id, ids) { + log_debug("Unlinking '%s'", *id); + ssize_t idx = boot_config_find_in(config, source, *id); + if (idx < 0) + continue; /* There is nothing to remove. */ + + log_debug("Index %zi", idx); + + if (idx == config->default_entry) + log_warning("%s is the default boot entry", *id); + if (idx == config->selected_entry) + log_warning("%s is the selected boot entry", *id); + + r = boot_entry_unlink(config->entries + idx, root, root_fd, known_files, dry_run); + if (r < 0) + RET_GATHER(ret, r); + else + n_removed++; + } + + if (n_removed == 0) + log_info("No matching entries found or removed."); + + return ret; +} + +static int unlink_context_from_cmdline(UnlinkContext *ret) { int r; + assert(ret); + + _cleanup_(unlink_context_done) UnlinkContext b = UNLINK_CONTEXT_NULL; + b.entry_token_type = arg_entry_token_type; + + if (strdup_to(&b.entry_token, arg_entry_token) < 0) + return log_oom(); + + if (arg_root) { + b.root_fd = open(arg_root, O_CLOEXEC|O_DIRECTORY|O_PATH); + if (b.root_fd < 0) + return log_error_errno(errno, "Failed to open root directory '%s': %m", arg_root); + + if (strdup_to(&b.root, arg_root) < 0) + return log_oom(); + } else + b.root_fd = XAT_FDROOT; + r = acquire_esp(/* unprivileged_mode= */ false, /* graceful= */ false, - /* ret_fd= */ NULL, + &b.esp_fd, /* ret_part= */ NULL, /* ret_pstart= */ NULL, /* ret_psize= */ NULL, /* ret_uuid= */ NULL, - &esp_devid); - if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */ - return log_error_errno(r, "Failed to determine ESP location: %m"); - if (r < 0) - return r; + &b.esp_devid); + if (r < 0 && r != -ENOKEY) + return r; /* About all other errors acquire_esp() logs on its own */ + if (r > 0) { + 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_fd= */ NULL, + &b.xbootldr_fd, /* ret_uuid= */ NULL, - &xbootldr_devid); - if (r == -EACCES) - return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m"); - if (r < 0) + &b.xbootldr_devid); + if (r < 0 && r != -ENOKEY) return r; + if (r > 0) { + 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(); + } + + /* Only if we found neither ESP nor XBOOTLDR let's fail. */ + if (!b.xbootldr_path && !b.esp_path) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Neither ESP nor XBOOTLDR found, refusing."); + + *ret = TAKE_GENERIC(b, UnlinkContext, UNLINK_CONTEXT_NULL); + return 0; +} + +static int run_unlink( + UnlinkContext *c, + char **_ids, + bool dry_run) { + + int r; + assert(c); + + _cleanup_free_ char *x = NULL, *y = NULL; + if (c->root && c->esp_path) { + x = path_join(c->root, c->esp_path); + if (!x) + return log_oom(); + } + + if (c->root && c->xbootldr_path) { + y = path_join(c->root, c->xbootldr_path); + if (!y) + return log_oom(); + } _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL; r = boot_config_load_and_select( &config, - arg_root, - arg_esp_path, - esp_devid, - arg_xbootldr_path, - xbootldr_devid); + c->root, + x ?: c->esp_path, + c->esp_devid, + y ?: c->xbootldr_path, + c->xbootldr_devid); if (r < 0) return r; + _cleanup_(strv_freep) char **ids = NULL; + if (strv_isempty(_ids)) { + r = id128_get_machine_at(c->root_fd, &c->machine_id); + if (r < 0 && !ERRNO_IS_NEG_MACHINE_ID_UNSET(r)) + return log_error_errno(r, "Failed to get machine-id: %m"); + + 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; + + r = boot_config_find_oldest_commit( + &config, + c->entry_token, + &ids); + if (r == -ENXIO) + return log_error_errno(r, "No suitable boot menu entry to delete found."); + if (r == -EBUSY) + return log_error_errno(r, "Refusing to remove currently booted boot menu entry."); + if (r < 0) + return log_error_errno(r, "Failed to find suitable oldest boot menu entry: %m"); + + STRV_FOREACH(id, ids) + log_info("Will unlink '%s'.", *id); + } else { + ids = strv_copy(_ids); + if (!ids) + return log_oom(); + } + + strv_sort_uniq(ids); + r = 0; - RET_GATHER(r, unlink_entry(&config, arg_esp_path, argv[1])); + if (c->esp_path) + RET_GATHER(r, unlink_entry(&config, x ?: c->esp_path, c->esp_fd, BOOT_ENTRY_ESP, ids, dry_run)); - if (arg_xbootldr_path && xbootldr_devid != esp_devid) - RET_GATHER(r, unlink_entry(&config, arg_xbootldr_path, argv[1])); + if (c->xbootldr_path && c->xbootldr_devid != c->esp_devid) + RET_GATHER(r, unlink_entry(&config, y ?: c->xbootldr_path, c->xbootldr_fd, BOOT_ENTRY_XBOOTLDR, ids, dry_run)); return r; } + +int verb_unlink(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + assert(argc < 3); + + if (arg_oldest != isempty(argv[1])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Exactly one of an entry ID or --oldest= must be specified."); + + const char *id = empty_to_null(argv[1]); + + _cleanup_(unlink_context_done) UnlinkContext c = UNLINK_CONTEXT_NULL; + r = unlink_context_from_cmdline(&c); + if (r < 0) + return r; + + return run_unlink(&c, STRV_MAKE(id), arg_dry_run); +} + +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_boot_entry_token_type, BootEntryTokenType, boot_entry_token_type_from_string); + +typedef struct UnlinkParameters { + UnlinkContext context; + unsigned root_fd_index; + const char *id; + bool oldest; +} UnlinkParameters; + +static void unlink_parameters_done(UnlinkParameters *p) { + assert(p); + + unlink_context_done(&p->context); +} + +int vl_method_unlink( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + int r; + + assert(link); + + _cleanup_(unlink_parameters_done) UnlinkParameters p = { + .context = UNLINK_CONTEXT_NULL, + .root_fd_index = UINT_MAX, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "rootFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, root_fd_index), 0 }, + { "rootDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, voffsetof(p, context.root), 0 }, + { "bootEntryTokenType", SD_JSON_VARIANT_STRING, json_dispatch_boot_entry_token_type, voffsetof(p, context.entry_token_type), 0 }, + { "id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, id), 0 }, + { "oldest", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, voffsetof(p, oldest), 0 }, + {}, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + /* Only allow oldest *or* id to be set */ + if (p.oldest == !!p.id) + return sd_varlink_error_invalid_parameter_name(link, "id"); + if (p.id && !efi_loader_entry_name_valid(p.id)) + return sd_varlink_error_invalid_parameter_name(link, "id"); + + if (p.root_fd_index != UINT_MAX) { + p.context.root_fd = sd_varlink_peek_dup_fd(link, p.root_fd_index); + if (p.context.root_fd < 0) + return log_debug_errno(p.context.root_fd, "Failed to acquire root fd from Varlink: %m"); + + r = fd_verify_safe_flags_full(p.context.root_fd, O_DIRECTORY); + if (r < 0) + return sd_varlink_error_invalid_parameter_name(link, "rootFileDescriptor"); + + r = fd_verify_directory(p.context.root_fd); + if (r < 0) + return log_debug_errno(r, "Specified file descriptor does not refer to a directory: %m"); + + if (!p.context.root) { + r = fd_get_path(p.context.root_fd, &p.context.root); + if (r < 0) + return log_debug_errno(r, "Failed to get path of file descriptor: %m"); + + if (empty_or_root(p.context.root)) + p.context.root = mfree(p.context.root); + } + } else if (p.context.root) { + p.context.root_fd = open(p.context.root, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (p.context.root_fd < 0) + return log_debug_errno(errno, "Failed to open '%s': %m", p.context.root); + } else + p.context.root_fd = XAT_FDROOT; + + if (p.context.entry_token_type < 0) + p.context.entry_token_type = BOOT_ENTRY_TOKEN_AUTO; + + r = find_esp_and_warn_at_full( + p.context.root_fd, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &p.context.esp_path, + &p.context.esp_fd, + /* ret_part= */ NULL, + /* ret_pstart= */ NULL, + /* ret_psize= */ NULL, + /* ret_uuid= */ NULL, + &p.context.esp_devid); + if (r < 0 && r != -ENOKEY) + return r; + r = find_xbootldr_and_warn_at_full( + p.context.root_fd, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &p.context.xbootldr_path, + &p.context.xbootldr_fd, + /* ret_uuid= */ NULL, + &p.context.xbootldr_devid); + if (r < 0 && r != -ENOKEY) + return r; + + /* Only if we found neither ESP nor XBOOTLDR let's fail. */ + if (!p.context.xbootldr_path && !p.context.esp_path) + return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL); + + r = run_unlink(&p.context, STRV_MAKE(p.id), /* dry_run= */ false); + if (r == -EUNATCH) /* no boot entry token is set */ + return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL); + if (r < 0) + return r; + + return sd_varlink_reply(link, NULL); +} diff --git a/src/bootctl/bootctl-unlink.h b/src/bootctl/bootctl-unlink.h index 5c330888594..728c775d26e 100644 --- a/src/bootctl/bootctl-unlink.h +++ b/src/bootctl/bootctl-unlink.h @@ -5,4 +5,8 @@ int verb_unlink(int argc, char *argv[], uintptr_t _data, void *userdata); -int boot_config_count_known_files(const BootConfig *config, const char* root, Hashmap **ret_known_files); +int vl_method_unlink(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); + +int boot_config_count_known_files(const BootConfig *config, BootEntrySource source, Hashmap **ret_known_files); + +int boot_entry_unlink(const BootEntry *e, const char *root, int root_fd, Hashmap *known_files, bool dry_run); diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c index 6869e838cfc..67a20814daf 100644 --- a/src/bootctl/bootctl.c +++ b/src/bootctl/bootctl.c @@ -34,6 +34,7 @@ #include "options.h" #include "pager.h" #include "parse-argument.h" +#include "parse-util.h" #include "path-util.h" #include "pretty-print.h" #include "string-table.h" @@ -80,6 +81,7 @@ char *arg_certificate_source = NULL; char *arg_private_key = NULL; KeySourceType arg_private_key_source_type = OPENSSL_KEY_SOURCE_FILE; char *arg_private_key_source = NULL; +bool arg_oldest = false; STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep); @@ -361,7 +363,7 @@ VERB_GROUP("Boot Loader Specification Commands"); VERB_SCOPE_NOARG(, verb_list, "list", "List boot loader entries"); -VERB_SCOPE(, verb_unlink, "unlink", "ID", 2, 2, 0, +VERB_SCOPE(, verb_unlink, "unlink", "ID", VERB_ANY, 2, 0, "Remove boot loader entry"); VERB_SCOPE_NOARG(, verb_cleanup, "cleanup", @@ -631,6 +633,14 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { if (r < 0) return r; break; + + OPTION_LONG("oldest", "BOOL", + "Delete oldest boot menu entry"): + r = parse_boolean_argument("--oldest=", opts.arg, &arg_oldest); + if (r < 0) + return r; + + break; } char **args = option_parser_get_args(&opts); @@ -700,7 +710,8 @@ static int vl_server(void) { "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.Install", vl_method_install); + "io.systemd.BootControl.Install", vl_method_install, + "io.systemd.BootControl.Unlink", vl_method_unlink); if (r < 0) return log_error_errno(r, "Failed to bind Varlink methods: %m"); diff --git a/src/bootctl/bootctl.h b/src/bootctl/bootctl.h index d0daab9dd12..ea097ba3297 100644 --- a/src/bootctl/bootctl.h +++ b/src/bootctl/bootctl.h @@ -51,6 +51,7 @@ extern char *arg_certificate_source; extern char *arg_private_key; extern KeySourceType arg_private_key_source_type; extern char *arg_private_key_source; +extern bool arg_oldest; static inline const char* arg_dollar_boot_path(void) { /* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */ diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index 1207fe8a258..e850d8982bd 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -14,6 +14,7 @@ struct in_addr_full; typedef enum AskPasswordFlags AskPasswordFlags; typedef enum BootEntryTokenType BootEntryTokenType; +typedef enum BootEntrySource BootEntrySource; typedef enum BusPrintPropertyFlags BusPrintPropertyFlags; typedef enum BusTransport BusTransport; typedef enum CatFlags CatFlags; @@ -48,6 +49,7 @@ typedef enum UserStorage UserStorage; typedef struct AskPasswordRequest AskPasswordRequest; typedef struct Bitmap Bitmap; typedef struct BootConfig BootConfig; +typedef struct BootEntry BootEntry; typedef struct BPFProgram BPFProgram; typedef struct BusObjectImplementation BusObjectImplementation; typedef struct CalendarSpec CalendarSpec; diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c index 70203300ffe..42f7b53492e 100644 --- a/src/shared/varlink-io.systemd.BootControl.c +++ b/src/shared/varlink-io.systemd.BootControl.c @@ -134,6 +134,19 @@ static SD_VARLINK_DEFINE_METHOD( 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_METHOD( + Unlink, + SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors passed along with this message, pointing to file descriptor to root file system to operate on"), + SD_VARLINK_DEFINE_INPUT(rootFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Root directory to operate relative to. If both this and rootFileDescriptor is specified, this is purely informational. If only this is specified, it is what will be used."), + SD_VARLINK_DEFINE_INPUT(rootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Selects how to identify boot entries"), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(bootEntryTokenType, BootEntryTokenType, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The ID of the boot loader entry to remove."), + SD_VARLINK_DEFINE_INPUT(id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, remove the oldest entry."), + SD_VARLINK_DEFINE_INPUT(oldest, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); + static SD_VARLINK_DEFINE_ERROR( RebootToFirmwareNotSupported); @@ -143,6 +156,9 @@ static SD_VARLINK_DEFINE_ERROR( static SD_VARLINK_DEFINE_ERROR( NoESPFound); +static SD_VARLINK_DEFINE_ERROR( + NoDollarBootFound); + static SD_VARLINK_DEFINE_ERROR( BootEntryTokenUnavailable); @@ -170,11 +186,15 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_type_BootEntryTokenType, SD_VARLINK_SYMBOL_COMMENT("Install the boot loader on the ESP."), &vl_method_Install, + SD_VARLINK_SYMBOL_COMMENT("Unlink a boot menu item"), + &vl_method_Unlink, 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, SD_VARLINK_SYMBOL_COMMENT("No EFI System Partition (ESP) found."), &vl_error_NoESPFound, - SD_VARLINK_SYMBOL_COMMENT("The select boot entry token could not be determined."), + SD_VARLINK_SYMBOL_COMMENT("Neither ESP nor XBOOTLDR found, hence no $BOOT location identified."), + &vl_error_NoDollarBootFound, + SD_VARLINK_SYMBOL_COMMENT("The selected boot entry token could not be determined."), &vl_error_BootEntryTokenUnavailable);