bootctl: introduce "link" as alternative to kernel-install (#41543)

This adds "bootctl link" as alternative to kernel-install for installing
a UKI together with sidecards (confext, sysext, creds) as type 1
entries.

It has a much tigher focus than kernel-install, and doesn't do plugins
or anything.

It acts as "inverse" of the pre-existing "bootctl unlink".
This commit is contained in:
Lennart Poettering
2026-05-01 10:38:22 +02:00
committed by GitHub
22 changed files with 3188 additions and 330 deletions
+6 -4
View File
@@ -143,6 +143,12 @@ SPDX-License-Identifier: LGPL-2.1-or-later
volume is auto-destroyed. Would be exposed via a new flag on the Acquire
call, similar to the locking logic above.
- clean up credential naming a bit: let's say encrypted creds always should
carry .cred suffix, and unencrypted should not.
- clean up naming of sidecar files in sd-stub: let's put global ones strictly
into /loader/extras/
- a small tool that can do basic btrfs raid policy mgmt. i.e. gets started as
part of the initial transaction for some btrfs raid fs, waits for some time,
then puts message on screen (plymouth, console) that some devices apparently
@@ -2163,10 +2169,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- run0: maybe enable utmp for run0 sessions, so that they are easily visible.
- sd-boot/sd-stub: install a uefi "handle" to a sidecar dir of bls type #1
entries with an "uki" or "uki-url" stanza, and make sd-stub look for
that. That way we can parameterize type #1 entries nicely.
- **sd-boot:**
- do something useful if we find exactly zero entries (ignoring items
such as reboot/poweroff/factory reset). Show a help text or so.
+122 -4
View File
@@ -102,15 +102,51 @@
</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>
<varlistentry>
<term><option>link</option> <replaceable>KERNEL</replaceable></term>
<listitem><para>Creates one or more Type #1 boot loader entries pointing to the specified UKI. Takes
the path to a Unified Kernel Image (UKI) as argument. The UKI is copied into the ESP (or XBOOTLDR
partition if present) below the configured entry token directory, and one or more <ulink
url="https://uapi-group.org/specifications/specs/boot_loader_specification">Boot Loader
Specification</ulink> Type #1 entries are generated referring to it (one per UKI profile, if multiple
profiles are embedded).</para>
<para>The title, version, commit number and initial try counter of the generated entries
may be overridden with <option>--entry-title=</option>, <option>--entry-version=</option>,
<option>--entry-commit=</option> and <option>--tries-left=</option>. Additional sidecar resources
(system extension images, configuration extension images, credential files) to pass to the UKI at
boot may be specified with <option>--extra=</option>.</para>
<para>If the ESP/XBOOTLDR do not have enough free space for the new boot loader entry and its
referenced resources the oldest existing boot loader entry matching the selected entry token is
removed (along with any resources referenced by it that are no longer referenced by any other
entry). This step is repeated until the new boot loader entry fits. For robustness reasons the
currently booted boot loader entry is never removed, nor is the last existing boot loader
entry.</para>
<para>By default, the operation refuses to proceed if the resulting ESP/XBOOTLDR free space would
drop below a safety threshold after automatic removal of older entries completes; use
<option>--keep-free=</option> to adjust.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>cleanup</option></term>
@@ -560,6 +596,88 @@
<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>--keep-free=<replaceable>BYTES</replaceable></option></term>
<listitem><para>When used with <command>link</command>, controls the minimum amount of free space
(in bytes) that must remain on the target partition (ESP or XBOOTLDR) after the new entry has been
materialized. The operation fails if installing the entry would drop the free space below this
threshold. Accepts the usual size suffixes (K, M, G, …). If empty, the built-in default is
restored. If set to zero no minimum amount of free space is kept.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--entry-title=<replaceable>TITLE</replaceable></option></term>
<listitem><para>When used with <command>link</command>, specifies the title of the generated boot
loader entry (the <literal>title</literal> field of the Type #1 entry). If not specified, a title is
derived from the UKI's embedded metadata.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--entry-version=<replaceable>VERSION</replaceable></option></term>
<listitem><para>When used with <command>link</command>, specifies the version string of the
generated boot loader entry (the <literal>version</literal> field of the Type #1 entry). If not
specified, the version is derived from the UKI's embedded metadata. Used by the boot loader to sort
and select entries.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--entry-commit=<replaceable>NR</replaceable></option></term>
<listitem><para>When used with <command>link</command>, specifies the commit number for the generated
boot loader entry.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--extra=<replaceable>PATH</replaceable></option></term>
<term><option>-X <replaceable>PATH</replaceable></option></term>
<listitem><para>When used with <command>link</command>, registers an additional sidecar resource
file that shall be passed to the UKI at boot. This may be a system extension image
(<filename>*.sysext.raw</filename>), configuration extension image
(<filename>*.confext.raw</filename>), or credential file
(<filename>*.cred</filename>). The file is copied into the ESP/XBOOTLDR alongside the UKI and the
boot loader will load and pass it to the kernel via initrd. This option may be used multiple times
to register more than one extra resource. If passed an empty argument, all previously specified
extras are cleared.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--tries-left=<replaceable>NR</replaceable></option></term>
<listitem><para>When used with <command>link</command>, initializes the boot counting
<literal>tries-left</literal> counter for the generated entry. If set, the resulting boot entry file
is named according to the boot counting scheme described in the <ulink
url="https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT">Automatic Boot Assessment</ulink> documentation,
so that the boot loader decreases the counter on each attempted boot and eventually marks the entry
as bad. If not specified, boot counting is not enabled for the generated entry.</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>
+9 -6
View File
@@ -40,8 +40,11 @@ _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 --keep-free --entry-title --entry-version --entry-commit
-X --extra --tries-left'
)
if __contains_word "$prev" ${OPTS[ARG]}; then
@@ -60,14 +63,14 @@ _bootctl() {
--entry-token)
comps="machine-id os-id os-image-id auto literal:"
;;
--image|--root)
--image|--root|-X|--extra)
compopt -o nospace
comps=$( compgen -A file -- "$cur" )
;;
--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,9 +88,9 @@ _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'
[FILE]='kernel-identify kernel-inspect link'
)
for ((i=0; i < COMP_CWORD; i++)); do
+26
View File
@@ -24,10 +24,30 @@ _bootctl_set-oneshot() {
_bootctl_comp_ids
}
_bootctl_set-sysfail() {
_bootctl_comp_ids
}
_bootctl_set-preferred() {
_bootctl_comp_ids
}
_bootctl_unlink() {
_bootctl_comp_ids
}
_bootctl_link() {
_files
}
_bootctl_kernel-identify() {
_files
}
_bootctl_kernel-inspect() {
_files
}
_bootctl_reboot-to-firmware() {
local -a _completions
_completions=( yes no )
@@ -49,10 +69,15 @@ _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"
"link:Add 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 +104,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;
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "shared-forward.h"
int verb_link(int argc, char *argv[], uintptr_t data, void *userdata);
int vl_method_link(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+525 -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,556 @@ 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;
}
FOREACH_ARRAY(x, e->local_extras.items, e->local_extras.n_items) {
r = ref_file(known_files, x->location, 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);
FOREACH_ARRAY(x, e->local_extras.items, e->local_extras.n_items)
(void) unref_unlink_file(&known_files, root, root_fd, x->location, 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);
+135 -2
View File
@@ -9,6 +9,7 @@
#include "bootctl.h"
#include "bootctl-cleanup.h"
#include "bootctl-install.h"
#include "bootctl-link.h"
#include "bootctl-random-seed.h"
#include "bootctl-reboot-to-firmware.h"
#include "bootctl-set-efivar.h"
@@ -16,6 +17,7 @@
#include "bootctl-uki.h"
#include "bootctl-unlink.h"
#include "bootctl-util.h"
#include "bootspec-util.h"
#include "build.h"
#include "crypto-util.h"
#include "devnum-util.h"
@@ -34,6 +36,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 +83,13 @@ 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;
uint64_t arg_keep_free = KEEP_FREE_BYTES_DEFAULT;
char *arg_entry_title = NULL;
char *arg_entry_version = NULL;
uint64_t arg_entry_commit = 0;
char **arg_extras = NULL;
unsigned arg_tries_left = UINT_MAX;
STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
@@ -93,6 +103,9 @@ STATIC_DESTRUCTOR_REGISTER(arg_certificate, freep);
STATIC_DESTRUCTOR_REGISTER(arg_certificate_source, freep);
STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_private_key_source, freep);
STATIC_DESTRUCTOR_REGISTER(arg_entry_title, freep);
STATIC_DESTRUCTOR_REGISTER(arg_entry_version, freep);
STATIC_DESTRUCTOR_REGISTER(arg_extras, strv_freep);
static const char* const install_source_table[_INSTALL_SOURCE_MAX] = {
[INSTALL_SOURCE_IMAGE] = "image",
@@ -361,9 +374,12 @@ 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(, verb_link, "link", "KERNEL", 2, 2, 0,
"Create boot loader entry for specified kernel");
VERB_SCOPE_NOARG(, verb_cleanup, "cleanup",
"Remove files in ESP not referenced in any boot entry");
@@ -631,8 +647,123 @@ 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;
OPTION_LONG("keep-free", "BYTES",
"How much space to keep free on ESP/XBOOTLDR"):
if (isempty(opts.arg))
arg_keep_free = KEEP_FREE_BYTES_DEFAULT;
else {
r = parse_size(opts.arg, 1024, &arg_keep_free);
if (r < 0)
return log_error_errno(r, "Failed to parse --keep-free=: %s", opts.arg);
}
break;
OPTION_LONG("entry-title", "TITLE",
"Selects the entry title for the new boot menu entry"):
if (isempty(opts.arg)) {
arg_entry_title = mfree(arg_entry_title);
break;
}
if (!efi_loader_entry_title_valid(opts.arg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid boot menu entry title: %s", opts.arg);
r = free_and_strdup_warn(&arg_entry_title, opts.arg);
if (r < 0)
return r;
break;
OPTION_LONG("entry-version", "VERSION",
"Selects the entry version for the new boot menu entry"):
if (isempty(opts.arg)) {
arg_entry_version = mfree(arg_entry_version);
break;
}
if (!version_is_valid_versionspec(opts.arg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid boot menu entry version: %s", opts.arg);
r = free_and_strdup_warn(&arg_entry_version, opts.arg);
if (r < 0)
return r;
break;
OPTION_LONG("entry-commit", "NR",
"Selects the entry commit version for the new boot menu entry"): {
if (isempty(opts.arg)) {
arg_entry_commit = 0;
break;
}
uint64_t n;
r = safe_atou64(opts.arg, &n);
if (r < 0)
return log_error_errno(r, "Failed to parse --entry-commit= parameter: %s", opts.arg);
if (!entry_commit_valid(n))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid entry commit number.");
arg_entry_commit = n;
break;
}
OPTION('X', "extra", "PATH",
"Pass extra resource (confext, sysext, credential) to the invoked UKI of the boot menu entry"): {
if (isempty(opts.arg)) {
arg_extras = strv_free(arg_extras);
break;
}
_cleanup_free_ char *x = NULL;
r = parse_path_argument(opts.arg, /* suppress_root= */ false, &x);
if (r < 0)
return r;
_cleanup_free_ char *fn = NULL;
r = path_extract_filename(x, &fn);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from '%s': %m", x);
if (!efi_loader_entry_resource_filename_valid(fn))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra filename '%s' is not suitable for reference in a boot menu entry.", fn);
r = strv_consume(&arg_extras, TAKE_PTR(x));
if (r < 0)
return log_oom();
strv_uniq(arg_extras);
break;
}
OPTION_LONG("tries-left", "NR",
"Set boot menu entries tries-left counter to the specified value"): {
if (isempty(opts.arg)) {
arg_tries_left = UINT_MAX;
break;
}
unsigned u;
r = safe_atou(opts.arg, &u);
if (r < 0)
return log_error_errno(r, "Failed to parse tries left counter: %s", opts.arg);
if (u >= UINT_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Tries left counter too large, refusing: %u", u);
arg_tries_left = u;
break;
}}
char **args = option_parser_get_args(&opts);
if (!!arg_print_esp_path + !!arg_print_dollar_boot_path + (arg_print_root_device > 0) + arg_print_loader_path + arg_print_stub_path + arg_print_efi_architecture > 1)
@@ -700,7 +831,9 @@ 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.Link", vl_method_link,
"io.systemd.BootControl.Unlink", vl_method_unlink);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
+11
View File
@@ -51,6 +51,13 @@ 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;
extern uint64_t arg_keep_free;
extern char *arg_entry_title;
extern char *arg_entry_version;
extern uint64_t arg_entry_commit;
extern char **arg_extras;
extern unsigned arg_tries_left;
static inline const char* arg_dollar_boot_path(void) {
/* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */
@@ -67,3 +74,7 @@ int acquire_xbootldr(int unprivileged_mode, int *ret_fd, sd_id128_t *ret_uuid, d
* string, but we limit the length to something reasonable to prevent from the firmware
* having to deal with a potentially too long string. */
#define EFI_BOOT_OPTION_DESCRIPTION_MAX ((size_t) 255)
/* Before we "materialize" a new entry, let's ensure we have this much space free still on the partition, by
* default */
#define KEEP_FREE_BYTES_DEFAULT (5U * U64_MB)
+214
View File
@@ -1,11 +1,18 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "boot-entry.h"
#include "bootspec-util.h"
#include "devnum-util.h"
#include "efi-loader.h"
#include "errno-util.h"
#include "log.h"
#include "parse-util.h"
#include "path-util.h"
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
#include "utf8.h"
int boot_config_load_and_select(
BootConfig *config,
@@ -39,3 +46,210 @@ int boot_config_load_and_select(
return boot_config_select_special_entries(config, /* skip_efivars= */ !!root);
}
int boot_entry_make_commit_filename(
const char *entry_token,
uint64_t entry_commit,
const char *version,
unsigned profile_nr,
unsigned tries_left,
char **ret) {
assert(entry_token);
assert(ret);
/* Generate a new entry filename from the entry token, the commit number, and (optionally) the
* image/OS version, (if non-zero) the profile number, and (unless UINT_MAX) the number of tries
* left. */
if (!boot_entry_token_valid(entry_token))
return -EINVAL;
if (!entry_commit_valid(entry_commit))
return -EINVAL;
_cleanup_free_ char *filename = asprintf_safe("%s-commit_%" PRIu64, entry_token, entry_commit);
if (!filename)
return -ENOMEM;
if (version && !strextend(&filename, ".", version))
return -ENOMEM;
if (profile_nr > 0 && strextendf(&filename, "@%u", profile_nr) < 0)
return -ENOMEM;
if (tries_left != UINT_MAX && strextendf(&filename, "+%u", tries_left) < 0)
return -ENOMEM;
if (!strextend(&filename, ".conf"))
return -ENOMEM;
if (!filename_is_valid(filename) || string_has_cc(filename, /* ok= */ NULL) || !utf8_is_valid(filename))
return -EINVAL;
*ret = TAKE_PTR(filename);
return 0;
}
int boot_entry_parse_commit_filename(
const char *filename,
char **ret_entry_token,
uint64_t *ret_entry_commit) {
int r;
assert(filename);
if (!filename_is_valid(filename))
return -EINVAL;
_cleanup_free_ char *stripped = NULL;
r = boot_filename_extract_tries(filename, &stripped, /* ret_tries_left= */ NULL, /* ret_tries_done= */ NULL);
if (r < 0)
return r;
const char *a = strrstr_no_case(stripped, "-commit_");
if (!a)
return -EBADMSG;
const char *c = endswith_no_case(stripped, ".conf");
if (!c)
return -EBADMSG;
assert(a < c);
_cleanup_free_ char *entry_token = strndup(stripped, a - stripped);
if (!entry_token)
return -ENOMEM;
if (!boot_entry_token_valid(entry_token))
return -EBADMSG;
const char *b = a + STRLEN("-commit_");
size_t n = strspn(b, DIGITS);
if (n <= 0 || !IN_SET(b[n], '+', '.', '@'))
return -EBADMSG;
_cleanup_free_ char *entry_commit_string = strndup(b, n);
if (!entry_commit_string)
return -ENOMEM;
uint64_t entry_commit;
r = safe_atou64_full(entry_commit_string, 10, &entry_commit);
if (r < 0)
return r;
if (!entry_commit_valid(entry_commit))
return -EBADMSG;
if (ret_entry_token)
*ret_entry_token = TAKE_PTR(entry_token);
if (ret_entry_commit)
*ret_entry_commit = entry_commit;
return 0;
}
int boot_entry_parse_commit(
BootEntry *entry,
char **ret_entry_token,
uint64_t *ret_entry_commit) {
int r;
assert(entry);
if (entry->type != BOOT_ENTRY_TYPE1)
return -EADDRNOTAVAIL;
_cleanup_free_ char *fn = NULL;
r = path_extract_filename(entry->path, &fn);
if (r < 0)
return r;
return boot_entry_parse_commit_filename(fn, ret_entry_token, ret_entry_commit);
}
int boot_config_find_oldest_commit(
BootConfig *config,
const char *entry_token,
char ***ret_ids) {
int r;
assert(config);
assert(entry_token);
assert(ret_ids);
uint64_t commit_oldest = UINT64_MAX, commit_2nd_oldest = UINT64_MAX, commit_blocked = UINT64_MAX;
/* First, determine which commit is the oldest (that isn't the current one), and hence the candidate
* to be removed */
FOREACH_ARRAY(b, config->entries, config->n_entries) {
_cleanup_free_ char *et = NULL;
uint64_t ec;
r = boot_entry_parse_commit(b, &et, &ec);
if (r == -EADDRNOTAVAIL)
continue;
if (r < 0) {
log_debug_errno(r, "Failed to parse entry filename of '%s', ignoring: %m", strna(b->id));
continue;
}
if (!streq(et, entry_token)) /* Not ours? */
continue;
if (ec < commit_oldest) {
commit_2nd_oldest = commit_oldest;
commit_oldest = ec;
} else if (ec > commit_oldest && ec < commit_2nd_oldest)
commit_2nd_oldest = ec;
if (boot_config_selected_entry(config) == b) {
assert(commit_blocked == UINT64_MAX);
commit_blocked = ec;
}
}
uint64_t commit_picked;
if (commit_oldest == UINT64_MAX)
return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "No matching entry found while determining oldest entry.");
if (commit_oldest != commit_blocked)
commit_picked = commit_oldest;
else {
if (commit_2nd_oldest == UINT64_MAX)
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Only matching entry found while determining oldest entry is current one, skipping it.");
assert(commit_2nd_oldest != commit_blocked);
commit_picked = commit_2nd_oldest;
}
log_debug("Determined commit %" PRIu64 " to be oldest.", commit_picked);
/* Second loop: actually remove all entries matching this commit (which can be multiple, since UKIs
* have profiles) */
_cleanup_(strv_freep) char **l = NULL;
FOREACH_ARRAY(b, config->entries, config->n_entries) {
_cleanup_free_ char *et = NULL;
uint64_t ec;
r = boot_entry_parse_commit(b, &et, &ec);
if (r == -EADDRNOTAVAIL)
continue;
if (r < 0) {
log_debug_errno(r, "Failed to parse entry filename of '%s', ignoring: %m", strna(b->id));
continue;
}
if (!streq(et, entry_token)) /* Not ours? */
continue;
if (ec != commit_picked)
continue;
r = strv_extend(&l, b->id);
if (r < 0)
return r;
}
/* The list cannot be empty, the first loop above and the 2nd loop must have found the same matching
* entries, and if the first loop didn't find any we'd not come this far. */
assert(!strv_isempty(l));
*ret_ids = TAKE_PTR(l);
return 0;
}
+12
View File
@@ -4,3 +4,15 @@
#include "bootspec.h"
int boot_config_load_and_select(BootConfig *config, const char *root, const char *esp_path, dev_t esp_devid, const char *xbootldr_path, dev_t xbootldr_devid);
static inline bool entry_commit_valid(uint64_t commit) {
return commit > 0 && commit < UINT64_MAX;
}
int boot_entry_make_commit_filename(const char *entry_token, uint64_t entry_commit, const char *version, unsigned profile_nr, unsigned tries_left, char **ret);
int boot_entry_parse_commit_filename(const char *filename, char **ret_entry_token, uint64_t *ret_entry_commit);
int boot_entry_parse_commit(BootEntry *entry, char **ret_entry_token, uint64_t *ret_entry_commit);
int boot_config_find_oldest_commit(BootConfig *config, const char *entry_token, char ***ret_ids);
+7
View File
@@ -3,6 +3,7 @@
bootctl_sources = files(
'bootctl.c',
'bootctl-install.c',
'bootctl-link.c',
'bootctl-random-seed.c',
'bootctl-reboot-to-firmware.c',
'bootctl-set-efivar.c',
@@ -25,4 +26,10 @@ executables += [
'link_with' : boot_link_with,
'dependencies' : [libopenssl_cflags],
},
test_template + {
'sources' : files(
'test-bootspec-util.c',
'bootspec-util.c',
),
},
]
+51
View File
@@ -0,0 +1,51 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "bootspec-util.h"
#include "tests.h"
static void test_one(
const char *entry_token,
uint64_t entry_commit,
const char *version,
unsigned profile_nr,
unsigned tries_left,
const char *expected) {
_cleanup_free_ char *fn = NULL;
ASSERT_OK(boot_entry_make_commit_filename(entry_token, entry_commit, version, profile_nr, tries_left, &fn));
ASSERT_STREQ(fn, expected);
_cleanup_free_ char *token = NULL;
uint64_t commit = 0;
ASSERT_OK(boot_entry_parse_commit_filename(fn, &token, &commit));
ASSERT_STREQ(token, entry_token);
ASSERT_EQ(commit, entry_commit);
}
TEST(boot_entry_commit_filename) {
test_one("foo", 1, NULL, 0, UINT_MAX, "foo-commit_1.conf");
test_one("foo", 42, "1.0", 0, UINT_MAX, "foo-commit_42.1.0.conf");
test_one("foo", 42, "1.0", 3, UINT_MAX, "foo-commit_42.1.0@3.conf");
test_one("foo", 42, "1.0", 3, 5, "foo-commit_42.1.0@3+5.conf");
test_one("foo", 42, NULL, 3, UINT_MAX, "foo-commit_42@3.conf");
test_one("foo", 42, NULL, 3, 7, "foo-commit_42@3+7.conf");
test_one("foo", 42, NULL, 0, 9, "foo-commit_42+9.conf");
test_one("my-token", 123456, "v2", 0, UINT_MAX, "my-token-commit_123456.v2.conf");
/* Invalid inputs for make */
_cleanup_free_ char *fn = NULL;
ASSERT_ERROR(boot_entry_make_commit_filename("foo/bar", 1, NULL, 0, UINT_MAX, &fn), EINVAL);
ASSERT_ERROR(boot_entry_make_commit_filename("foo", 0, NULL, 0, UINT_MAX, &fn), EINVAL);
ASSERT_ERROR(boot_entry_make_commit_filename("foo", UINT64_MAX, NULL, 0, UINT_MAX, &fn), EINVAL);
/* Invalid inputs for parse */
_cleanup_free_ char *token = NULL;
uint64_t commit = 0;
ASSERT_ERROR(boot_entry_parse_commit_filename("foo.conf", &token, &commit), EBADMSG);
ASSERT_ERROR(boot_entry_parse_commit_filename("foo-commit_.conf", &token, &commit), EBADMSG);
ASSERT_ERROR(boot_entry_parse_commit_filename("foo-commit_abc.conf", &token, &commit), EBADMSG);
ASSERT_ERROR(boot_entry_parse_commit_filename("foo-commit_0.conf", &token, &commit), EBADMSG);
}
DEFINE_TEST_MAIN(LOG_INFO);
+458 -186
View File
@@ -14,10 +14,12 @@
#include "efi-loader.h"
#include "efivars.h"
#include "env-file.h"
#include "errno-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "fileio.h"
#include "find-esp.h"
#include "json-util.h"
#include "log.h"
#include "parse-util.h"
#include "path-util.h"
@@ -31,6 +33,7 @@
#include "string-util.h"
#include "strv.h"
#include "uki.h"
#include "utf8.h"
static const char* const boot_entry_type_description_table[_BOOT_ENTRY_TYPE_MAX] = {
[BOOT_ENTRY_TYPE1] = "Boot Loader Specification Type #1 (.conf)",
@@ -64,15 +67,65 @@ static const char* const boot_entry_source_table[_BOOT_ENTRY_SOURCE_MAX] = {
DEFINE_STRING_TABLE_LOOKUP_TO_STRING(boot_entry_source, BootEntrySource);
static void boot_entry_addons_done(BootEntryAddons *addons) {
assert(addons);
static BootEntryExtraType boot_entry_extra_type_from_filename(const char *path) {
if (!path)
return _BOOT_ENTRY_EXTRA_TYPE_INVALID;
FOREACH_ARRAY(addon, addons->items, addons->n_items) {
free(addon->cmdline);
free(addon->location);
if (endswith_no_case(path, ".addon.efi"))
return BOOT_ENTRY_ADDON;
if (endswith_no_case(path, ".confext.raw"))
return BOOT_ENTRY_CONFEXT;
if (endswith_no_case(path, ".sysext.raw"))
return BOOT_ENTRY_SYSEXT;
if (endswith_no_case(path, ".cred"))
return BOOT_ENTRY_CREDENTIAL;
return _BOOT_ENTRY_EXTRA_TYPE_INVALID;
}
static void boot_entry_extras_done(BootEntryExtras *extras) {
assert(extras);
FOREACH_ARRAY(extra, extras->items, extras->n_items) {
free(extra->location);
free(extra->cmdline);
}
addons->items = mfree(addons->items);
addons->n_items = 0;
extras->items = mfree(extras->items);
extras->n_items = 0;
}
static int boot_entry_extras_add(
BootEntryExtras *extras,
BootEntryExtraType type,
const char *path,
const char *cmdline) {
assert(extras);
assert(type >= 0);
assert(type < _BOOT_ENTRY_EXTRA_TYPE_MAX);
assert(path);
_cleanup_free_ char *p = strdup(path);
if (!p)
return -ENOMEM;
_cleanup_free_ char *c = NULL;
if (cmdline) {
c = strdup(cmdline);
if (!c)
return -ENOMEM;
}
if (!GREEDY_REALLOC(extras->items, extras->n_items + 1))
return -ENOMEM;
extras->items[extras->n_items++] = (BootEntryExtra) {
.type = type,
.location = TAKE_PTR(p),
.cmdline = TAKE_PTR(c),
};
return 0;
}
static void boot_entry_free(BootEntry *entry) {
@@ -90,7 +143,7 @@ static void boot_entry_free(BootEntry *entry) {
free(entry->machine_id);
free(entry->architecture);
strv_free(entry->options);
boot_entry_addons_done(&entry->local_addons);
boot_entry_extras_done(&entry->local_extras);
free(entry->kernel);
free(entry->efi);
free(entry->uki);
@@ -212,6 +265,50 @@ static int parse_path_many(
return strv_extend_strv_consume(s, TAKE_PTR(f), /* filter_duplicates= */ false);
}
static int parse_extra(
const char *fname,
unsigned line,
const char *field,
BootEntryExtras *extras,
const char *p) {
int r;
assert(extras);
_cleanup_strv_free_ char **l = strv_split(p, NULL);
if (!l)
return -ENOMEM;
STRV_FOREACH(i, l) {
_cleanup_free_ char *c = NULL;
r = mangle_path(fname, line, field, *i, &c);
if (r < 0)
return r;
if (r == 0)
continue;
BootEntryExtraType type = boot_entry_extra_type_from_filename(c);
if (type < 0) {
log_debug_errno(type, "Failed to determine boot entry extra type of '%s', skipping: %m", c);
continue;
}
/* Let's filter out EFI addons for now. We have no protocol for passing them from sd-boot to
* sd-stub, hence supporting them would require major plumbing first. */
if (type == BOOT_ENTRY_ADDON) {
log_debug("EFI addons are currently not supported for Type #1 entries, skipping '%s'.", c);
continue;
}
r = boot_entry_extras_add(extras, type, c, /* cmdline= */ NULL);
if (r < 0)
return r;
}
return 0;
}
static int parse_tries(const char *fname, const char **p, unsigned *ret) {
_cleanup_free_ char *d = NULL;
unsigned tries;
@@ -420,6 +517,8 @@ static int boot_entry_load_type1(
r = parse_path_one(tmp.path, line, field, &tmp.device_tree, p);
else if (streq(field, "devicetree-overlay"))
r = parse_path_many(tmp.path, line, field, &tmp.device_tree_overlay, p);
else if (streq(field, "extra"))
r = parse_extra(tmp.path, line, field, &tmp.local_extras, p);
else {
log_syntax(NULL, LOG_WARNING, tmp.path, line, 0, "Unknown line '%s', ignoring.", field);
continue;
@@ -457,7 +556,7 @@ int boot_config_load_type1(
return r;
config->n_entries++;
entry->global_addons = &config->global_addons[source];
entry->global_extras = &config->global_extras[source];
return 0;
}
@@ -478,8 +577,8 @@ void boot_config_free(BootConfig *config) {
boot_entry_free(i);
free(config->entries);
FOREACH_ELEMENT(i, config->global_addons)
boot_entry_addons_done(i);
FOREACH_ELEMENT(i, config->global_extras)
boot_entry_extras_done(i);
set_free(config->inodes_seen);
}
@@ -711,6 +810,110 @@ static int boot_entries_find_type1(
return 0;
}
static void mangle_osrelease_string(char **s, const char *field) {
assert(s);
assert(field);
if (!isempty(*s) && !string_has_cc(*s, /* ok= */ NULL) && utf8_is_valid(*s))
return;
if (*s) {
log_debug("OS release field '%s' is not clean, suppressing.", field);
*s = mfree(*s);
}
}
int bootspec_extract_osrelease(
const char *text,
char **ret_good_name,
char **ret_good_version,
char **ret_good_sort_key,
char **ret_os_id,
char **ret_os_version_id,
char **ret_image_id,
char **ret_image_version) {
int r;
assert(text);
_cleanup_free_ char *os_pretty_name = NULL, *image_id = NULL, *os_name = NULL, *os_id = NULL,
*image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
r = parse_env_data(text, /* size= */ SIZE_MAX,
"os-release",
"PRETTY_NAME", &os_pretty_name,
"IMAGE_ID", &image_id,
"NAME", &os_name,
"ID", &os_id,
"IMAGE_VERSION", &image_version,
"VERSION", &os_version,
"VERSION_ID", &os_version_id,
"BUILD_ID", &os_build_id);
if (r < 0)
return r;
mangle_osrelease_string(&os_pretty_name, "PRETTY_NAME");
mangle_osrelease_string(&image_id, "IMAGE_ID");
mangle_osrelease_string(&os_name, "NAME");
mangle_osrelease_string(&os_id, "ID");
mangle_osrelease_string(&image_version, "IMAGE_VERSION");
mangle_osrelease_string(&os_version, "VERSION");
mangle_osrelease_string(&os_version_id, "VERSION_ID");
mangle_osrelease_string(&os_build_id, "BUILD_ID");
const char *good_name, *good_version, *good_sort_key;
if (!bootspec_pick_name_version_sort_key(
os_pretty_name,
image_id,
os_name,
os_id,
image_version,
os_version,
os_version_id,
os_build_id,
&good_name,
&good_version,
&good_sort_key))
return -EBADMSG;
_cleanup_free_ char *copy_good_name = NULL, *copy_good_version = NULL, *copy_good_sort_key = NULL;
if (ret_good_name) {
copy_good_name = strdup(good_name);
if (!copy_good_name)
return -ENOMEM;
}
if (ret_good_version && good_version) {
copy_good_version = strdup(good_version);
if (!copy_good_version)
return -ENOMEM;
}
if (ret_good_sort_key && good_sort_key) {
copy_good_sort_key = strdup(good_sort_key);
if (!copy_good_sort_key)
return -ENOMEM;
}
if (ret_good_name)
*ret_good_name = TAKE_PTR(copy_good_name);
if (ret_good_version)
*ret_good_version = TAKE_PTR(copy_good_version);
if (ret_good_sort_key)
*ret_good_sort_key = TAKE_PTR(copy_good_sort_key);
if (ret_os_id)
*ret_os_id = TAKE_PTR(os_id);
if (ret_os_version_id)
*ret_os_version_id = TAKE_PTR(os_version_id);
if (ret_image_id)
*ret_image_id = TAKE_PTR(image_id);
if (ret_image_version)
*ret_image_version = TAKE_PTR(image_version);
return 0;
}
static int boot_entry_load_unified(
const char *root,
const BootEntrySource source,
@@ -721,9 +924,6 @@ static int boot_entry_load_unified(
const char *cmdline_text,
BootEntry *ret) {
_cleanup_free_ char *fname = NULL, *os_pretty_name = NULL, *os_image_id = NULL, *os_name = NULL, *os_id = NULL,
*os_image_version = NULL, *os_version = NULL, *os_version_id = NULL, *os_build_id = NULL;
const char *k, *good_name, *good_version, *good_sort_key;
int r;
assert(root);
@@ -731,36 +931,22 @@ static int boot_entry_load_unified(
assert(osrelease_text);
assert(ret);
k = path_startswith(path, root);
const char *k = path_startswith(path, root);
if (!k)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path is not below root: %s", path);
r = parse_env_data(osrelease_text, /* size= */ SIZE_MAX,
".osrel",
"PRETTY_NAME", &os_pretty_name,
"IMAGE_ID", &os_image_id,
"NAME", &os_name,
"ID", &os_id,
"IMAGE_VERSION", &os_image_version,
"VERSION", &os_version,
"VERSION_ID", &os_version_id,
"BUILD_ID", &os_build_id);
_cleanup_free_ char *good_name = NULL, *good_version = NULL, *good_sort_key = NULL, *os_id = NULL, *os_version_id = NULL;
r = bootspec_extract_osrelease(
osrelease_text,
&good_name,
&good_version,
&good_sort_key,
&os_id,
&os_version_id,
/* ret_image_id= */ NULL,
/* ret_image_version= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to parse os-release data from unified kernel image %s: %m", path);
if (!bootspec_pick_name_version_sort_key(
os_pretty_name,
os_image_id,
os_name,
os_id,
os_image_version,
os_version,
os_version_id,
os_build_id,
&good_name,
&good_version,
&good_sort_key))
return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing fields in os-release data from unified kernel image %s, refusing.", path);
return log_error_errno(r, "Failed to extract name/version/sort-key from os-release data from unified kernel image %s, refusing: %m", path);
_cleanup_free_ char *profile_id = NULL, *profile_title = NULL;
if (profile_text) {
@@ -773,6 +959,7 @@ static int boot_entry_load_unified(
return log_error_errno(r, "Failed to parse profile data from unified kernel image '%s': %m", path);
}
_cleanup_free_ char *fname = NULL;
r = path_extract_filename(path, &fname);
if (r < 0)
return log_error_errno(r, "Failed to extract file name from '%s': %m", path);
@@ -963,7 +1150,7 @@ static int trim_cmdline(char **cmdline) {
* the ones we do care about and we are willing to load into memory have this size limit.) */
#define PE_SECTION_SIZE_MAX (4U*1024U*1024U)
static int pe_find_uki_sections(
int pe_find_uki_sections(
int fd,
const char *path,
unsigned profile,
@@ -979,9 +1166,6 @@ static int pe_find_uki_sections(
assert(fd >= 0);
assert(path);
assert(profile != UINT_MAX);
assert(ret_osrelease);
assert(ret_profile);
assert(ret_cmdline);
r = pe_load_headers_and_sections(fd, path, &sections, &pe_header);
if (r < 0)
@@ -1038,13 +1222,22 @@ static int pe_find_uki_sections(
if (trim_cmdline(&cmdline_text) < 0)
return log_oom();
*ret_osrelease = TAKE_PTR(osrelease_text);
*ret_profile = TAKE_PTR(profile_text);
*ret_cmdline = TAKE_PTR(cmdline_text);
if (ret_osrelease)
*ret_osrelease = TAKE_PTR(osrelease_text);
if (ret_profile)
*ret_profile = TAKE_PTR(profile_text);
if (ret_cmdline)
*ret_cmdline = TAKE_PTR(cmdline_text);
return 1;
nothing:
*ret_osrelease = *ret_profile = *ret_cmdline = NULL;
if (ret_osrelease)
*ret_osrelease = NULL;
if (ret_profile)
*ret_profile = NULL;
if (ret_cmdline)
*ret_cmdline = NULL;
return 0;
}
@@ -1093,126 +1286,140 @@ nothing:
return 0;
}
static int insert_boot_entry_addon(
BootEntryAddons *addons,
char *location,
char *cmdline) {
assert(addons);
if (!GREEDY_REALLOC(addons->items, addons->n_items + 1))
return log_oom();
addons->items[addons->n_items++] = (BootEntryAddon) {
.location = location,
.cmdline = cmdline,
};
return 0;
}
static int boot_entries_find_unified_addons(
static int boot_entries_find_unified_extras(
BootConfig *config,
int d_fd,
const char *addon_dir,
const char *root,
BootEntryAddons *ret_addons) {
const char *extra_dir,
BootEntryExtraType only_type,
const char *where,
bool suppress_seen,
BootEntryExtras *extras) {
_cleanup_closedir_ DIR *d = NULL;
_cleanup_free_ char *full = NULL;
_cleanup_(boot_entry_addons_done) BootEntryAddons addons = {};
int r;
assert(ret_addons);
assert(config);
assert(extras);
r = chase_and_opendirat(d_fd, d_fd, addon_dir, /* chase_flags= */ 0, &full, &d);
_cleanup_closedir_ DIR *d = NULL;
r = chase_and_opendirat(
/* root_fd= */ d_fd,
/* dir_fd= */ d_fd,
extra_dir,
/* chase_flags= */ 0,
/* ret_path= */ NULL,
&d);
if (r == -ENOENT)
return 0;
if (r < 0)
return log_error_errno(r, "Failed to open '%s/%s': %m", root, skip_leading_slash(addon_dir));
FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read %s: %m", full)) {
_cleanup_free_ char *j = NULL, *cmdline = NULL, *location = NULL;
_cleanup_close_ int fd = -EBADF;
return log_error_errno(r, "Failed to open '%s/%s': %m", where, skip_leading_slash(extra_dir));
FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read '%s': %m", extra_dir)) {
if (!dirent_is_file(de))
continue;
if (!endswith_no_case(de->d_name, ".addon.efi"))
BootEntryExtraType type = boot_entry_extra_type_from_filename(de->d_name);
if (type < 0) {
log_debug_errno(type, "Unrecognized extra file '%s', skipping.", de->d_name);
continue;
fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOFOLLOW|O_NOCTTY);
if (fd < 0) {
log_warning_errno(errno, "Failed to open %s/%s, ignoring: %m", full, de->d_name);
}
if (only_type >= 0 && type != only_type) {
log_debug("Extra file '%s' type not permitted in '%s', skipping.", de->d_name, extra_dir);
continue;
}
r = config_check_inode_relevant_and_unseen(config, fd, de->d_name);
if (r < 0)
return r;
if (r == 0) /* inode already seen or otherwise not relevant */
continue;
j = path_join(full, de->d_name);
if (!j)
return log_oom();
if (pe_find_addon_sections(fd, j, &cmdline) <= 0)
continue;
location = strdup(j);
_cleanup_free_ char *location = path_join(extra_dir, de->d_name);
if (!location)
return log_oom();
r = insert_boot_entry_addon(&addons, location, cmdline);
_cleanup_close_ int pin_fd = openat(dirfd(d), de->d_name, O_PATH|O_CLOEXEC|O_NOFOLLOW);
if (pin_fd < 0) {
log_debug_errno(errno, "Failed to pin '%s', ignoring: %m", location);
continue;
}
r = fd_verify_regular(pin_fd);
if (r < 0) {
log_debug_errno(r, "Unrecognized inode type of '%s', skipping.", location);
continue;
}
if (suppress_seen) {
r = config_check_inode_relevant_and_unseen(config, pin_fd, location);
if (r < 0)
return r;
if (r == 0) /* inode already seen or otherwise not relevant */
continue;
}
_cleanup_free_ char *cmdline = NULL;
if (type == BOOT_ENTRY_ADDON) {
_cleanup_close_ int fd = fd_reopen(pin_fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
if (fd < 0) {
log_debug_errno(fd, "Failed to open '%s', ignoring: %m", location);
continue;
}
/* Try to extract the command line, but let's handle any failures gracefully, but
* still mention the extra file exists. */
(void) pe_find_addon_sections(fd, location, &cmdline);
}
r = boot_entry_extras_add(extras, type, location, cmdline);
if (r < 0)
return r;
TAKE_PTR(location);
TAKE_PTR(cmdline);
}
*ret_addons = TAKE_STRUCT(addons);
return 0;
}
static int boot_entries_find_unified_global_addons(
static int boot_entries_find_unified_global_extras(
BootConfig *config,
const char *root,
const char *d_name,
BootEntryAddons *ret_addons) {
const char *where,
const char *extra_dir,
BootEntryExtraType only_type,
BootEntryExtras *extras) {
int r;
_cleanup_closedir_ DIR *d = NULL;
assert(extras);
assert(ret_addons);
r = chase_and_opendir(root, NULL, CHASE_PROHIBIT_SYMLINKS, NULL, &d);
if (r == -ENOENT)
_cleanup_close_ int where_fd = RET_NERRNO(open(where, O_DIRECTORY|O_CLOEXEC));
if (where_fd == -ENOENT)
return 0;
if (r < 0)
return log_error_errno(r, "Failed to open '%s/%s': %m", root, skip_leading_slash(d_name));
if (where_fd < 0)
return log_error_errno(where_fd, "Failed to open '%s': %m", where);
return boot_entries_find_unified_addons(config, dirfd(d), d_name, root, ret_addons);
return boot_entries_find_unified_extras(
config,
where_fd,
extra_dir,
only_type,
where,
/* suppress_seen= */ true,
extras);
}
static int boot_entries_find_unified_local_addons(
static int boot_entries_find_unified_local_extras(
BootConfig *config,
int d_fd,
const char *d_name,
const char *root,
const char *uki,
const char *where,
BootEntry *ret) {
_cleanup_free_ char *addon_dir = NULL;
_cleanup_free_ char *extra_dir = NULL;
assert(ret);
addon_dir = strjoin(d_name, ".extra.d");
if (!addon_dir)
extra_dir = strjoin(uki, ".extra.d");
if (!extra_dir)
return log_oom();
return boot_entries_find_unified_addons(config, d_fd, addon_dir, root, &ret->local_addons);
return boot_entries_find_unified_extras(
config,
d_fd,
extra_dir,
/* only_type= */ _BOOT_ENTRY_EXTRA_TYPE_INVALID,
where,
/* suppress_seen= */ false,
&ret->local_extras);
}
static int boot_entries_find_unified(
@@ -1274,11 +1481,11 @@ static int boot_entries_find_unified(
if (boot_entry_load_unified(root, source, j, p, osrelease, profile, cmdline, entry) < 0)
continue;
/* look for .efi.extra.d */
(void) boot_entries_find_unified_local_addons(config, dirfd(d), de->d_name, full, entry);
/* Look for .efi.extra.d/ */
(void) boot_entries_find_unified_local_extras(config, dirfd(d), de->d_name, full, entry);
/* Set up the backpointer, so that we can find the global addons */
entry->global_addons = &config->global_addons[source];
/* Set up the backpointer, so that we can find the global extras */
entry->global_extras = &config->global_extras[source];
config->n_entries++;
}
@@ -1519,6 +1726,52 @@ int boot_config_finalize(BootConfig *config) {
return 0;
}
static int boot_entries_load(
BootConfig *config,
BootEntrySource source,
const char *where) { /* Mount point of ESP/XBOOTLDR */
int r;
assert(config);
assert(source >= 0);
assert(source < _BOOT_ENTRY_SOURCE_MAX);
if (!where)
return 0;
r = boot_entries_find_type1(config, where, source, "/loader/entries");
if (r < 0)
return r;
r = boot_entries_find_unified(config, where, source, "/EFI/Linux/");
if (r < 0)
return r;
static const struct {
BootEntryExtraType extra_type;
const char *directory;
} table[] = {
{ BOOT_ENTRY_ADDON, "/loader/addons/" },
{ BOOT_ENTRY_CONFEXT, "/loader/extensions/" },
{ BOOT_ENTRY_SYSEXT, "/loader/extensions/" },
{ BOOT_ENTRY_CREDENTIAL, "/loader/credentials/" },
};
FOREACH_ELEMENT(i, table) {
r = boot_entries_find_unified_global_extras(
config,
where,
i->directory,
i->extra_type,
&config->global_extras[source]);
if (r < 0)
return r;
}
return 0;
}
int boot_config_load(
BootConfig *config,
const char *esp_path,
@@ -1533,31 +1786,13 @@ int boot_config_load(
if (r < 0)
return r;
r = boot_entries_find_type1(config, esp_path, BOOT_ENTRY_ESP, "/loader/entries");
if (r < 0)
return r;
r = boot_entries_find_unified(config, esp_path, BOOT_ENTRY_ESP, "/EFI/Linux/");
if (r < 0)
return r;
r = boot_entries_find_unified_global_addons(config, esp_path, "/loader/addons/",
&config->global_addons[BOOT_ENTRY_ESP]);
r = boot_entries_load(config, BOOT_ENTRY_ESP, esp_path);
if (r < 0)
return r;
}
if (xbootldr_path) {
r = boot_entries_find_type1(config, xbootldr_path, BOOT_ENTRY_XBOOTLDR, "/loader/entries");
if (r < 0)
return r;
r = boot_entries_find_unified(config, xbootldr_path, BOOT_ENTRY_XBOOTLDR, "/EFI/Linux/");
if (r < 0)
return r;
r = boot_entries_find_unified_global_addons(config, xbootldr_path, "/loader/addons/",
&config->global_addons[BOOT_ENTRY_XBOOTLDR]);
r = boot_entries_load(config, BOOT_ENTRY_XBOOTLDR, xbootldr_path);
if (r < 0)
return r;
}
@@ -1629,7 +1864,7 @@ int boot_config_augment_from_loader(
char **found_by_loader,
bool auto_only) {
static const BootEntryAddons no_addons = (BootEntryAddons) {};
static const BootEntryExtras no_extras = (BootEntryExtras) {};
static const char *const title_table[] = {
/* Pretty names for a few well-known automatically discovered entries. */
"auto-osx", "macOS",
@@ -1688,7 +1923,7 @@ int boot_config_augment_from_loader(
.tries_left = UINT_MAX,
.tries_done = UINT_MAX,
.profile = UINT_MAX,
.global_addons = &no_addons,
.global_extras = &no_extras,
};
}
@@ -1711,10 +1946,10 @@ static void boot_entry_file_list(
const char *field,
const char *root,
const char *p,
int *ret_status) {
int *pstatus) {
assert(p);
assert(ret_status);
assert(pstatus);
int status = chase_and_access(p, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, NULL);
@@ -1729,16 +1964,23 @@ static void boot_entry_file_list(
} else
printf("%s\n", p);
if (*ret_status == 0 && status < 0)
*ret_status = status;
if (*pstatus == 0 && status < 0)
*pstatus = status;
}
static void print_addon(
BootEntryAddon *addon,
const char *addon_str) {
static void print_extra(
const BootEntry *e,
const BootEntryExtra *extra,
const char *field,
int *status) {
printf(" %s: %s\n", addon_str, addon->location);
printf(" options: %s%s\n", glyph(GLYPH_TREE_RIGHT), addon->cmdline);
assert(e);
assert(extra);
boot_entry_file_list(field, e->root, extra->location, status);
if (extra->cmdline)
printf(" options: %s%s\n", glyph(GLYPH_TREE_RIGHT), extra->cmdline);
}
static int indent_embedded_newlines(char *cmdline, char **ret_cmdline) {
@@ -1756,12 +1998,10 @@ static int indent_embedded_newlines(char *cmdline, char **ret_cmdline) {
return -ENOMEM;
*ret_cmdline = TAKE_PTR(t);
return 0;
}
static int print_cmdline(const BootEntry *e) {
static int print_cmdline(const BootEntry *e, int *status) {
_cleanup_free_ char *options = NULL, *combined_cmdline = NULL, *t2 = NULL;
assert(e);
@@ -1782,17 +2022,20 @@ static int print_cmdline(const BootEntry *e) {
return log_oom();
}
FOREACH_ARRAY(addon, e->global_addons->items, e->global_addons->n_items) {
print_addon(addon, "global-addon");
if (!strextend(&t2, " ", addon->cmdline))
return log_oom();
FOREACH_ARRAY(extra, e->global_extras->items, e->global_extras->n_items) {
print_extra(e, extra, "extra", status);
if (extra->cmdline)
if (!strextend(&t2, " ", extra->cmdline))
return log_oom();
}
FOREACH_ARRAY(addon, e->local_addons.items, e->local_addons.n_items) {
/* Add space at the beginning of addon_str to align it correctly */
print_addon(addon, " local-addon");
if (!strextend(&t2, " ", addon->cmdline))
return log_oom();
FOREACH_ARRAY(extra, e->local_extras.items, e->local_extras.n_items) {
print_extra(e, extra, "extra", status);
if (extra->cmdline)
if (!strextend(&t2, " ", extra->cmdline))
return log_oom();
}
/* Don't print the combined cmdline if it's same as options. */
@@ -1809,19 +2052,19 @@ static int print_cmdline(const BootEntry *e) {
}
static int json_addon(
BootEntryAddon *addon,
const char *addon_str,
const BootEntryExtra *extra,
const char *extra_str,
sd_json_variant **array) {
int r;
assert(addon);
assert(addon_str);
assert(extra);
assert(extra_str);
r = sd_json_variant_append_arraybo(
array,
SD_JSON_BUILD_PAIR_STRING(addon_str, addon->location),
SD_JSON_BUILD_PAIR_STRING("options", addon->cmdline));
SD_JSON_BUILD_PAIR_STRING(extra_str, extra->location),
JSON_BUILD_PAIR_STRING_NON_EMPTY("options", extra->cmdline));
if (r < 0)
return log_oom();
@@ -1845,20 +2088,31 @@ static int json_cmdline(
return log_oom();
}
FOREACH_ARRAY(addon, e->global_addons->items, e->global_addons->n_items) {
r = json_addon(addon, "globalAddon", &addons_array);
/* NB: these JSON fields are kinda obsolete, we want the more generic 'extra' ones to be used. */
FOREACH_ARRAY(extra, e->global_extras->items, e->global_extras->n_items) {
if (extra->type != BOOT_ENTRY_ADDON)
continue;
r = json_addon(extra, "globalAddon", &addons_array);
if (r < 0)
return r;
if (!strextend(&combined_cmdline, " ", addon->cmdline))
return log_oom();
if (extra->cmdline)
if (!strextend(&combined_cmdline, " ", extra->cmdline))
return log_oom();
}
FOREACH_ARRAY(addon, e->local_addons.items, e->local_addons.n_items) {
r = json_addon(addon, "localAddon", &addons_array);
FOREACH_ARRAY(extra, e->local_extras.items, e->local_extras.n_items) {
if (extra->type != BOOT_ENTRY_ADDON)
continue;
r = json_addon(extra, "localAddon", &addons_array);
if (r < 0)
return r;
if (!strextend(&combined_cmdline, " ", addon->cmdline))
return log_oom();
if (extra->cmdline)
if (!strextend(&combined_cmdline, " ", extra->cmdline))
return log_oom();
}
r = sd_json_variant_merge_objectbo(
@@ -1969,7 +2223,7 @@ int show_boot_entry(
*s,
&status);
r = print_cmdline(e);
r = print_cmdline(e, &status);
if (r < 0)
return r;
@@ -2049,6 +2303,24 @@ int boot_entry_to_json(const BootConfig *c, size_t i, sd_json_variant **ret) {
if (r < 0)
return log_oom();
_cleanup_(sd_json_variant_unrefp) sd_json_variant *jextras = NULL;
FOREACH_ARRAY(extra, e->global_extras->items, e->global_extras->n_items) {
r = sd_json_variant_append_arrayb(&jextras, SD_JSON_BUILD_STRING(extra->location));
if (r < 0)
return log_oom();
}
FOREACH_ARRAY(extra, e->local_extras.items, e->local_extras.n_items) {
r = sd_json_variant_append_arrayb(&jextras, SD_JSON_BUILD_STRING(extra->location));
if (r < 0)
return log_oom();
}
r = sd_json_variant_merge_objectbo(
&v,
SD_JSON_BUILD_PAIR_CONDITION(!!jextras, "extras", SD_JSON_BUILD_VARIANT(jextras)));
if (r < 0)
return log_oom();
*ret = TAKE_PTR(v);
return 1;
}
+34 -10
View File
@@ -20,15 +20,25 @@ typedef enum BootEntrySource {
_BOOT_ENTRY_SOURCE_INVALID = -EINVAL,
} BootEntrySource;
typedef struct BootEntryAddon {
char *location;
char *cmdline;
} BootEntryAddon;
typedef enum BootEntryExtraType {
BOOT_ENTRY_ADDON,
BOOT_ENTRY_CONFEXT,
BOOT_ENTRY_SYSEXT,
BOOT_ENTRY_CREDENTIAL,
_BOOT_ENTRY_EXTRA_TYPE_MAX,
_BOOT_ENTRY_EXTRA_TYPE_INVALID = -EINVAL,
} BootEntryExtraType;
typedef struct BootEntryAddons {
BootEntryAddon *items;
typedef struct BootEntryExtra {
BootEntryExtraType type;
char *location;
char *cmdline; /* only for BOOT_ENTRY_ADDON */
} BootEntryExtra;
typedef struct BootEntryExtras {
BootEntryExtra *items;
size_t n_items;
} BootEntryAddons;
} BootEntryExtras;
typedef struct BootEntry {
BootEntryType type;
@@ -46,8 +56,8 @@ typedef struct BootEntry {
char *machine_id;
char *architecture;
char **options;
BootEntryAddons local_addons;
const BootEntryAddons *global_addons; /* Backpointer into the BootConfig; we don't own this here */
BootEntryExtras local_extras;
const BootEntryExtras *global_extras; /* Backpointer into the BootConfig; we don't own this here */
char *kernel; /* linux is #defined to 1, yikes! */
char *efi;
char *uki;
@@ -84,7 +94,7 @@ typedef struct BootConfig {
BootEntry *entries;
size_t n_entries;
BootEntryAddons global_addons[_BOOT_ENTRY_SOURCE_MAX];
BootEntryExtras global_extras[_BOOT_ENTRY_SOURCE_MAX];
ssize_t default_entry;
ssize_t selected_entry;
@@ -116,6 +126,16 @@ static inline const BootEntry* boot_config_default_entry(const BootConfig *confi
return config->entries + config->default_entry;
}
static inline const BootEntry* boot_config_selected_entry(const BootConfig *config) {
assert(config);
if (config->selected_entry < 0)
return NULL;
assert((size_t) config->selected_entry < config->n_entries);
return config->entries + config->selected_entry;
}
void boot_config_free(BootConfig *config);
int boot_loader_read_conf(BootConfig *config, FILE *file, const char *path);
@@ -152,3 +172,7 @@ int show_boot_entries(
int boot_filename_extract_tries(const char *fname, char **ret_stripped, unsigned *ret_tries_left, unsigned *ret_tries_done);
int boot_entry_to_json(const BootConfig *c, size_t i, sd_json_variant **ret);
int pe_find_uki_sections(int fd, const char *path, unsigned profile, char **ret_osrelease, char **ret_profile, char **ret_cmdline);
int bootspec_extract_osrelease(const char *text, char **ret_good_name, char **ret_good_version, char **ret_good_sort_key, char **ret_os_id, char **ret_os_version_id, char **ret_image_id, char **ret_image_version);
+11
View File
@@ -433,3 +433,14 @@ bool efi_loader_entry_name_valid(const char *s) {
return in_charset(s, ALPHANUMERICAL "+-_.@");
}
bool efi_loader_entry_title_valid(const char *s) {
return string_is_safe(s, /* flags= */ 0);
}
bool efi_loader_entry_resource_filename_valid(const char *s) {
/* Validates file names so that they are safe for their inclusion in boot loader type #1
* entries. i.e. may not contain CCs, and should be ASCII */
return string_is_safe(s, STRING_ASCII|STRING_FILENAME);
}
+2
View File
@@ -23,3 +23,5 @@ int efi_loader_update_entry_one_shot_cache(char **cache, struct stat *cache_stat
int efi_get_variable_id128(const char *variable, sd_id128_t *ret);
bool efi_loader_entry_name_valid(const char *s);
bool efi_loader_entry_title_valid(const char *s);
bool efi_loader_entry_resource_filename_valid(const char *s);
+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;
+68 -3
View File
@@ -27,7 +27,7 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_FIELD_COMMENT("The location of the local addon."),
SD_VARLINK_DEFINE_FIELD(localAddon, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The command line options by the addon."),
SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, 0));
SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
BootEntry,
@@ -80,6 +80,8 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_DEFINE_FIELD(isSelected, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Addon images of the entry."),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(addons, BootEntryAddon, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
SD_VARLINK_FIELD_COMMENT("Extra files associated with the entry."),
SD_VARLINK_DEFINE_FIELD(extras, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
SD_VARLINK_FIELD_COMMENT("Command line options of the entry."),
SD_VARLINK_DEFINE_FIELD(cmdline, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
@@ -134,6 +136,53 @@ 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_STRUCT_TYPE(
BootEntryExtraFile,
SD_VARLINK_FIELD_COMMENT("The name of the extra file"),
SD_VARLINK_DEFINE_FIELD(filename, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors, pointing to a file descriptor referencing the extra file."),
SD_VARLINK_DEFINE_FIELD(fileDescriptor, SD_VARLINK_INT, 0));
static SD_VARLINK_DEFINE_METHOD(
Link,
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 entry title for the newly created boot menu entry"),
SD_VARLINK_DEFINE_INPUT(entryTitle, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The entry version for the newly created boot menu entry"),
SD_VARLINK_DEFINE_INPUT(entryVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The commit number for the newly created boot menu entry"),
SD_VARLINK_DEFINE_INPUT(entryCommit, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Target filename for the kernel image (UKI) in the $BOOT partition"),
SD_VARLINK_DEFINE_INPUT(kernelFilename, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors passed along with this message, pointing to file descriptor to the kernel image to copy"),
SD_VARLINK_DEFINE_INPUT(kernelFileDescriptor, SD_VARLINK_INT, 0),
SD_VARLINK_FIELD_COMMENT("An array of 'extra' files for this entry, i.e. credentials, confexts, sysexts, addons."),
SD_VARLINK_DEFINE_INPUT_BY_TYPE(extraFiles, BootEntryExtraFile, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
SD_VARLINK_FIELD_COMMENT("What to set the triesLeft counter of the boot menu entry to initially."),
SD_VARLINK_DEFINE_INPUT(triesLeft, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("How much space to always keep free on ESP/XBOOTLDR. Defaults to 1 MiB"),
SD_VARLINK_DEFINE_INPUT(keepFree, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The IDs of the created boot loader entries."),
SD_VARLINK_DEFINE_OUTPUT(ids, SD_VARLINK_STRING, SD_VARLINK_ARRAY));
static SD_VARLINK_DEFINE_ERROR(
RebootToFirmwareNotSupported);
@@ -143,9 +192,15 @@ static SD_VARLINK_DEFINE_ERROR(
static SD_VARLINK_DEFINE_ERROR(
NoESPFound);
static SD_VARLINK_DEFINE_ERROR(
NoDollarBootFound);
static SD_VARLINK_DEFINE_ERROR(
BootEntryTokenUnavailable);
static SD_VARLINK_DEFINE_ERROR(
InvalidKernelImage);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_BootControl,
"io.systemd.BootControl",
@@ -156,6 +211,8 @@ SD_VARLINK_DEFINE_INTERFACE(
&vl_type_BootEntrySource,
SD_VARLINK_SYMBOL_COMMENT("A structure encapsulating an addon of a boot entry"),
&vl_type_BootEntryAddon,
SD_VARLINK_SYMBOL_COMMENT("An additional file to install"),
&vl_type_BootEntryExtraFile,
SD_VARLINK_SYMBOL_COMMENT("A structure encapsulating a boot entry"),
&vl_type_BootEntry,
SD_VARLINK_SYMBOL_COMMENT("The operation to execute"),
@@ -170,11 +227,19 @@ 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("Install a kernel as boot menu item"),
&vl_method_Link,
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."),
&vl_error_BootEntryTokenUnavailable);
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,
SD_VARLINK_SYMBOL_COMMENT("The specified kernel image is not valid."),
&vl_error_InvalidKernelImage);
+272 -1
View File
@@ -374,7 +374,7 @@ testcase_00_secureboot() {
bootctl status | grep "Secure Boot: enabled" >/dev/null
# Ensure the addon is fully loaded and parsed
bootctl status | grep "global-addon: loader/addons/test.addon.efi" >/dev/null
bootctl status | grep "extra: /boot//loader/addons/test.addon.efi" >/dev/null
bootctl status | grep "cmdline" | grep addonfoobar >/dev/null
grep -q addonfoobar /proc/cmdline
}
@@ -397,4 +397,275 @@ testcase_install_varlink() {
bootctl is-installed
}
cleanup_link() {
if [[ -n "${LINK_WORKDIR:-}" ]]; then
rm -rf "$LINK_WORKDIR"
unset LINK_WORKDIR
fi
restore_esp
}
testcase_bootctl_link() {
if ! command -v ukify >/dev/null; then
echo "ukify not found, skipping."
return 0
fi
backup_esp
LINK_WORKDIR="$(mktemp --directory /tmp/test-bootctl-link.XXXXXXXXXX)"
trap cleanup_link RETURN ERR
# Ensure loader/entries directory is present
bootctl install --make-entry-directory=yes
local ESP
ESP="$(bootctl --print-esp-path)"
# Build a minimal UKI via ukify. The .linux content does not need to be a
# real kernel — bootctl link only requires a valid PE with .osrel (and the
# systemd-stub SBAT marker that pe_is_uki() checks for).
cat >"$LINK_WORKDIR/os-release" <<'EOF'
ID=testos
NAME="Test OS"
PRETTY_NAME="Test OS"
EOF
echo "fake-kernel" >"$LINK_WORKDIR/vmlinuz"
echo "fake-initrd" >"$LINK_WORKDIR/initrd"
echo "fake-sysext-data" >"$LINK_WORKDIR/hello.sysext.raw"
echo "fake-confext-data" >"$LINK_WORKDIR/hello.confext.raw"
echo "fake-credential" >"$LINK_WORKDIR/hello.cred"
ukify build \
--linux "$LINK_WORKDIR/vmlinuz" \
--initrd "$LINK_WORKDIR/initrd" \
--os-release "@$LINK_WORKDIR/os-release" \
--uname "1.2.3-testkernel" \
--cmdline "quiet" \
--output "$LINK_WORKDIR/testuki.efi"
# Pin an explicit entry token so the resulting filenames are deterministic
local TOKEN="systemdtest"
local BOOTCTL=(bootctl "--entry-token=literal:$TOKEN")
# --- Test 1: basic link/unlink ---
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi"
# Exactly one entry file should exist, named "${TOKEN}-commit_1.conf"
local ENTRY="$ESP/loader/entries/${TOKEN}-commit_1.conf"
test -f "$ENTRY"
test -f "$ESP/$TOKEN/testuki.efi"
# Verify the entry file contents
grep "^title " "$ENTRY" >/dev/null
grep "^uki /${TOKEN}/testuki.efi\$" "$ENTRY" >/dev/null
grep "^version 1\$" "$ENTRY" >/dev/null
# Make sure bootctl list sees it
bootctl list --json=short | grep -F "${TOKEN}-commit_1.conf" >/dev/null
# Remove it again using the ID (entry IDs include the .conf suffix)
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
test ! -e "$ENTRY"
test ! -e "$ESP/$TOKEN/testuki.efi"
# --- Test 2: link with --entry-title/--entry-version/--entry-commit/--tries-left ---
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" \
--entry-title="My Funky Entry" \
--entry-version="9.8.7" \
--entry-commit=42 \
--tries-left=3
ENTRY="$ESP/loader/entries/${TOKEN}-commit_42.9.8.7+3.conf"
test -f "$ENTRY"
test -f "$ESP/$TOKEN/testuki.efi"
grep "^title My Funky Entry\$" "$ENTRY" >/dev/null
grep "^version 42.9.8.7\$" "$ENTRY" >/dev/null
grep "^uki /${TOKEN}/testuki.efi\$" "$ENTRY" >/dev/null
# Unlink using the ID (the tries counter "+3" is stripped from the canonical ID)
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_42.9.8.7.conf"
test ! -e "$ENTRY"
test ! -e "$ESP/$TOKEN/testuki.efi"
# --- Test 3: link with extras (-X and --extra=) ---
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" \
--entry-commit=50 \
-X "$LINK_WORKDIR/hello.sysext.raw" \
--extra="$LINK_WORKDIR/hello.confext.raw" \
-X "$LINK_WORKDIR/hello.cred"
ENTRY="$ESP/loader/entries/${TOKEN}-commit_50.conf"
test -f "$ENTRY"
test -f "$ESP/$TOKEN/testuki.efi"
test -f "$ESP/$TOKEN/hello.sysext.raw"
test -f "$ESP/$TOKEN/hello.confext.raw"
test -f "$ESP/$TOKEN/hello.cred"
grep "^extra /${TOKEN}/hello.sysext.raw\$" "$ENTRY" >/dev/null
grep "^extra /${TOKEN}/hello.confext.raw\$" "$ENTRY" >/dev/null
grep "^extra /${TOKEN}/hello.cred\$" "$ENTRY" >/dev/null
# Unlink must also clean up the extra resources
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_50.conf"
test ! -e "$ENTRY"
test ! -e "$ESP/$TOKEN/testuki.efi"
test ! -e "$ESP/$TOKEN/hello.sysext.raw"
test ! -e "$ESP/$TOKEN/hello.confext.raw"
test ! -e "$ESP/$TOKEN/hello.cred"
# --- Test 4: --oldest drops the lowest commit first ---
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=10
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=20
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=30
test -f "$ESP/loader/entries/${TOKEN}-commit_10.conf"
test -f "$ESP/loader/entries/${TOKEN}-commit_20.conf"
test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf"
test -f "$ESP/$TOKEN/testuki.efi"
"${BOOTCTL[@]}" unlink --oldest=yes
test ! -e "$ESP/loader/entries/${TOKEN}-commit_10.conf"
test -f "$ESP/loader/entries/${TOKEN}-commit_20.conf"
test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf"
test -f "$ESP/$TOKEN/testuki.efi"
"${BOOTCTL[@]}" unlink --oldest=yes
test ! -e "$ESP/loader/entries/${TOKEN}-commit_20.conf"
test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf"
test -f "$ESP/$TOKEN/testuki.efi"
# --- Test 5: --dry-run leaves everything in place ---
"${BOOTCTL[@]}" --dry-run unlink "${TOKEN}-commit_30.conf"
test -f "$ESP/loader/entries/${TOKEN}-commit_30.conf"
test -f "$ESP/$TOKEN/testuki.efi"
# Actually remove it now
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_30.conf"
test ! -e "$ESP/loader/entries/${TOKEN}-commit_30.conf"
test ! -e "$ESP/$TOKEN/testuki.efi"
# --- Test 6: invalid combinations are rejected ---
# Neither an ID nor --oldest
(! "${BOOTCTL[@]}" unlink)
# Both an ID and --oldest
(! "${BOOTCTL[@]}" unlink --oldest=yes "${TOKEN}-commit_1.conf")
# --- Test 7: refusing to link when --keep-free cannot be satisfied ---
(! "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=99 --keep-free=1T)
test ! -e "$ESP/loader/entries/${TOKEN}-commit_99.conf"
# --- Test 8: refusing to re-link the same commit number ---
"${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=77
(! "${BOOTCTL[@]}" link "$LINK_WORKDIR/testuki.efi" --entry-commit=77)
"${BOOTCTL[@]}" unlink "${TOKEN}-commit_77.conf"
# --- Test 9: passing a non-UKI is rejected ---
(! "${BOOTCTL[@]}" link "$LINK_WORKDIR/vmlinuz")
# === Varlink coverage ===
#
# Exercise io.systemd.BootControl.Link/Unlink by forking bootctl as a
# varlink server via 'varlinkctl call <binary>'. Note the Varlink schema
# has no way to supply a literal entry token (unlike --entry-token= on
# the command line), so the token is chosen by bootctl from
# machine-id/os-release — we recover it from the returned id.
local BOOTCTL_BIN vreply vid vtoken
BOOTCTL_BIN="$(type -p bootctl)"
# --- Test 10: Link + Unlink via varlink ---
vreply="$(varlinkctl call --json=short \
--push-fd="$LINK_WORKDIR/testuki.efi" \
"$BOOTCTL_BIN" io.systemd.BootControl.Link \
'{"kernelFilename":"vluki.efi","kernelFileDescriptor":0}')"
vid="$(echo "$vreply" | jq -r '.ids[0]')"
test -n "$vid"
test "$vid" != "null"
vtoken="${vid%%-commit_*}"
test -n "$vtoken"
test -f "$ESP/loader/entries/$vid"
test -f "$ESP/$vtoken/vluki.efi"
grep "^uki /$vtoken/vluki.efi\$" "$ESP/loader/entries/$vid" >/dev/null
varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
"{\"id\":\"$vid\"}"
test ! -e "$ESP/loader/entries/$vid"
test ! -e "$ESP/$vtoken/vluki.efi"
# --- Test 11: Link with entryTitle/entryVersion/entryCommit/triesLeft + extraFiles via varlink ---
vreply="$(varlinkctl call --json=short \
--push-fd="$LINK_WORKDIR/testuki.efi" \
--push-fd="$LINK_WORKDIR/hello.sysext.raw" \
--push-fd="$LINK_WORKDIR/hello.cred" \
"$BOOTCTL_BIN" io.systemd.BootControl.Link \
'{"kernelFilename":"vluki2.efi","kernelFileDescriptor":0,"entryTitle":"Varlink Title","entryVersion":"2.3.4","entryCommit":111,"triesLeft":2,"extraFiles":[{"filename":"hello.sysext.raw","fileDescriptor":1},{"filename":"hello.cred","fileDescriptor":2}]}')"
vid="$(echo "$vreply" | jq -r '.ids[0]')"
# The returned id has the tries counter ("+2") stripped
assert_eq "$vid" "$vtoken-commit_111.2.3.4.conf"
# The on-disk entry filename includes the tries counter
local VENTRY="$ESP/loader/entries/$vtoken-commit_111.2.3.4+2.conf"
test -f "$VENTRY"
test -f "$ESP/$vtoken/vluki2.efi"
test -f "$ESP/$vtoken/hello.sysext.raw"
test -f "$ESP/$vtoken/hello.cred"
grep "^title Varlink Title\$" "$VENTRY" >/dev/null
grep "^version 111.2.3.4\$" "$VENTRY" >/dev/null
grep "^extra /$vtoken/hello.sysext.raw\$" "$VENTRY" >/dev/null
grep "^extra /$vtoken/hello.cred\$" "$VENTRY" >/dev/null
varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
"{\"id\":\"$vid\"}"
test ! -e "$VENTRY"
test ! -e "$ESP/$vtoken/vluki2.efi"
test ! -e "$ESP/$vtoken/hello.sysext.raw"
test ! -e "$ESP/$vtoken/hello.cred"
# --- Test 12: Unlink oldest via varlink ---
local c
for c in 210 220 230; do
varlinkctl call --quiet \
--push-fd="$LINK_WORKDIR/testuki.efi" \
"$BOOTCTL_BIN" io.systemd.BootControl.Link \
"{\"kernelFilename\":\"vluki3.efi\",\"kernelFileDescriptor\":0,\"entryCommit\":$c}"
done
test -f "$ESP/loader/entries/$vtoken-commit_210.conf"
test -f "$ESP/loader/entries/$vtoken-commit_220.conf"
test -f "$ESP/loader/entries/$vtoken-commit_230.conf"
varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
'{"oldest":true}'
test ! -e "$ESP/loader/entries/$vtoken-commit_210.conf"
test -f "$ESP/loader/entries/$vtoken-commit_220.conf"
test -f "$ESP/loader/entries/$vtoken-commit_230.conf"
test -f "$ESP/$vtoken/vluki3.efi"
# Clean up remaining entries
varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
"{\"id\":\"$vtoken-commit_220.conf\"}"
varlinkctl call --quiet "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
"{\"id\":\"$vtoken-commit_230.conf\"}"
test ! -e "$ESP/loader/entries/$vtoken-commit_220.conf"
test ! -e "$ESP/loader/entries/$vtoken-commit_230.conf"
test ! -e "$ESP/$vtoken/vluki3.efi"
# --- Test 13: Link with a non-UKI via varlink returns InvalidKernelImage ---
varlinkctl call --quiet \
--push-fd="$LINK_WORKDIR/vmlinuz" \
--graceful=io.systemd.BootControl.InvalidKernelImage \
"$BOOTCTL_BIN" io.systemd.BootControl.Link \
'{"kernelFilename":"notauki.efi","kernelFileDescriptor":0}'
# --- Test 14: Unlink with invalid argument combinations is rejected ---
# Both id and oldest=true
(! varlinkctl call "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
'{"id":"foo.conf","oldest":true}')
# Neither id nor oldest
(! varlinkctl call "$BOOTCTL_BIN" io.systemd.BootControl.Unlink '{}')
# Invalid id characters (e.g. a glob)
(! varlinkctl call "$BOOTCTL_BIN" io.systemd.BootControl.Unlink \
'{"id":"foo*.conf"}')
}
run_testcases