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:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)' \
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
@@ -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, §ions, &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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user