]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
bootctl: rework/modernize "unlink" and add Varlink API for it
authorLennart Poettering <lennart@amutable.com>
Fri, 10 Apr 2026 12:48:25 +0000 (14:48 +0200)
committerLennart Poettering <lennart@amutable.com>
Fri, 1 May 2026 05:10:31 +0000 (07:10 +0200)
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.

man/bootctl.xml
shell-completion/bash/bootctl
shell-completion/zsh/_bootctl
src/bootctl/bootctl-cleanup.c
src/bootctl/bootctl-unlink.c
src/bootctl/bootctl-unlink.h
src/bootctl/bootctl.c
src/bootctl/bootctl.h
src/shared/shared-forward.h
src/shared/varlink-io.systemd.BootControl.c

index 558891eaa116976a29b35b340fcb8070ed2bce7e..fb5f4b2b2a9eb76b416fb2c20561bcf393fb9759 100644 (file)
       </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>
         <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>
index d7714731c2aac8087fb0ac162d9b6637a12864a9..792fc0c0acc83334b77e3848d3257578e6c0d539 100644 (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'
     )
index f7ed2a8e4148ad34212d4b012f9626e87848d58b..c23c1c888dae55438dff24e8f4e849963f781373 100644 (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)' \
index 1e8819bea18134eb1820da641a4c20496cb4329d..011567d187be67a0dcf657fe799b4d7a1c40ef13 100644 (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;
 }
index 0d0e7ad076b602aafc828662bd0f3a3f6422d454..80e74926c6c7601e7441c4d0cdb77d3defdc90f2 100644 (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);
-                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);
+}
index 5c33088859437f1ca28447a02ab1959b73c6c5e1..728c775d26e8ee9006a2a3bac717a90d78df6121 100644 (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);
index 6869e838cfc4ecee614348efbd8d1407fcd3d1ca..67a20814daf9e6ec5fcb493abefd76cbb0e26d15 100644 (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");
 
index d0daab9dd12b311be2a40c4ec9c746f1d2dcc354..ea097ba329753b2fbcedeefbf368b702bfad8f99 100644 (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 */
index 1207fe8a258266e6f2f9c2dbd3d18caf5b6dc02b..e850d8982bd303a4aa16fd994d33eeed77e55a0d 100644 (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;
index 70203300ffe5e2dec2ba2f90d4dac29d9d94f6c9..42f7b53492e45204d4725f0982c43f5432c8cd26 100644 (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);