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.
This commit is contained in:
Lennart Poettering
2026-05-01 07:10:31 +02:00
parent 4d0f6ac593
commit e68bf712be
10 changed files with 611 additions and 125 deletions
+21 -4
View File
@@ -102,11 +102,17 @@
</varlistentry>
<varlistentry>
<term><option>unlink</option> <replaceable>ID</replaceable></term>
<term><option>unlink</option> <optional><replaceable>ID</replaceable></optional></term>
<listitem><para>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.</para>
<listitem><para>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.</para>
<para>If no <replaceable>ID</replaceable> argument is specified, the <option>--oldest</option> 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).</para>
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry>
@@ -560,6 +566,17 @@
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--oldest</option></term>
<listitem><para>When used with <command>unlink</command>, 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.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--secure-boot-auto-enroll=yes|no</option></term>
<term><option>--private-key=<replaceable>PATH/URI</replaceable></option></term>
+6 -4
View File
@@ -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'
)
+21
View File
@@ -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)' \
+4 -3
View File
@@ -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;
}
+517 -110
View File
@@ -3,20 +3,70 @@
#include <fcntl.h>
#include <fnmatch.h>
#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);
}
+5 -1
View File
@@ -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);
+13 -2
View File
@@ -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");
+1
View File
@@ -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 */
+2
View File
@@ -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;
+21 -1
View File
@@ -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);