diff --git a/man/bootctl.xml b/man/bootctl.xml
index 558891eaa1..fb5f4b2b2a 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 d7714731c2..792fc0c0ac 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 f7ed2a8e41..c23c1c888d 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 1e8819bea1..011567d187 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 0d0e7ad076..80e74926c6 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);
+ r = boot_entry_ref_files(e, &known_files, +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);
- 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);
+ 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;
+ }
+
+ _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("Would remove \"%s\"", path);
- return;
+ log_info("Removed '%s'.", resolved_parent);
}
- 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);
- }
+ return 1;
}
-static int boot_config_find_in(const BootConfig *config, const char *root, const char *id) {
- assert(config);
+static ssize_t boot_config_find_in(
+ const BootConfig *config,
+ BootEntrySource source,
+ const char *id) {
- if (!root || !id)
+ assert(config);
+ assert(source >= 0);
+ assert(source < _BOOT_ENTRY_SOURCE_MAX);
+
+ 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 boot_entry_unlink(
+ const BootEntry *e,
+ const char *root,
+ int root_fd,
+ Hashmap *known_files,
+ bool dry_run) {
+
int r;
- assert(config);
+ assert(e);
+ assert(root_fd >= 0);
- 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);
-
- e = &config->entries[r];
-
- 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;
- r = 0;
- RET_GATHER(r, unlink_entry(&config, arg_esp_path, argv[1]));
+ _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");
- if (arg_xbootldr_path && xbootldr_devid != esp_devid)
- RET_GATHER(r, unlink_entry(&config, arg_xbootldr_path, argv[1]));
+ 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;
+ if (c->esp_path)
+ RET_GATHER(r, unlink_entry(&config, x ?: c->esp_path, c->esp_fd, BOOT_ENTRY_ESP, ids, dry_run));
+
+ 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 5c33088859..728c775d26 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 6869e838cf..67a20814da 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 d0daab9dd1..ea097ba329 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 1207fe8a25..e850d8982b 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 70203300ff..42f7b53492 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);