bootctl: add "link" command

This commit is contained in:
Lennart Poettering
2026-05-01 07:10:31 +02:00
parent e68bf712be
commit e48d8a1ea0
11 changed files with 1515 additions and 5 deletions
+101
View File
@@ -117,6 +117,36 @@
<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>
@@ -577,6 +607,77 @@
<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>
+4 -3
View File
@@ -43,7 +43,8 @@ _bootctl() {
--efi-boot-option-description --efi-boot-option-description-with-device
--secure-boot-auto-enroll --private-key
--private-key-source --certificate --certificate-source
--oldest'
--oldest --keep-free --entry-title --entry-version --entry-commit
-X --extra --tries-left'
)
if __contains_word "$prev" ${OPTS[ARG]}; then
@@ -62,7 +63,7 @@ _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" )
;;
@@ -89,7 +90,7 @@ _bootctl() {
[STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup'
[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
+5
View File
@@ -36,6 +36,10 @@ _bootctl_unlink() {
_bootctl_comp_ids
}
_bootctl_link() {
_files
}
_bootctl_kernel-identify() {
_files
}
@@ -70,6 +74,7 @@ _bootctl_reboot-to-firmware() {
"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"
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "shared-forward.h"
int verb_link(int argc, char *argv[], uintptr_t data, void *userdata);
int vl_method_link(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+122
View File
@@ -9,6 +9,7 @@
#include "bootctl.h"
#include "bootctl-cleanup.h"
#include "bootctl-install.h"
#include "bootctl-link.h"
#include "bootctl-random-seed.h"
#include "bootctl-reboot-to-firmware.h"
#include "bootctl-set-efivar.h"
@@ -16,6 +17,7 @@
#include "bootctl-uki.h"
#include "bootctl-unlink.h"
#include "bootctl-util.h"
#include "bootspec-util.h"
#include "build.h"
#include "crypto-util.h"
#include "devnum-util.h"
@@ -82,6 +84,12 @@ 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);
@@ -95,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",
@@ -366,6 +377,9 @@ VERB_SCOPE_NOARG(, verb_list, "list",
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");
@@ -641,8 +655,115 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
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)
@@ -711,6 +832,7 @@ static int vl_server(void) {
"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.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");
+10
View File
@@ -52,6 +52,12 @@ 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 */
@@ -68,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
View File
@@ -3,6 +3,7 @@
bootctl_sources = files(
'bootctl.c',
'bootctl-install.c',
'bootctl-link.c',
'bootctl-random-seed.c',
'bootctl-reboot-to-firmware.c',
'bootctl-set-efivar.c',
+11
View File
@@ -433,3 +433,14 @@ bool efi_loader_entry_name_valid(const char *s) {
return in_charset(s, ALPHANUMERICAL "+-_.@");
}
bool efi_loader_entry_title_valid(const char *s) {
return string_is_safe(s, /* flags= */ 0);
}
bool efi_loader_entry_resource_filename_valid(const char *s) {
/* Validates file names so that they are safe for their inclusion in boot loader type #1
* entries. i.e. may not contain CCs, and should be ASCII */
return string_is_safe(s, STRING_ASCII|STRING_FILENAME);
}
+2
View File
@@ -23,3 +23,5 @@ int efi_loader_update_entry_one_shot_cache(char **cache, struct stat *cache_stat
int efi_get_variable_id128(const char *variable, sd_id128_t *ret);
bool efi_loader_entry_name_valid(const char *s);
bool efi_loader_entry_title_valid(const char *s);
bool efi_loader_entry_resource_filename_valid(const char *s);
+45 -2
View File
@@ -27,7 +27,7 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
SD_VARLINK_FIELD_COMMENT("The location of the local addon."),
SD_VARLINK_DEFINE_FIELD(localAddon, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The command line options by the addon."),
SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, 0));
SD_VARLINK_DEFINE_FIELD(options, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
BootEntry,
@@ -147,6 +147,40 @@ static SD_VARLINK_DEFINE_METHOD(
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);
@@ -162,6 +196,9 @@ static SD_VARLINK_DEFINE_ERROR(
static SD_VARLINK_DEFINE_ERROR(
BootEntryTokenUnavailable);
static SD_VARLINK_DEFINE_ERROR(
InvalidKernelImage);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_BootControl,
"io.systemd.BootControl",
@@ -172,6 +209,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"),
@@ -188,6 +227,8 @@ SD_VARLINK_DEFINE_INTERFACE(
&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."),
@@ -197,4 +238,6 @@ SD_VARLINK_DEFINE_INTERFACE(
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);
&vl_error_BootEntryTokenUnavailable,
SD_VARLINK_SYMBOL_COMMENT("The specified kernel image is not valid."),
&vl_error_InvalidKernelImage);