diff --git a/TODO.md b/TODO.md
index a302415b84..d8badaf5ab 100644
--- a/TODO.md
+++ b/TODO.md
@@ -280,12 +280,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- firstboot/sysinstall: add simple interface for prompting users to enable
"features" exposed by of sysupdate.
-- bootctl link + sysupdate integration
- - make sysupdate call out to a special varlink dir on completion
- - bind bootctl link socket in there, which when invoked goes to new dir in
- /var/ where downloaded kernels+confext+sysext are dropped in (place in
- .v/) and then does "bootctl link" on them.
-
- a tool that can prep credentials, put them in the ESP, for provisioning
systems for SBC or UEFI/HTTP boot. Should be doing what sysinstall does with
the credentials, and maybe even *be* sysinstall.
diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md
index b528fce37d..31f180beea 100644
--- a/docs/ENVIRONMENT.md
+++ b/docs/ENVIRONMENT.md
@@ -870,3 +870,10 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as
'freshness' check via `BEST-BEFORE-YYYY-MM-DD` files in `SHA256SUMS` manifest
files is disabled, and updating from outdated manifests will not result in an
error.
+
+* `$SYSTEMD_SYSUPDATE_FORCE_NOTIFY` – takes a boolean. If true the notification
+ callouts in `/run/systemd/sysupdate/notify/` are invoked even when no update
+ was applied (i.e. the system was already up-to-date). In this case the
+ notification carries no list of updated resources. This is useful to
+ unconditionally trigger follow-up work such as relinking a kernel or
+ recomputing a TPM policy.
diff --git a/man/bootctl.xml b/man/bootctl.xml
index 39ddfdd58c..bf5d2bf230 100644
--- a/man/bootctl.xml
+++ b/man/bootctl.xml
@@ -147,6 +147,34 @@
+
+
+
+ Like link, but instead of taking the kernel and sidecar resources
+ as arguments, discovers them automatically. The following directories are searched, in decreasing
+ order of priority: /etc/systemd/uki/, /run/systemd/uki/,
+ /var/lib/systemd/uki/ (where
+ systemd-sysupdate8
+ stages downloaded resources), /usr/local/lib/systemd/uki/ and
+ /usr/lib/systemd/uki/. In each directory the UKI is taken from
+ kernel.efi, or — if absent — the newest version in the versioned directory
+ kernel.efi.v/ (see
+ systemd.v7); the
+ UKI from the highest-priority directory that has one is used. Sidecar resources are picked up from
+ the extras.d/ subdirectory of each, matching the suffixes
+ .sysext.raw, .confext.raw and .cred, each
+ either as a plain file or as a versioned .v/ directory; resources are combined
+ across all directories, with the same resource in a higher-priority directory taking precedence. Any
+ files specified with are linked in addition to the discovered ones.
+
+ This is used by
+ systemd-sysupdate8
+ when it completes an update, so a freshly downloaded kernel and its sidecar resources are linked into
+ place without specifying any paths.
+
+
+
+
@@ -845,6 +873,32 @@ Boot Loader Entries:
+
+ Files
+
+
+
+ /etc/systemd/uki/
+ /run/systemd/uki/
+ /var/lib/systemd/uki/
+ /usr/local/lib/systemd/uki/
+ /usr/lib/systemd/uki/
+
+ The link-auto command discovers the UKI to install from these
+ directories (listed in decreasing order of priority). It uses kernel.efi or the
+ versioned kernel.efi.v/ directory from the highest-priority directory that
+ contains one. Additional sidecar resources (system extension images, configuration extension images,
+ credential files) are picked up from the extras.d/ subdirectory of each, combined
+ across all directories. /var/lib/systemd/uki/ is the directory where
+ systemd-sysupdate8
+ is supposed to place downloaded kernels, system extensions, configuration extensions and
+ credentials.
+
+
+
+
+
+
See Also
diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset
index 85108dcdd5..bce96b7a6d 100644
--- a/presets/90-systemd.preset
+++ b/presets/90-systemd.preset
@@ -40,6 +40,9 @@ enable systemd-report-sign-tsm.socket
enable systemd-report.socket
enable systemd-resolved.service
enable systemd-sysext.service
+enable systemd-sysupdate-notify-bootctl.socket
+enable systemd-sysupdate-notify-pcrlock.socket
+enable systemd-sysupdate-notify-sysext.socket
enable systemd-timesyncd.service
enable systemd-tpm2-clear.service
enable systemd-userdbd.socket
diff --git a/shell-completion/bash/bootctl b/shell-completion/bash/bootctl
index 3b7290c230..16aafbe4ce 100644
--- a/shell-completion/bash/bootctl
+++ b/shell-completion/bash/bootctl
@@ -87,7 +87,7 @@ _bootctl() {
fi
local -A VERBS=(
- [STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup'
+ [STANDALONE]='help status install update remove is-installed random-seed list set-timeout set-timeout-oneshot cleanup link-auto'
[BOOTENTRY]='set-default set-oneshot set-sysfail set-preferred unlink'
[BOOLEAN]='reboot-to-firmware'
[FILE]='kernel-identify kernel-inspect link'
diff --git a/shell-completion/zsh/_bootctl b/shell-completion/zsh/_bootctl
index 423828ba33..46956e55e3 100644
--- a/shell-completion/zsh/_bootctl
+++ b/shell-completion/zsh/_bootctl
@@ -75,6 +75,7 @@ _bootctl_reboot-to-firmware() {
"set-timeout-oneshot:Set the menu timeout for the next boot only"
"unlink:Remove boot loader entry"
"link:Add boot loader entry"
+ "link-auto:Add boot loader entry for kernel and resources staged in /var/lib/systemd/uki/"
"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"
diff --git a/src/basic/constants.h b/src/basic/constants.h
index 3dbffdee26..475d8bf79a 100644
--- a/src/basic/constants.h
+++ b/src/basic/constants.h
@@ -68,6 +68,8 @@
#define VARLINK_PATH_MACHINED_RESOLVE_HOOK "/run/systemd/resolve.hook/io.systemd.Machine"
/* Path where to connect to send varlink prekill events */
#define VARLINK_DIR_OOMD_PREKILL_HOOK "/run/systemd/oomd.prekill.hook/"
+/* Directory whose sockets receive io.systemd.SysUpdate.Notify() after a successful sysupdate run */
+#define VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK "/run/systemd/sysupdate/notify/"
/* Recommended baseline - see README for details */
#define KERNEL_BASELINE_VERSION "5.14"
diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c
index 834d46faca..f8ba48a485 100644
--- a/src/bootctl/bootctl-install.c
+++ b/src/bootctl/bootctl-install.c
@@ -45,6 +45,7 @@
#include "tmpfile-util.h"
#include "umask-util.h"
#include "utf8.h"
+#include "varlink-util.h"
typedef enum InstallOperation {
INSTALL_NEW,
@@ -2121,6 +2122,10 @@ int vl_method_install(
if (r != 0)
return r;
+ r = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
+
if (!IN_SET(p.context.operation, INSTALL_NEW, INSTALL_UPDATE))
return sd_varlink_error_invalid_parameter_name(link, "operation");
diff --git a/src/bootctl/bootctl-link.c b/src/bootctl/bootctl-link.c
index b8372752b5..af0955b64a 100644
--- a/src/bootctl/bootctl-link.c
+++ b/src/bootctl/bootctl-link.c
@@ -32,6 +32,7 @@
#include "parse-argument.h"
#include "path-util.h"
#include "recurse-dir.h"
+#include "set.h"
#include "stat-util.h"
#include "stdio-util.h"
#include "string-util.h"
@@ -39,6 +40,8 @@
#include "tmpfile-util.h"
#include "uki.h"
#include "utf8.h"
+#include "varlink-util.h"
+#include "vpick.h"
/* Keeps track of an "extra" file to associate with the type 1 entries to generate */
typedef struct ExtraFile {
@@ -205,77 +208,14 @@ static void link_context_done(LinkContext *c) {
c->linked_ids = strv_free(c->linked_ids);
}
-static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
+static int link_context_acquire_dollar_boot(LinkContext *b) {
int r;
- assert(ret);
- assert(kernel);
-
- _cleanup_(link_context_done) LinkContext b = LINK_CONTEXT_NULL;
- b.entry_token_type = arg_entry_token_type;
- b.tries_left = arg_tries_left;
- b.entry_commit = arg_entry_commit;
- b.keep_free = arg_keep_free;
-
- if (strdup_to(&b.entry_token, arg_entry_token) < 0 ||
- strdup_to(&b.entry_title, arg_entry_title) < 0 ||
- strdup_to(&b.entry_version, arg_entry_version) < 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 = path_extract_filename(kernel, &b.kernel_filename);
- if (r < 0)
- return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel);
- if (!efi_loader_entry_resource_filename_valid(b.kernel_filename))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Kernel '%s' is not suitable for reference in a boot menu entry.", kernel);
- b.kernel_fd = xopenat_full(AT_FDCWD, kernel, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
- if (b.kernel_fd < 0)
- return log_error_errno(b.kernel_fd, "Failed to open kernel path '%s': %m", kernel);
-
- KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID;
- r = inspect_kernel(b.kernel_fd, /* filename= */ NULL, &kit);
- if (r == -EBADMSG)
- return log_error_errno(r, "Kernel image '%s' is not valid.", kernel);
- if (r < 0)
- return log_error_errno(r, "Failed to determine kernel image type of '%s': %m", kernel);
- if (kit != KERNEL_IMAGE_TYPE_UKI)
- return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Kernel image '%s' is not a UKI.", kernel);
-
- STRV_FOREACH(x, arg_extras) {
- _cleanup_free_ char *fn = NULL;
- r = path_extract_filename(*x, &fn);
- if (r < 0)
- return log_error_errno(r, "Failed to extract filename from path '%s': %m", *x);
- if (r == O_DIRECTORY)
- return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Extra file path '%s' does not refer to regular file.", *x);
-
- _cleanup_close_ int fd = -EBADF;
- fd = xopenat_full(AT_FDCWD, *x, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
- if (fd < 0)
- return log_error_errno(fd, "Failed to open '%s': %m", *x);
-
- if (!GREEDY_REALLOC(b.extra, b.n_extra+1))
- return log_oom();
-
- b.extra[b.n_extra++] = (ExtraFile) {
- .source_fd = TAKE_FD(fd),
- .filename = TAKE_PTR(fn),
- .temp_fd = -EBADF,
- };
- }
+ assert(b);
r = acquire_xbootldr(
/* unprivileged_mode= */ false,
- &b.dollar_boot_fd,
+ &b->dollar_boot_fd,
/* ret_uuid= */ NULL,
/* ret_devid= */ NULL);
if (r < 0)
@@ -288,19 +228,19 @@ static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
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.dollar_boot_path, e);
+ r = strdup_to(&b->dollar_boot_path, e);
} else
- r = strdup_to(&b.dollar_boot_path, arg_xbootldr_path);
+ r = strdup_to(&b->dollar_boot_path, arg_xbootldr_path);
if (r < 0)
return log_oom();
- b.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
+ b->dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
} else {
/* No XBOOTLDR has been found, look for ESP */
r = acquire_esp(/* unprivileged_mode= */ false,
/* graceful= */ false,
- &b.dollar_boot_fd,
+ &b->dollar_boot_fd,
/* ret_part= */ NULL,
/* ret_pstart= */ NULL,
/* ret_psize= */ NULL,
@@ -316,15 +256,126 @@ static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
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.dollar_boot_path, e);
+ r = strdup_to(&b->dollar_boot_path, e);
} else
- r = strdup_to(&b.dollar_boot_path, arg_esp_path);
+ r = strdup_to(&b->dollar_boot_path, arg_esp_path);
if (r < 0)
return log_oom();
- b.dollar_boot_source = BOOT_ENTRY_ESP;
+ b->dollar_boot_source = BOOT_ENTRY_ESP;
}
+ return 0;
+}
+
+/* Fills the bits of a LinkContext that both "bootctl link" and "bootctl link-auto" derive from the global
+ * command line options (everything except the kernel image and extra files themselves). */
+static int link_context_from_cmdline_common(LinkContext *b) {
+ assert(b);
+
+ b->entry_token_type = arg_entry_token_type;
+ b->tries_left = arg_tries_left;
+ b->entry_commit = arg_entry_commit;
+ b->keep_free = arg_keep_free;
+
+ if (strdup_to(&b->entry_token, arg_entry_token) < 0 ||
+ strdup_to(&b->entry_title, arg_entry_title) < 0 ||
+ strdup_to(&b->entry_version, arg_entry_version) < 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;
+
+ return 0;
+}
+
+/* Appends the files passed via --extra= on the command line as extra resources to the link context. */
+static int link_context_add_cmdline_extras(LinkContext *b) {
+ int r;
+
+ assert(b);
+
+ STRV_FOREACH(x, arg_extras) {
+ _cleanup_free_ char *fn = NULL;
+ r = path_extract_filename(*x, &fn);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from path '%s': %m", *x);
+ if (r == O_DIRECTORY)
+ return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Extra file path '%s' does not refer to regular file.", *x);
+
+ _cleanup_close_ int fd = xopenat_full(AT_FDCWD, *x, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to open '%s': %m", *x);
+
+ if (!GREEDY_REALLOC(b->extra, b->n_extra+1))
+ return log_oom();
+
+ b->extra[b->n_extra++] = (ExtraFile) {
+ .source_fd = TAKE_FD(fd),
+ .filename = TAKE_PTR(fn),
+ .temp_fd = -EBADF,
+ };
+ }
+
+ return 0;
+}
+
+static int validate_kernel(int kernel_fd, const char *filename) {
+ int r;
+
+ assert(kernel_fd >= 0);
+
+ KernelImageType kit = _KERNEL_IMAGE_TYPE_INVALID;
+ r = inspect_kernel(kernel_fd, /* filename= */ NULL, &kit);
+ if (r == -EBADMSG)
+ return log_error_errno(r, "UKI '%s' is not valid.", filename);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine kernel image type of '%s': %m", filename);
+ if (kit != KERNEL_IMAGE_TYPE_UKI)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image '%s' is not a UKI.", filename);
+
+ return 0;
+}
+
+static int link_context_from_cmdline(LinkContext *ret, const char *kernel) {
+ int r;
+
+ assert(ret);
+ assert(kernel);
+
+ _cleanup_(link_context_done) LinkContext b = LINK_CONTEXT_NULL;
+ r = link_context_from_cmdline_common(&b);
+ if (r < 0)
+ return r;
+
+ r = path_extract_filename(kernel, &b.kernel_filename);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", kernel);
+ if (!efi_loader_entry_resource_filename_valid(b.kernel_filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Kernel '%s' is not suitable for reference in a boot menu entry.", kernel);
+ b.kernel_fd = xopenat_full(AT_FDCWD, kernel, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
+ if (b.kernel_fd < 0)
+ return log_error_errno(b.kernel_fd, "Failed to open kernel path '%s': %m", kernel);
+
+ r = validate_kernel(b.kernel_fd, kernel);
+ if (r < 0)
+ return r;
+
+ r = link_context_add_cmdline_extras(&b);
+ if (r < 0)
+ return r;
+
+ r = link_context_acquire_dollar_boot(&b);
+ if (r < 0)
+ return r;
+
*ret = TAKE_GENERIC(b, LinkContext, LINK_CONTEXT_NULL);
return 0;
}
@@ -993,6 +1044,311 @@ int verb_link(int argc, char *argv[], uintptr_t data, void *userdata) {
return run_link(&c);
}
+/* Directories (relative to the operative root) below which "bootctl link-auto" looks for a staged UKI and
+ * extra resources, in decreasing priority order. /var/lib/systemd/uki/ is where systemd-sysupdate stages
+ * downloaded resources; the others follow the usual configuration search path precedence, with
+ * /var/lib/ slotted in just below the volatile locations and above the vendor trees. */
+static const char* const uki_auto_dirs[] = {
+ "etc/systemd/uki",
+ "run/systemd/uki",
+ "var/lib/systemd/uki",
+ "usr/local/lib/systemd/uki",
+ "usr/lib/systemd/uki",
+};
+
+static const PickFilter pick_filter_uki = {
+ .type_mask = UINT32_C(1) << DT_REG,
+ .architecture = _ARCHITECTURE_INVALID,
+ .suffix = ".efi",
+};
+
+/* The recognized suffixes of extra resources we pick up alongside the UKI. Each may appear as a plain file
+ * (e.g. foo.sysext.raw) or as a versioned directory (e.g. foo.sysext.raw.v/) resolved via the vpick
+ * mechanism. */
+static const char* const auto_link_extra_suffixes[] = {
+ ".sysext.raw",
+ ".confext.raw",
+ ".cred",
+};
+
+static int link_context_add_extra(LinkContext *c, int dir_fd, const char *path, const char *filename) {
+ assert(c);
+ assert(dir_fd >= 0);
+ assert(path);
+ assert(filename);
+
+ if (!efi_loader_entry_resource_filename_valid(filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Extra file '%s' is not suitable for reference in a boot menu entry, refusing.", filename);
+
+ _cleanup_close_ int fd = xopenat_full(dir_fd, path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY, XO_REGULAR, /* mode= */ MODE_INVALID);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to open extra file '%s': %m", path);
+
+ _cleanup_free_ char *fn = strdup(filename);
+ if (!fn)
+ return log_oom();
+
+ if (!GREEDY_REALLOC(c->extra, c->n_extra + 1))
+ return log_oom();
+
+ c->extra[c->n_extra++] = (ExtraFile) {
+ .source_fd = TAKE_FD(fd),
+ .filename = TAKE_PTR(fn),
+ .temp_fd = -EBADF,
+ };
+
+ return 0;
+}
+
+/* Adds the extra resources found in /extras.d/ to the link context. The 'seen' set tracks the
+ * logical resource names already added by a higher-priority directory, so the same resource appearing in
+ * multiple directories is taken from the highest-priority one only. */
+static int link_context_add_extras(LinkContext *c, int uki_dir_fd, Set **seen) {
+ int r;
+
+ assert(c);
+ assert(uki_dir_fd >= 0);
+ assert(seen);
+
+ _cleanup_close_ int extras_dir_fd = chase_and_openat(
+ c->root_fd,
+ uki_dir_fd,
+ "extras.d",
+ /* chase_flags= */ 0,
+ O_DIRECTORY|O_CLOEXEC,
+ /* ret_path= */ NULL);
+ if (extras_dir_fd == -ENOENT)
+ return 0;
+ if (extras_dir_fd < 0)
+ return log_error_errno(extras_dir_fd, "Failed to open extras.d/ directory: %m");
+
+ _cleanup_free_ DirectoryEntries *des = NULL;
+ r = readdir_all(extras_dir_fd, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_SORT, &des);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate extras.d/ directory: %m");
+
+ FOREACH_ARRAY(i, des->entries, des->n_entries) {
+ struct dirent *de = *i;
+
+ if (!IN_SET(de->d_type, DT_DIR, DT_REG))
+ continue;
+
+ FOREACH_ELEMENT(suffix, auto_link_extra_suffixes) {
+ _cleanup_free_ char *dot_v = strjoin(*suffix, ".v");
+ if (!dot_v)
+ return log_oom();
+
+ bool versioned;
+ _cleanup_free_ char *key = NULL;
+ if (endswith(de->d_name, *suffix)) {
+ if (de->d_type != DT_REG)
+ continue;
+
+ versioned = false;
+ key = strdup(de->d_name);
+
+ } else if (endswith(de->d_name, dot_v)) {
+ if (de->d_type != DT_DIR)
+ continue;
+
+ /* The dedup key is the directory entry name with any trailing ".v" stripped, so that
+ * a plain file and a versioned directory of the same resource collapse, and a
+ * higher-priority directory wins. */
+ versioned = true;
+ key = strndup(de->d_name, strlen(de->d_name) - strlen(".v"));
+ } else
+ continue;
+ if (!key)
+ return log_oom();
+
+ if (set_contains(*seen, key))
+ break;
+
+ if (versioned) {
+ /* A versioned directory: resolve the best version via vpick, relative to the
+ * extras directory fd. We deliberately do not honour boot-counting suffixes
+ * here. */
+ _cleanup_(pick_result_done) PickResult pick = PICK_RESULT_NULL;
+ const PickFilter filter = {
+ .type_mask = UINT32_C(1) << DT_REG,
+ .architecture = _ARCHITECTURE_INVALID,
+ .suffix = *suffix,
+ };
+
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ c->root_fd,
+ /* dir_fd= */ extras_dir_fd,
+ de->d_name,
+ &filter,
+ /* n_filters= */ 1,
+ /* flags= */ 0,
+ &pick);
+ if (r == -ENOENT || r == 0) {
+ log_debug("No matching entry in versioned directory '%s', skipping.", de->d_name);
+ break;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to pick version in '%s': %m", de->d_name);
+
+ _cleanup_free_ char *fn = NULL;
+ r = path_extract_filename(pick.path, &fn);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from '%s': %m", pick.path);
+
+ r = link_context_add_extra(c, extras_dir_fd, pick.path, fn);
+ } else
+ /* A plain file. */
+ r = link_context_add_extra(c, extras_dir_fd, de->d_name, de->d_name);
+ if (r < 0)
+ return r;
+
+ r = set_ensure_consume(seen, &string_hash_ops_free, TAKE_PTR(key));
+ if (r < 0)
+ return log_oom();
+
+ break;
+ }
+ }
+
+ return 0;
+}
+
+/* Looks for a UKI (kernel.efi, or the best version in kernel.efi.v/) in the directory referenced by
+ * uki_dir_fd and, if found, fills c->kernel_fd and c->kernel_filename. Returns 1 if found, 0 if not, and a
+ * negative errno on error. We deliberately do not honour boot-counting suffixes here. */
+static int link_context_find_kernel(LinkContext *c, int uki_dir_fd) {
+ int r;
+
+ assert(c);
+ assert(uki_dir_fd >= 0);
+
+ _cleanup_free_ char *filename = NULL;
+ _cleanup_close_ int kernel_fd = xopenat_full(
+ uki_dir_fd, "kernel.efi",
+ O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_NOFOLLOW,
+ XO_REGULAR,
+ /* mode= */ MODE_INVALID);
+ if (kernel_fd == -ENOENT) {
+ _cleanup_(pick_result_done) PickResult pick = PICK_RESULT_NULL;
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ c->root_fd,
+ /* dir_fd= */ uki_dir_fd,
+ "kernel.efi.v",
+ &pick_filter_uki,
+ /* n_filters= */ 1,
+ /* flags= */ 0,
+ &pick);
+ if (r == -ENOENT || r == 0)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to pick UKI version in 'kernel.efi.v': %m");
+
+ r = path_extract_filename(pick.path, &filename);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract filename from '%s': %m", pick.path);
+
+ kernel_fd = fd_reopen(pick.fd, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (kernel_fd < 0)
+ return log_error_errno(kernel_fd, "Failed to open UKI '%s': %m", pick.path);
+
+ r = fd_verify_regular(kernel_fd);
+ if (r < 0)
+ return log_error_errno(r, "UKI '%s' is not a regular file: %m", pick.path);
+
+ } else if (kernel_fd < 0)
+ return log_error_errno(kernel_fd, "Failed to open 'kernel.efi': %m");
+ else {
+ filename = strdup("kernel.efi");
+ if (!filename)
+ return log_oom();
+ }
+
+ if (!efi_loader_entry_resource_filename_valid(filename))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UKI '%s' is not suitable for reference in a boot menu entry.", filename);
+
+ r = validate_kernel(kernel_fd, filename);
+ if (r < 0)
+ return r;
+
+ c->kernel_fd = TAKE_FD(kernel_fd);
+ c->kernel_filename = TAKE_PTR(filename);
+ return 1;
+}
+
+/* Discovers the staged UKI and extra resources below the uki_auto_dirs[] (relative to the already-pinned
+ * c->root_fd) and fills c->kernel_fd, c->kernel_filename and c->extra[] accordingly. The directories are
+ * searched in priority order: the UKI is taken from the first directory that has one, and extra resources
+ * are combined across all directories with higher-priority directories winning on conflicts. Returns 1 if a
+ * UKI was found, 0 if there is nothing to link, and a negative errno on error. */
+static int link_context_discover_resources(LinkContext *c) {
+ _cleanup_set_free_ Set *seen = NULL;
+ int r;
+
+ assert(c);
+
+ FOREACH_ELEMENT(dir, uki_auto_dirs) {
+ _cleanup_close_ int uki_dir_fd = chase_and_openat(
+ /* root_fd= */ c->root_fd,
+ /* dir_fd= */ c->root_fd,
+ *dir,
+ CHASE_MUST_BE_DIRECTORY,
+ O_RDONLY|O_DIRECTORY|O_CLOEXEC,
+ /* ret_path= */ NULL);
+ if (uki_dir_fd == -ENOENT)
+ continue;
+ if (uki_dir_fd < 0)
+ return log_error_errno(uki_dir_fd, "Failed to open '%s': %m", *dir);
+
+ /* Take the UKI from the first (highest-priority) directory that has one. */
+ if (c->kernel_fd < 0) {
+ r = link_context_find_kernel(c, uki_dir_fd);
+ if (r < 0)
+ return r;
+ }
+
+ r = link_context_add_extras(c, uki_dir_fd, &seen);
+ if (r < 0)
+ return r;
+ }
+
+ if (c->kernel_fd < 0) {
+ log_debug("No UKI found in any of the search directories, nothing to link.");
+ return 0;
+ }
+
+ return 1;
+}
+
+int verb_link_auto(int argc, char *argv[], uintptr_t data, void *userdata) {
+ int r;
+
+ assert(argc == 1);
+
+ _cleanup_(link_context_done) LinkContext c = LINK_CONTEXT_NULL;
+ r = link_context_from_cmdline_common(&c);
+ if (r < 0)
+ return r;
+
+ r = link_context_discover_resources(&c);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ log_info("No UKI staged for linking, nothing to do.");
+ return 0;
+ }
+
+ /* In addition to the auto-discovered resources, also honour any files passed via --extra=. */
+ r = link_context_add_cmdline_extras(&c);
+ if (r < 0)
+ return r;
+
+ r = link_context_acquire_dollar_boot(&c);
+ if (r < 0)
+ return r;
+
+ return run_link(&c);
+}
+
static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_boot_entry_token_type, BootEntryTokenType, boot_entry_token_type_from_string);
typedef struct LinkParameters {
@@ -1089,6 +1445,107 @@ static int dispatch_extras(const char *name, sd_json_variant *v, sd_json_dispatc
return 0;
}
+/* Resolves the root_fd of the LinkContext from the dispatched Link/LinkAuto parameters, fills in the
+ * boot entry token type default, and validates the entry title/version/commit fields. Returns 0 on success,
+ * non-zero (Varlink error or negative errno) otherwise. */
+static int vl_link_prepare(sd_varlink *link, LinkParameters *p) {
+ int r;
+
+ assert(link);
+ assert(p);
+
+ 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;
+
+ if (p->context.entry_title && !efi_loader_entry_title_valid(p->context.entry_title))
+ return sd_varlink_error_invalid_parameter_name(link, "entryTitle");
+
+ if (p->context.entry_version && !version_is_valid_versionspec(p->context.entry_version))
+ return sd_varlink_error_invalid_parameter_name(link, "entryVersion");
+
+ if (p->context.entry_commit != 0 && !entry_commit_valid(p->context.entry_commit))
+ return sd_varlink_error_invalid_parameter_name(link, "entryCommit");
+
+ return 0;
+}
+
+/* Resolves $BOOT, runs the link operation and sends the reply. Expects the kernel and extra files to already
+ * be set up in the context. If with_ids is true the reply carries the list of created entry IDs (as expected
+ * by Link()/LinkAuto()), otherwise an empty reply is sent (as expected by
+ * io.systemd.SysUpdate.Notify.OnCompletedUpdate(), which declares no output fields). */
+static int vl_link_finish(sd_varlink *link, LinkParameters *p, bool with_ids) {
+ int r;
+
+ assert(link);
+ assert(p);
+
+ r = find_xbootldr_and_warn_at(
+ p->context.root_fd,
+ /* path= */ NULL,
+ /* unprivileged_mode= */ false,
+ &p->context.dollar_boot_path,
+ &p->context.dollar_boot_fd);
+ if (r < 0) {
+ if (r != -ENOKEY)
+ return r;
+
+ /* No XBOOTLDR found, let's look for ESP then. */
+
+ r = find_esp_and_warn_at(
+ p->context.root_fd,
+ /* path= */ NULL,
+ /* unprivileged_mode= */ false,
+ &p->context.dollar_boot_path,
+ &p->context.dollar_boot_fd);
+ if (r == -ENOKEY)
+ return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL);
+ if (r < 0)
+ return r;
+
+ p->context.dollar_boot_source = BOOT_ENTRY_ESP;
+ } else
+ p->context.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
+
+ r = run_link(&p->context);
+ if (r == -EUNATCH) /* no boot entry token is set */
+ return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL);
+ if (r < 0)
+ return r;
+
+ if (with_ids)
+ return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", p->context.linked_ids));
+
+ return sd_varlink_reply(link, NULL);
+}
+
int vl_method_link(
sd_varlink *link,
sd_json_variant *parameters,
@@ -1125,45 +1582,13 @@ int vl_method_link(
if (r != 0)
return r;
- 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 = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
- 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;
-
- if (p.context.entry_title && !efi_loader_entry_title_valid(p.context.entry_title))
- return sd_varlink_error_invalid_parameter_name(link, "entryTitle");
-
- if (p.context.entry_version && !version_is_valid_versionspec(p.context.entry_version))
- return sd_varlink_error_invalid_parameter_name(link, "entryVersion");
-
- if (p.context.entry_commit != 0 && !entry_commit_valid(p.context.entry_commit))
- return sd_varlink_error_invalid_parameter_name(link, "entryCommit");
+ r = vl_link_prepare(link, &p);
+ if (r != 0)
+ return r;
p.context.kernel_fd = sd_varlink_peek_dup_fd(link, p.kernel_fd_index);
if (p.context.kernel_fd < 0)
@@ -1186,38 +1611,95 @@ int vl_method_link(
if (kit != KERNEL_IMAGE_TYPE_UKI)
return sd_varlink_error(link, "io.systemd.BootControl.InvalidKernelImage", NULL);
- r = find_xbootldr_and_warn_at(
- p.context.root_fd,
- /* path= */ NULL,
- /* unprivileged_mode= */ false,
- &p.context.dollar_boot_path,
- &p.context.dollar_boot_fd);
- if (r < 0) {
- if (r != -ENOKEY)
- return r;
+ return vl_link_finish(link, &p, /* with_ids= */ true);
+}
- /* No XBOOTLDR found, let's look for ESP then. */
+int vl_method_link_auto(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
- r = find_esp_and_warn_at(
- p.context.root_fd,
- /* path= */ NULL,
- /* unprivileged_mode= */ false,
- &p.context.dollar_boot_path,
- &p.context.dollar_boot_fd);
- if (r == -ENOKEY)
- return sd_varlink_error(link, "io.systemd.BootControl.NoDollarBootFound", NULL);
- if (r < 0)
- return r;
+ int r;
- p.context.dollar_boot_source = BOOT_ENTRY_ESP;
- } else
- p.context.dollar_boot_source = BOOT_ENTRY_XBOOTLDR;
+ assert(link);
- r = run_link(&p.context);
- if (r == -EUNATCH) /* no boot entry token is set */
- return sd_varlink_error(link, "io.systemd.BootControl.BootEntryTokenUnavailable", NULL);
+ _cleanup_(link_parameters_done) LinkParameters p = {
+ .context = LINK_CONTEXT_NULL,
+ .root_fd_index = UINT_MAX,
+ .kernel_fd_index = UINT_MAX,
+ .link = link,
+ };
+
+ 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 },
+ { "entryTitle", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(p, context.entry_title), 0 },
+ { "entryVersion", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(p, context.entry_version), 0 },
+ { "entryCommit", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint64, voffsetof(p, context.entry_commit), 0 },
+ { "triesLeft", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, context.tries_left), 0 },
+ { "keepFree", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, context.keep_free), 0 },
+ {},
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ r = varlink_check_privileged_peer(link);
if (r < 0)
return r;
- return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", p.context.linked_ids));
+ r = vl_link_prepare(link, &p);
+ if (r != 0)
+ return r;
+
+ r = link_context_discover_resources(&p.context);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Nothing staged for linking. */
+ return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRV("ids", STRV_EMPTY));
+
+ return vl_link_finish(link, &p, /* with_ids= */ true);
+}
+
+int vl_method_on_completed_update(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ int r;
+
+ assert(link);
+
+ /* Only honour update notifications if they come from root */
+ r = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
+
+ /* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
+ * (we don't even dispatch them) and behave like LinkAuto() with default parameters: discover the
+ * staged UKI and extra resources and link them in. Contrary to LinkAuto() we reply without any
+ * parameters, matching the io.systemd.SysUpdate.Notify.OnCompletedUpdate() signature. */
+
+ _cleanup_(link_parameters_done) LinkParameters p = {
+ .context = LINK_CONTEXT_NULL,
+ .root_fd_index = UINT_MAX,
+ .kernel_fd_index = UINT_MAX,
+ .link = link,
+ };
+
+ r = vl_link_prepare(link, &p);
+ if (r != 0)
+ return r;
+
+ r = link_context_discover_resources(&p.context);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Nothing staged for linking. */
+ return sd_varlink_reply(link, NULL);
+
+ return vl_link_finish(link, &p, /* with_ids= */ false);
}
diff --git a/src/bootctl/bootctl-link.h b/src/bootctl/bootctl-link.h
index de64563b87..fb04c31511 100644
--- a/src/bootctl/bootctl-link.h
+++ b/src/bootctl/bootctl-link.h
@@ -4,5 +4,8 @@
#include "shared-forward.h"
int verb_link(int argc, char *argv[], uintptr_t data, void *userdata);
+int verb_link_auto(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);
+int vl_method_link_auto(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
diff --git a/src/bootctl/bootctl-reboot-to-firmware.c b/src/bootctl/bootctl-reboot-to-firmware.c
index aa5f186af9..42ee7286a0 100644
--- a/src/bootctl/bootctl-reboot-to-firmware.c
+++ b/src/bootctl/bootctl-reboot-to-firmware.c
@@ -11,6 +11,7 @@
#include "errno-util.h"
#include "log.h"
#include "parse-util.h"
+#include "varlink-util.h"
int verb_reboot_to_firmware(int argc, char *argv[], uintptr_t _data, void *userdata) {
int r;
@@ -61,6 +62,10 @@ int vl_method_set_reboot_to_firmware(sd_varlink *link, sd_json_variant *paramete
if (r != 0)
return r;
+ r = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
+
r = efi_set_reboot_to_firmware(b);
if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
return sd_varlink_error(link, "io.systemd.BootControl.RebootToFirmwareNotSupported", NULL);
diff --git a/src/bootctl/bootctl-unlink.c b/src/bootctl/bootctl-unlink.c
index 6e8e5c4bb1..44954d4fa3 100644
--- a/src/bootctl/bootctl-unlink.c
+++ b/src/bootctl/bootctl-unlink.c
@@ -26,6 +26,7 @@
#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
+#include "varlink-util.h"
typedef struct UnlinkContext {
char *root;
@@ -593,6 +594,10 @@ int vl_method_unlink(
if (p.id && !efi_loader_entry_name_valid(p.id))
return sd_varlink_error_invalid_parameter_name(link, "id");
+ r = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
+
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)
diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c
index b5cca6d222..6e522f01e6 100644
--- a/src/bootctl/bootctl.c
+++ b/src/bootctl/bootctl.c
@@ -43,6 +43,7 @@
#include "string-util.h"
#include "strv.h"
#include "varlink-io.systemd.BootControl.h"
+#include "varlink-io.systemd.SysUpdate.Notify.h"
#include "varlink-util.h"
#include "verbs.h"
#include "virt.h"
@@ -380,6 +381,9 @@ VERB_SCOPE(, verb_unlink, "unlink", "ID", VERB_ANY, 2, 0,
VERB_SCOPE(, verb_link, "link", "KERNEL", 2, 2, 0,
"Create boot loader entry for specified kernel");
+VERB_SCOPE_NOARG(, verb_link_auto, "link-auto",
+ "Create boot loader entry for the kernel and extra resources staged in /var/lib/systemd/uki/");
+
VERB_SCOPE_NOARG(, verb_cleanup, "cleanup",
"Remove files in ESP not referenced in any boot entry");
@@ -822,18 +826,23 @@ static int vl_server(void) {
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
- r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_BootControl);
+ r = sd_varlink_server_add_interface_many(
+ varlink_server,
+ &vl_interface_io_systemd_BootControl,
+ &vl_interface_io_systemd_SysUpdate_Notify);
if (r < 0)
- return log_error_errno(r, "Failed to add Varlink interface: %m");
+ return log_error_errno(r, "Failed to add Varlink interfaces: %m");
r = sd_varlink_server_bind_method_many(
varlink_server,
- "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.Link", vl_method_link,
- "io.systemd.BootControl.Unlink", vl_method_unlink);
+ "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.Link", vl_method_link,
+ "io.systemd.BootControl.LinkAuto", vl_method_link_auto,
+ "io.systemd.BootControl.Unlink", vl_method_unlink,
+ "io.systemd.SysUpdate.Notify.OnCompletedUpdate", vl_method_on_completed_update);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c
index a06852a6b5..c45c807198 100644
--- a/src/core/exec-invoke.c
+++ b/src/core/exec-invoke.c
@@ -3719,8 +3719,9 @@ static int pin_rootfs(
if (context->root_image) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
context->root_image,
pick_filter_image_raw,
ELEMENTSOF(pick_filter_image_raw),
@@ -3760,8 +3761,9 @@ static int pin_rootfs(
if (context->root_directory) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
context->root_directory,
pick_filter_image_dir,
ELEMENTSOF(pick_filter_image_dir),
@@ -3789,8 +3791,9 @@ static int pin_rootfs(
if (context->root_mstack) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
context->root_mstack,
pick_filter_image_mstack,
/* n_filters= */ 1,
diff --git a/src/core/namespace.c b/src/core/namespace.c
index a6ba2659e4..3fc0a8652d 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -564,8 +564,9 @@ static int append_extensions(
_cleanup_free_ char *mount_point = NULL;
const MountImage *m = mount_images + i;
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
m->source,
pick_filter_image_raw,
ELEMENTSOF(pick_filter_image_raw),
@@ -636,8 +637,9 @@ static int append_extensions(
if (startswith(e, "+"))
e++;
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
e,
pick_filter_image_dir,
ELEMENTSOF(pick_filter_image_dir),
diff --git a/src/libsystemd/sd-varlink/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c
index e026d4b07e..0b3304a43c 100644
--- a/src/libsystemd/sd-varlink/test-varlink-idl.c
+++ b/src/libsystemd/sd-varlink/test-varlink-idl.c
@@ -51,6 +51,7 @@
#include "varlink-io.systemd.Resolve.Monitor.h"
#include "varlink-io.systemd.Shutdown.h"
#include "varlink-io.systemd.StorageProvider.h"
+#include "varlink-io.systemd.SysUpdate.Notify.h"
#include "varlink-io.systemd.Udev.h"
#include "varlink-io.systemd.Unit.h"
#include "varlink-io.systemd.UserDatabase.h"
@@ -227,6 +228,7 @@ TEST(parse_format) {
&vl_interface_io_systemd_Resolve_Monitor,
&vl_interface_io_systemd_Shutdown,
&vl_interface_io_systemd_StorageProvider,
+ &vl_interface_io_systemd_SysUpdate_Notify,
&vl_interface_io_systemd_Udev,
&vl_interface_io_systemd_Unit,
&vl_interface_io_systemd_UserDatabase,
diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c
index 5aba4e966b..53b9b8a155 100644
--- a/src/pcrlock/pcrlock.c
+++ b/src/pcrlock/pcrlock.c
@@ -61,6 +61,7 @@
#include "unit-name.h"
#include "utf8.h"
#include "varlink-io.systemd.PCRLock.h"
+#include "varlink-io.systemd.SysUpdate.Notify.h"
#include "varlink-util.h"
#include "verbs.h"
@@ -5467,6 +5468,27 @@ static int vl_method_remove_policy(sd_varlink *link, sd_json_variant *parameters
return sd_varlink_reply(link, NULL);
}
+static int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ int r;
+
+ assert(link);
+
+ /* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
+ * (we don't even dispatch them) and simply recompute the PCR policy unconditionally, since the set
+ * of measured components might have changed. */
+
+ /* Only honour update notifications if they come from root */
+ r = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
+
+ r = make_policy(/* force= */ false, /* recovery_pin_mode= */ RECOVERY_PIN_HIDE);
+ if (r < 0)
+ return r;
+
+ return sd_varlink_reply(link, NULL);
+}
+
static int run(int argc, char *argv[]) {
int r;
@@ -5496,15 +5518,19 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
- r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_PCRLock);
+ r = sd_varlink_server_add_interface_many(
+ varlink_server,
+ &vl_interface_io_systemd_PCRLock,
+ &vl_interface_io_systemd_SysUpdate_Notify);
if (r < 0)
- return log_error_errno(r, "Failed to add Varlink interface: %m");
+ return log_error_errno(r, "Failed to add Varlink interfaces: %m");
r = sd_varlink_server_bind_method_many(
varlink_server,
- "io.systemd.PCRLock.ReadEventLog", vl_method_read_event_log,
- "io.systemd.PCRLock.MakePolicy", vl_method_make_policy,
- "io.systemd.PCRLock.RemovePolicy", vl_method_remove_policy);
+ "io.systemd.PCRLock.ReadEventLog", vl_method_read_event_log,
+ "io.systemd.PCRLock.MakePolicy", vl_method_make_policy,
+ "io.systemd.PCRLock.RemovePolicy", vl_method_remove_policy,
+ "io.systemd.SysUpdate.Notify.OnCompletedUpdate", vl_method_on_completed_update);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
diff --git a/src/portable/portable.c b/src/portable/portable.c
index a98c1162d9..9d510118be 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -821,8 +821,9 @@ static int extract_image_and_extensions(
/* If we get a path, then check if it can be resolved with vpick. We need this as we might just
* get a simple image name, which would make vpick error out. */
if (path_is_absolute(name_or_path)) {
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
name_or_path,
pick_filter_image_any,
ELEMENTSOF(pick_filter_image_any),
@@ -860,8 +861,9 @@ static int extract_image_and_extensions(
const char *path = *p;
if (path_is_absolute(*p)) {
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
*p,
pick_filter_image_any,
ELEMENTSOF(pick_filter_image_any),
@@ -2201,8 +2203,9 @@ static bool marker_matches_images(const char *marker, const char *name_or_path,
if (r < 0)
return log_debug_errno(r, "Failed to extract image name from %s, ignoring: %m", image);
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
*image_name_or_path,
pick_filter_image_any,
ELEMENTSOF(pick_filter_image_any),
diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c
index 0410b523ba..9994ff5da4 100644
--- a/src/shared/discover-image.c
+++ b/src/shared/discover-image.c
@@ -956,6 +956,7 @@ int image_find(RuntimeScope scope,
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(root,
+ rfd,
rfd,
fname_path, /* This has to be the unresolved entry with the .v suffix */
&filter,
@@ -1177,6 +1178,7 @@ int image_discover(
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(root,
+ rfd,
rfd,
fname_path, /* This has to be the unresolved entry with the .v suffix */
&filter,
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 8e874cb99d..52df34bb0f 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -250,6 +250,7 @@ shared_sources = files(
'varlink-io.systemd.Resolve.Monitor.c',
'varlink-io.systemd.Shutdown.c',
'varlink-io.systemd.StorageProvider.c',
+ 'varlink-io.systemd.SysUpdate.Notify.c',
'varlink-io.systemd.Udev.c',
'varlink-io.systemd.Unit.c',
'varlink-io.systemd.UserDatabase.c',
diff --git a/src/shared/mstack.c b/src/shared/mstack.c
index cc3cb9a75f..65f38ba3b3 100644
--- a/src/shared/mstack.c
+++ b/src/shared/mstack.c
@@ -139,7 +139,7 @@ static int mstack_load_one(MStack *mstack, const char *dir, int dir_fd, const ch
};
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick(dir, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result);
+ r = path_pick(dir, dir_fd, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result);
if (r < 0)
return log_debug_errno(r, "Failed to resolve '%s' directory: %m", fname);
if (r == 0)
diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c
index 62306f5d79..0be93990d9 100644
--- a/src/shared/varlink-io.systemd.BootControl.c
+++ b/src/shared/varlink-io.systemd.BootControl.c
@@ -185,6 +185,27 @@ static SD_VARLINK_DEFINE_METHOD(
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_METHOD(
+ LinkAuto,
+ 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("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);
@@ -233,6 +254,8 @@ SD_VARLINK_DEFINE_INTERFACE(
&vl_method_Unlink,
SD_VARLINK_SYMBOL_COMMENT("Install a kernel as boot menu item"),
&vl_method_Link,
+ SD_VARLINK_SYMBOL_COMMENT("Like Link(), but automatically discovers the UKI and extra resources staged below /var/lib/systemd/uki/"),
+ &vl_method_LinkAuto,
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."),
diff --git a/src/shared/varlink-io.systemd.SysUpdate.Notify.c b/src/shared/varlink-io.systemd.SysUpdate.Notify.c
new file mode 100644
index 0000000000..2fecc6005f
--- /dev/null
+++ b/src/shared/varlink-io.systemd.SysUpdate.Notify.c
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "varlink-io.systemd.SysUpdate.Notify.h"
+
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+ Resource,
+ SD_VARLINK_DEFINE_FIELD(transfer, SD_VARLINK_STRING, 0),
+ SD_VARLINK_DEFINE_FIELD(path, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD(
+ OnCompletedUpdate,
+ SD_VARLINK_DEFINE_INPUT(component, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_DEFINE_INPUT(version, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_DEFINE_INPUT_BY_TYPE(resources, Resource, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE));
+
+SD_VARLINK_DEFINE_INTERFACE(
+ io_systemd_SysUpdate_Notify,
+ "io.systemd.SysUpdate.Notify",
+ &vl_type_Resource,
+ &vl_method_OnCompletedUpdate);
diff --git a/src/shared/varlink-io.systemd.SysUpdate.Notify.h b/src/shared/varlink-io.systemd.SysUpdate.Notify.h
new file mode 100644
index 0000000000..72deccb380
--- /dev/null
+++ b/src/shared/varlink-io.systemd.SysUpdate.Notify.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-varlink-idl.h"
+
+extern const sd_varlink_interface vl_interface_io_systemd_SysUpdate_Notify;
diff --git a/src/shared/vpick.c b/src/shared/vpick.c
index 7890fb1708..c661f92faf 100644
--- a/src/shared/vpick.c
+++ b/src/shared/vpick.c
@@ -176,8 +176,9 @@ static int errno_from_mode(uint32_t type_mask, mode_t found) {
}
static int pin_choice(
- const char *toplevel_path,
- int toplevel_fd,
+ const char *root_path,
+ int root_fd,
+ int dir_fd,
const char *inode_path,
int _inode_fd, /* we always take ownership of the fd, even on failure */
unsigned tries_left,
@@ -190,14 +191,15 @@ static int pin_choice(
_cleanup_free_ char *resolved_path = NULL;
int r;
- assert(wildcard_fd_is_valid(toplevel_fd));
+ assert(wildcard_fd_is_valid(root_fd));
+ assert(wildcard_fd_is_valid(dir_fd));
assert(inode_path);
assert(filter);
assert(ret);
if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
- r = chaseat(toplevel_fd,
- toplevel_fd,
+ r = chaseat(root_fd,
+ dir_fd,
inode_path,
/* flags= */ 0,
FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : NULL,
@@ -212,11 +214,11 @@ static int pin_choice(
struct stat st;
if (fstat(inode_fd, &st) < 0)
return log_debug_errno(errno, "Failed to stat discovered inode '%s%s': %m",
- empty_to_root(toplevel_path), skip_leading_slash(inode_path));
+ empty_to_root(root_path), skip_leading_slash(inode_path));
if (filter->type_mask != 0 && !BIT_SET(filter->type_mask, IFTODT(st.st_mode))) {
log_debug("Inode '%s/%s' has wrong type, found '%s'.",
- empty_to_root(toplevel_path), skip_leading_slash(inode_path),
+ empty_to_root(root_path), skip_leading_slash(inode_path),
inode_type_to_string(st.st_mode));
*ret = PICK_RESULT_NULL;
return 0;
@@ -311,8 +313,9 @@ static bool architecture_matches(const PickFilter *filter, Architecture a) {
}
static int make_choice(
- const char *toplevel_path,
- int toplevel_fd,
+ const char *root_path,
+ int root_fd,
+ int dir_fd,
const char *inode_path,
int _inode_fd, /* we always take ownership of the fd, even on failure */
const PickFilter *filter,
@@ -322,13 +325,14 @@ static int make_choice(
_cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
int r;
- assert(wildcard_fd_is_valid(toplevel_fd));
+ assert(wildcard_fd_is_valid(root_fd));
+ assert(wildcard_fd_is_valid(dir_fd));
assert(inode_path);
assert(filter);
assert(ret);
if (inode_fd < 0) {
- r = chaseat(toplevel_fd, toplevel_fd, inode_path, /* flags= */ 0, NULL, &inode_fd);
+ r = chaseat(root_fd, dir_fd, inode_path, /* flags= */ 0, NULL, &inode_fd);
if (r < 0)
return r;
}
@@ -345,18 +349,19 @@ static int make_choice(
return log_oom_debug();
_cleanup_close_ int object_fd = -EBADF;
- r = chaseat(toplevel_fd, toplevel_fd, p, /* flags= */ 0, &object_path, &object_fd);
+ r = chaseat(root_fd, dir_fd, p, /* flags= */ 0, &object_path, &object_fd);
if (r == -ENOENT) {
*ret = PICK_RESULT_NULL;
return 0;
}
if (r < 0)
return log_debug_errno(r, "Failed to open '%s/%s': %m",
- empty_to_root(toplevel_path), skip_leading_slash(p));
+ empty_to_root(root_path), skip_leading_slash(p));
return pin_choice(
- toplevel_path,
- toplevel_fd,
+ root_path,
+ root_fd,
+ dir_fd,
FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p,
TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */
/* tries_left= */ UINT_MAX,
@@ -371,16 +376,16 @@ static int make_choice(
/* Underspecified, so we do our enumeration dance */
/* Convert O_PATH to a regular directory fd */
- _cleanup_close_ int dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
- if (dir_fd < 0)
- return log_debug_errno(dir_fd, "Failed to reopen '%s/%s' as directory: %m",
- empty_to_root(toplevel_path), skip_leading_slash(inode_path));
+ _cleanup_close_ int enumerate_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ if (enumerate_fd < 0)
+ return log_debug_errno(enumerate_fd, "Failed to reopen '%s/%s' as directory: %m",
+ empty_to_root(root_path), skip_leading_slash(inode_path));
_cleanup_free_ DirectoryEntries *de = NULL;
- r = readdir_all(dir_fd, 0, &de);
+ r = readdir_all(enumerate_fd, 0, &de);
if (r < 0)
return log_debug_errno(r, "Failed to read directory '%s/%s': %m",
- empty_to_root(toplevel_path), skip_leading_slash(inode_path));
+ empty_to_root(root_path), skip_leading_slash(inode_path));
_cleanup_(pick_result_done) PickResult best = PICK_RESULT_NULL;
@@ -457,8 +462,9 @@ static int make_choice(
return log_oom_debug();
_cleanup_(pick_result_done) PickResult found = PICK_RESULT_NULL;
- r = pin_choice(toplevel_path,
- toplevel_fd,
+ r = pin_choice(root_path,
+ root_fd,
+ dir_fd,
p,
/* _inode_fd= */ -EBADF,
found_tries_left,
@@ -501,8 +507,9 @@ static int make_choice(
}
static int path_pick_one(
- const char *toplevel_path,
- int toplevel_fd,
+ const char *root_path,
+ int root_fd,
+ int dir_fd,
const char *path,
const PickFilter *filter,
PickFlags flags,
@@ -513,7 +520,8 @@ static int path_pick_one(
uint32_t filter_type_mask;
int r;
- assert(wildcard_fd_is_valid(toplevel_fd));
+ assert(wildcard_fd_is_valid(root_fd));
+ assert(wildcard_fd_is_valid(dir_fd));
assert(path);
assert(filter);
assert(ret);
@@ -537,8 +545,9 @@ static int path_pick_one(
/* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */
if (filter->basename)
return make_choice(
- toplevel_path,
- toplevel_fd,
+ root_path,
+ root_fd,
+ dir_fd,
path,
/* inode_fd= */ -EBADF,
filter,
@@ -623,8 +632,9 @@ static int path_pick_one(
}
return make_choice(
- toplevel_path,
- toplevel_fd,
+ root_path,
+ root_fd,
+ dir_fd,
enumeration_path,
/* inode_fd= */ -EBADF,
&(const PickFilter) {
@@ -640,8 +650,9 @@ static int path_pick_one(
bypass:
/* Don't make any choice, but just use the passed path literally */
return pin_choice(
- toplevel_path,
- toplevel_fd,
+ root_path,
+ root_fd,
+ dir_fd,
path,
/* inode_fd= */ -EBADF,
/* tries_left= */ UINT_MAX,
@@ -651,8 +662,9 @@ bypass:
ret);
}
-int path_pick(const char *toplevel_path,
- int toplevel_fd,
+int path_pick(const char *root_path,
+ int root_fd,
+ int dir_fd,
const char *path,
const PickFilter filters[],
size_t n_filters,
@@ -662,7 +674,8 @@ int path_pick(const char *toplevel_path,
_cleanup_(pick_result_done) PickResult best = PICK_RESULT_NULL;
int r;
- assert(wildcard_fd_is_valid(toplevel_fd));
+ assert(wildcard_fd_is_valid(root_fd));
+ assert(wildcard_fd_is_valid(dir_fd));
assert(path);
assert(filters || n_filters == 0);
assert(ret);
@@ -671,7 +684,7 @@ int path_pick(const char *toplevel_path,
for (size_t i = 0; i < n_filters; i++) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick_one(toplevel_path, toplevel_fd, path, &filters[i], flags, &result);
+ r = path_pick_one(root_path, root_fd, dir_fd, path, &filters[i], flags, &result);
if (r < 0)
return r;
if (r == 0)
@@ -717,8 +730,9 @@ int path_pick_update_warn(
/* This updates the first argument if needed! */
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
*path,
filters,
n_filters,
diff --git a/src/shared/vpick.h b/src/shared/vpick.h
index 55596d24df..de67cbe353 100644
--- a/src/shared/vpick.h
+++ b/src/shared/vpick.h
@@ -45,8 +45,9 @@ void pick_result_done(PickResult *p);
int pick_result_compare(const PickResult *a, const PickResult *b, PickFlags flags);
-int path_pick(const char *toplevel_path,
- int toplevel_fd,
+int path_pick(const char *root_path,
+ int root_fd,
+ int dir_fd,
const char *path,
const PickFilter filters[],
size_t n_filters,
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 0f4102d850..4cc9883628 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -68,6 +68,7 @@
#include "strv.h"
#include "time-util.h"
#include "unit-name.h"
+#include "varlink-io.systemd.SysUpdate.Notify.h"
#include "varlink-io.systemd.sysext.h"
#include "varlink-util.h"
#include "verbs.h"
@@ -2898,6 +2899,42 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
return sd_varlink_reply(link, NULL);
}
+static int refresh_class(ImageClass image_class) {
+ _cleanup_strv_free_ char **hierarchies = NULL;
+ int r;
+
+ r = parse_env_extension_hierarchies(&hierarchies, image_class_info[image_class].name_env);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine %s hierarchies: %m", image_class_info[image_class].short_identifier);
+
+ return refresh(image_class, hierarchies, arg_force, arg_no_reload, arg_always_refresh, arg_noexec);
+}
+
+static int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ int r;
+
+ assert(link);
+
+ /* Only honour update notifications if they come from root */
+ r = varlink_check_privileged_peer(link);
+ if (r < 0)
+ return r;
+
+ /* Triggered by systemd-sysupdate after an update completed. We deliberately ignore all parameters
+ * (we don't even dispatch them). This method is reached through the sysext socket, but a single
+ * notification refreshes both image classes, so freshly downloaded sysexts and confexts are both
+ * picked up, equivalent to "systemd-sysext refresh" plus "systemd-confext refresh". We attempt both
+ * classes and only fail afterwards, so a problem with one does not prevent refreshing the other. */
+
+ r = 0;
+ RET_GATHER(r, refresh_class(IMAGE_SYSEXT));
+ RET_GATHER(r, refresh_class(IMAGE_CONFEXT));
+ if (r < 0)
+ return r;
+
+ return sd_varlink_reply(link, NULL);
+}
+
VERB_NOARG(verb_list, "list", "List installed extensions");
static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_hashmap_free_ Hashmap *images = NULL;
@@ -3184,16 +3221,20 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
- r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_sysext);
+ r = sd_varlink_server_add_interface_many(
+ varlink_server,
+ &vl_interface_io_systemd_sysext,
+ &vl_interface_io_systemd_SysUpdate_Notify);
if (r < 0)
- return log_error_errno(r, "Failed to add Varlink interface: %m");
+ return log_error_errno(r, "Failed to add Varlink interfaces: %m");
r = sd_varlink_server_bind_method_many(
varlink_server,
"io.systemd.sysext.Merge", vl_method_merge,
"io.systemd.sysext.Unmerge", vl_method_unmerge,
"io.systemd.sysext.Refresh", vl_method_refresh,
- "io.systemd.sysext.List", vl_method_list);
+ "io.systemd.sysext.List", vl_method_list,
+ "io.systemd.SysUpdate.Notify.OnCompletedUpdate", vl_method_on_completed_update);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index 81ed4861e0..9f78ee78bf 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -3,11 +3,14 @@
#include
#include "sd-daemon.h"
+#include "sd-json.h"
+#include "sd-varlink.h"
#include "build.h"
#include "conf-files.h"
#include "constants.h"
#include "dissect-image.h"
+#include "env-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
@@ -36,6 +39,7 @@
#include "sysupdate-transfer.h"
#include "sysupdate-update-set.h"
#include "sysupdate-util.h"
+#include "varlink-util.h"
#include "verbs.h"
static char *arg_definitions = NULL;
@@ -1192,6 +1196,82 @@ static int context_process_partial_and_pending(
return 1;
}
+static int notify_subscribers_reply(
+ sd_varlink *link,
+ sd_json_variant *reply,
+ const char *error_id,
+ sd_varlink_reply_flags_t flags,
+ void *userdata) {
+
+ assert(link);
+
+ if (error_id)
+ log_warning("Notification subscriber '%s' returned error, ignoring: %s",
+ strna(sd_varlink_get_description(link)), error_id);
+
+ return 0;
+}
+
+static int context_notify_subscribers(Context *c, UpdateSet *us) {
+ int r;
+
+ assert(c);
+
+ /* 'us' is NULL when we are forced to notify even though no update was applied (via
+ * SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1). In that case we send neither a version nor a resource list. */
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *resources = NULL;
+ if (us)
+ for (size_t i = 0; i < c->n_transfers; i++) {
+ Instance *inst = us->instances[i];
+ Transfer *t = c->transfers[i];
+
+ if (inst->resource == &t->target &&
+ !inst->is_pending)
+ continue;
+
+ /* Report where the resource was installed *to* (not the source it came from): the
+ * final on-disk path for filesystem targets, the partition device node for partition
+ * targets. */
+ const char *target_path =
+ RESOURCE_IS_FILESYSTEM(t->target.type) ? t->final_path :
+ t->target.type == RESOURCE_PARTITION ? t->partition_info.device :
+ NULL;
+
+ r = sd_json_variant_append_arraybo(
+ &resources,
+ SD_JSON_BUILD_PAIR_STRING("transfer", t->id),
+ SD_JSON_BUILD_PAIR_CONDITION(!!target_path, "path", SD_JSON_BUILD_STRING(target_path)));
+ if (r < 0)
+ return log_warning_errno(r, "Failed to build sysupdate notify resources list, skipping notification: %m");
+ }
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
+ r = sd_json_buildo(
+ ¶ms,
+ SD_JSON_BUILD_PAIR_CONDITION(!!arg_component, "component", SD_JSON_BUILD_STRING(arg_component)),
+ SD_JSON_BUILD_PAIR_CONDITION(!!us, "version", SD_JSON_BUILD_STRING(us ? us->version : NULL)),
+ SD_JSON_BUILD_PAIR_CONDITION(!!resources, "resources", SD_JSON_BUILD_VARIANT(resources)));
+ if (r < 0)
+ return log_warning_errno(r, "Failed to build sysupdate notify parameters, skipping notification: %m");
+
+ ssize_t n = varlink_execute_directory(
+ VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK,
+ "io.systemd.SysUpdate.Notify.OnCompletedUpdate",
+ params,
+ /* more= */ false,
+ /* timeout_usec= */ 5 * USEC_PER_MINUTE,
+ notify_subscribers_reply,
+ /* userdata= */ NULL);
+ if (n < 0)
+ log_debug_errno(n, "Failed to dispatch sysupdate notification to %s, ignoring: %m",
+ VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
+ else if (n > 0)
+ log_debug("Dispatched sysupdate notification to %zi subscribers in %s.", n, VARLINK_DIR_SYSUPDATE_NOTIFY_HOOK);
+
+ return 0;
+}
+
static int context_install(
Context *c,
const char *version,
@@ -1236,6 +1316,9 @@ static int context_install(
log_info("%s Successfully installed update '%s'.", glyph(GLYPH_SPARKLES), us->version);
+ if (!arg_root)
+ (void) context_notify_subscribers(c, us);
+
(void) sd_notifyf(/* unset_environment= */ false,
"STATUS=Installed '%s'.", us->version);
@@ -1625,6 +1708,17 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag
installed = r > 0;
}
+
+ /* context_install() returns > 0 (and emits a notification) only if it actually applied an update. If
+ * nothing was applied but SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 is set, still notify subscribers (without a
+ * resource list), so e.g. a kernel/policy refresh can be triggered unconditionally. */
+ if ((action_flags & UPDATE_ACTION_INSTALL) && !installed) {
+ int f = secure_getenv_bool("SYSTEMD_SYSUPDATE_FORCE_NOTIFY");
+ if (f < 0 && f != -ENXIO)
+ log_debug_errno(f, "Failed to parse $SYSTEMD_SYSUPDATE_FORCE_NOTIFY, ignoring: %m");
+ if (f > 0)
+ (void) context_notify_subscribers(context, /* us= */ NULL);
+ }
}
if (arg_cleanup > 0)
diff --git a/src/test/test-vpick.c b/src/test/test-vpick.c
index 400380b575..6fea29e594 100644
--- a/src/test/test-vpick.c
+++ b/src/test/test-vpick.c
@@ -48,7 +48,7 @@ TEST(path_pick) {
};
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "99");
ASSERT_EQ(result.architecture, ARCHITECTURE_X86);
@@ -58,7 +58,7 @@ TEST(path_pick) {
}
filter.architecture = ARCHITECTURE_X86_64;
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "55");
ASSERT_EQ(result.architecture, ARCHITECTURE_X86_64);
@@ -66,7 +66,7 @@ TEST(path_pick) {
pick_result_done(&result);
filter.architecture = ARCHITECTURE_IA64;
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
ASSERT_EQ(result.architecture, ARCHITECTURE_IA64);
@@ -75,7 +75,7 @@ TEST(path_pick) {
filter.architecture = _ARCHITECTURE_INVALID;
filter.version = "5";
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
if (native_architecture() != ARCHITECTURE_IA64) {
@@ -85,7 +85,7 @@ TEST(path_pick) {
pick_result_done(&result);
filter.architecture = ARCHITECTURE_IA64;
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
ASSERT_EQ(result.architecture, ARCHITECTURE_IA64);
@@ -93,7 +93,7 @@ TEST(path_pick) {
pick_result_done(&result);
filter.architecture = ARCHITECTURE_CRIS;
- ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_EQ(result.st.st_mode, MODE_INVALID);
ASSERT_NULL(result.version);
ASSERT_LT(result.architecture, 0);
@@ -104,7 +104,7 @@ TEST(path_pick) {
filter.architecture = _ARCHITECTURE_INVALID;
filter.version = NULL;
if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) {
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "55");
@@ -123,7 +123,7 @@ TEST(path_pick) {
pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo___.raw"));
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "55");
ASSERT_EQ(result.architecture, native_architecture());
@@ -137,10 +137,10 @@ TEST(path_pick) {
pp = ASSERT_NOT_NULL(path_join(p, "foo.v/foo_5.raw"));
filter.type_mask = UINT32_C(1) << DT_DIR;
- ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
filter.type_mask = UINT32_C(1) << DT_REG;
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_NULL(result.version);
ASSERT_EQ(result.architecture, _ARCHITECTURE_INVALID);
@@ -153,7 +153,7 @@ TEST(path_pick) {
filter.architecture = ARCHITECTURE_S390;
filter.basename = "quux";
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE|PICK_TRIES, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "2");
ASSERT_EQ(result.tries_left, 4U);
@@ -209,7 +209,7 @@ TEST(pick_filter_image_any) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
/* Test pick_filter_image_any: should pick the highest version, which is the directory test_5 */
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "5");
ASSERT_TRUE(endswith(result.path, "/test_5"));
@@ -219,14 +219,14 @@ TEST(pick_filter_image_any) {
ASSERT_OK(unlinkat(sub_dfd, "test_4", AT_REMOVEDIR));
ASSERT_OK(unlinkat(sub_dfd, "test_5", AT_REMOVEDIR));
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "3");
ASSERT_TRUE(endswith(result.path, "/test_3.raw"));
pick_result_done(&result);
/* Verify that pick_filter_image_raw only matches .raw files */
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISREG(result.st.st_mode));
ASSERT_STREQ(result.version, "3");
ASSERT_TRUE(endswith(result.path, "/test_3.raw"));
@@ -239,12 +239,12 @@ TEST(pick_filter_image_any) {
ASSERT_OK(unlinkat(sub_dfd, "test_3.raw", 0));
/* Now only test_10.txt, test_11.img, and test_12 remain - none should match */
- ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
/* But if we add a directory, it should be picked */
ASSERT_OK(mkdirat(sub_dfd, "test_6", 0755));
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "6");
ASSERT_TRUE(endswith(result.path, "/test_6"));
@@ -263,7 +263,7 @@ TEST(pick_filter_image_any) {
pick_result_done(&result);
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "2");
ASSERT_TRUE(endswith(result.path, "/myimage_2"));
@@ -273,7 +273,7 @@ TEST(pick_filter_image_any) {
ASSERT_OK(unlinkat(sub_dfd, "myimage_1", AT_REMOVEDIR));
ASSERT_OK(unlinkat(sub_dfd, "myimage_2", AT_REMOVEDIR));
- ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_ZERO(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
}
TEST(path_pick_resolve) {
@@ -294,26 +294,26 @@ TEST(path_pick_resolve) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
/* Test without PICK_RESOLVE - should return the symlink path */
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE, &result));
ASSERT_STREQ(result.version, "2");
ASSERT_TRUE(endswith(result.path, "/resolve_2.raw"));
pick_result_done(&result);
/* Test with PICK_RESOLVE - should return the resolved (target) path */
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_any, ELEMENTSOF(pick_filter_image_any), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
ASSERT_STREQ(result.version, "2");
ASSERT_TRUE(endswith(result.path, "/target_file.raw"));
pick_result_done(&result);
/* Test pick_filter_image_dir without PICK_RESOLVE */
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "1");
ASSERT_TRUE(endswith(result.path, "/resolve_1"));
pick_result_done(&result);
/* Test pick_filter_image_dir with PICK_RESOLVE */
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_dir, ELEMENTSOF(pick_filter_image_dir), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
ASSERT_TRUE(S_ISDIR(result.st.st_mode));
ASSERT_STREQ(result.version, "1");
ASSERT_TRUE(endswith(result.path, "/target_dir"));
@@ -323,12 +323,12 @@ TEST(path_pick_resolve) {
ASSERT_OK(symlinkat("target_file.raw", dfd, "intermediate_link.raw"));
ASSERT_OK(symlinkat("../intermediate_link.raw", sub_dfd, "resolve_3.raw"));
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE, &result));
ASSERT_STREQ(result.version, "3");
ASSERT_TRUE(endswith(result.path, "/resolve_3.raw"));
pick_result_done(&result);
- ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
+ ASSERT_OK_POSITIVE(path_pick(NULL, AT_FDCWD, AT_FDCWD, pp, pick_filter_image_raw, ELEMENTSOF(pick_filter_image_raw), PICK_ARCHITECTURE|PICK_RESOLVE, &result));
ASSERT_STREQ(result.version, "3");
/* The chain should be fully resolved to target_file.raw */
ASSERT_TRUE(endswith(result.path, "/target_file.raw"));
diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c
index f0b5ef44df..85f2b5c1cb 100644
--- a/src/vpick/vpick-tool.c
+++ b/src/vpick/vpick-tool.c
@@ -237,8 +237,9 @@ static int run(int argc, char *argv[]) {
return log_error_errno(r, "Failed to make path '%s' absolute: %m", *i);
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
- r = path_pick(/* toplevel_path= */ NULL,
- /* toplevel_fd= */ AT_FDCWD,
+ r = path_pick(/* root_path= */ NULL,
+ /* root_fd= */ AT_FDCWD,
+ /* dir_fd= */ AT_FDCWD,
p,
&(PickFilter) {
.basename = arg_filter_basename,
diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh
index 4ac8b8a52d..6cabcab5bb 100755
--- a/test/units/TEST-72-SYSUPDATE.sh
+++ b/test/units/TEST-72-SYSUPDATE.sh
@@ -49,6 +49,10 @@ systemctl daemon-reload
at_exit() {
set +e
+ systemctl stop test-sysupdate-notify-recorder.socket
+ rm -f /run/systemd/system/test-sysupdate-notify-recorder.socket \
+ /run/systemd/system/test-sysupdate-notify-recorder@.service
+
losetup -n --output NAME --associated "$BACKING_FILE" | while read -r loop_dev; do
losetup --detach "$loop_dev"
done
@@ -947,4 +951,85 @@ test ! -f "$CLEANUP/target/alpha-v2.bin"
rm -rf "$CONFIGDIR" "$INSTALLDB" "$CLEANUP"
+# Verify the notification callout: after a successful update, sysupdate must connect to every socket in
+# /run/systemd/sysupdate/notify/ and invoke io.systemd.SysUpdate.Notify.OnCompletedUpdate(). We hook a tiny
+# recorder socket into that directory that captures the request and replies with success.
+NOTIFY_LOG="$WORKDIR/notify.log"
+rm -f "$NOTIFY_LOG"
+
+cat >"$WORKDIR/notify-recorder.py" </run/systemd/system/test-sysupdate-notify-recorder.socket <"/run/systemd/system/test-sysupdate-notify-recorder@.service" <"$WORKDIR/source/notifytest-v1.bin"
+(cd "$WORKDIR/source" && sha256sum notifytest-v1.bin >SHA256SUMS)
+cat >"$CONFIGDIR/01-notifytest.transfer" </dev/null
+jq -e '.parameters.version == "v1"' <<<"$notify_line" >/dev/null
+jq -e '.parameters.resources | length >= 1' <<<"$notify_line" >/dev/null
+jq -e '.parameters.resources | all(has("transfer"))' <<<"$notify_line" >/dev/null
+
+# A no-op update ("No update needed") must NOT emit a notification.
+rm -f "$NOTIFY_LOG"
+"$SYSUPDATE" --verify=no update
+test ! -s "$NOTIFY_LOG"
+
+systemctl stop test-sysupdate-notify-recorder.socket
+rm -f /run/systemd/system/test-sysupdate-notify-recorder.socket \
+ /run/systemd/system/test-sysupdate-notify-recorder@.service
+systemctl daemon-reload
+rm -rf "$CONFIGDIR" "$WORKDIR/blobs"
+rm -f "$WORKDIR/source/notifytest-v1.bin" "$WORKDIR/source/SHA256SUMS" \
+ "$WORKDIR/notify-recorder.py" "$NOTIFY_LOG"
+
touch /testok
diff --git a/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh b/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh
index 2b095c1fbc..0b19acd017 100755
--- a/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh
+++ b/test/units/TEST-87-AUX-UTILS-VM.bootctl.sh
@@ -710,4 +710,162 @@ EOF
'{"id":"foo*.conf"}')
}
+cleanup_link_auto() {
+ rm -rf /run/systemd/uki /etc/systemd/uki
+ if [[ -n "${LINK_WORKDIR:-}" ]]; then
+ rm -rf "$LINK_WORKDIR"
+ unset LINK_WORKDIR
+ fi
+ restore_esp
+}
+
+testcase_bootctl_link_auto() {
+ 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-auto.XXXXXXXXXX)"
+ trap cleanup_link_auto RETURN ERR
+
+ # Ensure loader/entries directory is present
+ bootctl install --make-entry-directory=yes
+
+ local ESP
+ ESP="$(bootctl --print-esp-path)"
+
+ 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"
+
+ # Two distinct UKIs, so we can tell which one was picked up.
+ ukify build \
+ --linux "$LINK_WORKDIR/vmlinuz" \
+ --initrd "$LINK_WORKDIR/initrd" \
+ --os-release "@$LINK_WORKDIR/os-release" \
+ --uname "1.2.3-testkernel" \
+ --cmdline "quiet uki=a" \
+ --output "$LINK_WORKDIR/uki_a.efi"
+ ukify build \
+ --linux "$LINK_WORKDIR/vmlinuz" \
+ --initrd "$LINK_WORKDIR/initrd" \
+ --os-release "@$LINK_WORKDIR/os-release" \
+ --uname "1.2.3-testkernel" \
+ --cmdline "quiet uki=b" \
+ --output "$LINK_WORKDIR/uki_b.efi"
+
+ local TOKEN="systemdtest"
+ local BOOTCTL=(bootctl "--entry-token=literal:$TOKEN")
+ local ENTRY="$ESP/loader/entries/${TOKEN}-commit_1.conf"
+
+ # --- Test 1: link-auto picks up kernel.efi + extras.d/ from /run/systemd/uki/ ---
+ rm -rf /run/systemd/uki
+ mkdir -p /run/systemd/uki/extras.d
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi
+ echo "sysext-data" >/run/systemd/uki/extras.d/hello.sysext.raw
+ echo "confext-data" >/run/systemd/uki/extras.d/hello.confext.raw
+ echo "cred-data" >/run/systemd/uki/extras.d/hello.cred
+
+ "${BOOTCTL[@]}" link-auto
+
+ test -f "$ENTRY"
+ test -f "$ESP/$TOKEN/kernel.efi"
+ cmp "$LINK_WORKDIR/uki_a.efi" "$ESP/$TOKEN/kernel.efi"
+ test -f "$ESP/$TOKEN/hello.sysext.raw"
+ test -f "$ESP/$TOKEN/hello.confext.raw"
+ test -f "$ESP/$TOKEN/hello.cred"
+ grep "^uki /${TOKEN}/kernel.efi\$" "$ENTRY" >/dev/null
+ 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
+
+ "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
+ test ! -e "$ENTRY"
+ test ! -e "$ESP/$TOKEN/kernel.efi"
+
+ # --- Test 2: versioned kernel.efi.v/ and extras .v/ are resolved via vpick ---
+ rm -rf /run/systemd/uki
+ mkdir -p /run/systemd/uki/kernel.efi.v /run/systemd/uki/extras.d/hello.sysext.raw.v
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi.v/kernel_1.0.efi
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi.v/kernel_2.0.efi
+ echo "sysext-1" >/run/systemd/uki/extras.d/hello.sysext.raw.v/hello_1.0.sysext.raw
+ echo "sysext-2" >/run/systemd/uki/extras.d/hello.sysext.raw.v/hello_2.0.sysext.raw
+
+ "${BOOTCTL[@]}" link-auto
+
+ test -f "$ENTRY"
+ # vpick must select the newest version
+ test -f "$ESP/$TOKEN/kernel_2.0.efi"
+ test ! -e "$ESP/$TOKEN/kernel_1.0.efi"
+ test -f "$ESP/$TOKEN/hello_2.0.sysext.raw"
+ test ! -e "$ESP/$TOKEN/hello_1.0.sysext.raw"
+ grep "^uki /${TOKEN}/kernel_2.0.efi\$" "$ENTRY" >/dev/null
+ grep "^extra /${TOKEN}/hello_2.0.sysext.raw\$" "$ENTRY" >/dev/null
+
+ "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
+
+ # --- Test 3: priority — /etc/systemd/uki/ wins over /run/systemd/uki/ ---
+ rm -rf /run/systemd/uki /etc/systemd/uki
+ mkdir -p /run/systemd/uki/extras.d /etc/systemd/uki/extras.d
+ cp "$LINK_WORKDIR/uki_b.efi" /run/systemd/uki/kernel.efi
+ cp "$LINK_WORKDIR/uki_a.efi" /etc/systemd/uki/kernel.efi
+ echo "run-cred" >/run/systemd/uki/extras.d/hello.cred
+ echo "etc-cred" >/etc/systemd/uki/extras.d/hello.cred
+
+ "${BOOTCTL[@]}" link-auto
+
+ test -f "$ESP/$TOKEN/kernel.efi"
+ # The /etc copy (uki_a) must win over the /run copy (uki_b)
+ cmp "$LINK_WORKDIR/uki_a.efi" "$ESP/$TOKEN/kernel.efi"
+ cmp <(echo "etc-cred") "$ESP/$TOKEN/hello.cred"
+
+ "${BOOTCTL[@]}" unlink "${TOKEN}-commit_1.conf"
+ rm -rf /etc/systemd/uki
+
+ # --- Test 4: with nothing staged, link-auto is a successful no-op ---
+ rm -rf /run/systemd/uki
+ "${BOOTCTL[@]}" link-auto
+ # No entries referencing our token should remain.
+ local leftover
+ leftover="$(find "$ESP/loader/entries/" -name "*$TOKEN*" -print -quit 2>/dev/null)"
+ test -z "$leftover"
+
+ # === Varlink coverage: io.systemd.BootControl.LinkAuto ===
+ local BOOTCTL_BIN vreply vid vtoken
+ BOOTCTL_BIN="$(type -p bootctl)"
+
+ # --- Test 5: LinkAuto discovers kernel.efi + extras ---
+ rm -rf /run/systemd/uki
+ mkdir -p /run/systemd/uki/extras.d
+ cp "$LINK_WORKDIR/uki_a.efi" /run/systemd/uki/kernel.efi
+ echo "cred-data" >/run/systemd/uki/extras.d/hello.cred
+
+ vreply="$(varlinkctl call --json=short "$BOOTCTL_BIN" io.systemd.BootControl.LinkAuto '{}')"
+ 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/kernel.efi"
+ test -f "$ESP/$vtoken/hello.cred"
+ grep "^uki /$vtoken/kernel.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/kernel.efi"
+ test ! -e "$ESP/$vtoken/hello.cred"
+
+ # --- Test 6: LinkAuto with nothing staged returns an empty id list ---
+ rm -rf /run/systemd/uki
+ vreply="$(varlinkctl call --json=short "$BOOTCTL_BIN" io.systemd.BootControl.LinkAuto '{}')"
+ assert_eq "$(echo "$vreply" | jq -r '.ids | length')" "0"
+}
+
run_testcases
diff --git a/units/meson.build b/units/meson.build
index f261fded6f..83dae605fa 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -303,6 +303,14 @@ units = [
'conditions' : ['ENABLE_BOOTLOADER'],
'symlinks' : ['sockets.target.wants/'],
},
+ {
+ 'file' : 'systemd-sysupdate-notify-bootctl@.service',
+ 'conditions' : ['ENABLE_BOOTLOADER'],
+ },
+ {
+ 'file' : 'systemd-sysupdate-notify-bootctl.socket',
+ 'conditions' : ['ENABLE_BOOTLOADER'],
+ },
{
'file' : 'systemd-confext.service',
'conditions' : ['ENABLE_SYSEXT'],
@@ -706,6 +714,14 @@ units = [
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
'symlinks' : ['sockets.target.wants/'],
},
+ {
+ 'file' : 'systemd-sysupdate-notify-pcrlock@.service.in',
+ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
+ },
+ {
+ 'file' : 'systemd-sysupdate-notify-pcrlock.socket',
+ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
+ },
{
'file' : 'systemd-portabled.service.in',
'conditions' : ['ENABLE_PORTABLED'],
@@ -807,6 +823,14 @@ units = [
'conditions' : ['ENABLE_SYSEXT'],
'symlinks' : ['sockets.target.wants/'],
},
+ {
+ 'file' : 'systemd-sysupdate-notify-sysext@.service',
+ 'conditions' : ['ENABLE_SYSEXT'],
+ },
+ {
+ 'file' : 'systemd-sysupdate-notify-sysext.socket',
+ 'conditions' : ['ENABLE_SYSEXT'],
+ },
{
'file' : 'systemd-sysext@.service',
'conditions' : ['ENABLE_SYSEXT'],
diff --git a/units/systemd-boot-update.service b/units/systemd-boot-update.service
index 9a4fbc1e71..4c829ad314 100644
--- a/units/systemd-boot-update.service
+++ b/units/systemd-boot-update.service
@@ -10,9 +10,9 @@
[Unit]
Description=Automatic Boot Loader Update
Documentation=man:bootctl(1)
-ConditionPathExists=!/etc/initrd-release
-
DefaultDependencies=no
+ConditionPathExists=!/etc/initrd-release
+ConditionFirmware=uefi
After=local-fs.target
Before=sysinit.target systemd-update-done.service
Conflicts=shutdown.target
diff --git a/units/systemd-sysupdate-notify-bootctl.socket b/units/systemd-sysupdate-notify-bootctl.socket
new file mode 100644
index 0000000000..46114ddb20
--- /dev/null
+++ b/units/systemd-sysupdate-notify-bootctl.socket
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Boot Loader UKI Auto-Link on Completed System Update
+Documentation=man:bootctl(1)
+DefaultDependencies=no
+ConditionFirmware=uefi
+After=local-fs.target
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/sysupdate/notify/io.systemd.BootControl
+FileDescriptorName=varlink
+SocketMode=0644
+Accept=yes
+MaxConnectionsPerSource=16
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+XAttrAccept=user.varlink=server
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/systemd-sysupdate-notify-bootctl@.service b/units/systemd-sysupdate-notify-bootctl@.service
new file mode 100644
index 0000000000..6b0604c2f1
--- /dev/null
+++ b/units/systemd-sysupdate-notify-bootctl@.service
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Boot Loader UKI Auto-Link on Completed System Update
+Documentation=man:bootctl(1)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=local-fs.target
+Before=shutdown.target
+
+[Service]
+ExecStart=bootctl
diff --git a/units/systemd-sysupdate-notify-pcrlock.socket b/units/systemd-sysupdate-notify-pcrlock.socket
new file mode 100644
index 0000000000..6b26911bcc
--- /dev/null
+++ b/units/systemd-sysupdate-notify-pcrlock.socket
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Recompute TPM PCR Policy on Completed System Update
+Documentation=man:systemd-pcrlock(8)
+DefaultDependencies=no
+After=tpm2.target
+Before=sockets.target
+ConditionSecurity=measured-uki
+
+[Socket]
+ListenStream=/run/systemd/sysupdate/notify/io.systemd.PCRLock
+FileDescriptorName=varlink
+SocketMode=0644
+Accept=yes
+MaxConnectionsPerSource=16
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+XAttrAccept=user.varlink=server
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/systemd-sysupdate-notify-pcrlock@.service.in b/units/systemd-sysupdate-notify-pcrlock@.service.in
new file mode 100644
index 0000000000..50108b6f07
--- /dev/null
+++ b/units/systemd-sysupdate-notify-pcrlock@.service.in
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Recompute TPM PCR Policy on Completed System Update
+Documentation=man:systemd-pcrlock(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=systemd-tpm2-setup.service
+After=systemd-remount-fs.service var.mount
+Before=shutdown.target
+
+[Service]
+ExecStart={{LIBEXECDIR}}/systemd-pcrlock --location=770
diff --git a/units/systemd-sysupdate-notify-sysext.socket b/units/systemd-sysupdate-notify-sysext.socket
new file mode 100644
index 0000000000..55d29a28dd
--- /dev/null
+++ b/units/systemd-sysupdate-notify-sysext.socket
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=System Extension and Configuration Refresh on Completed System Update
+Documentation=man:systemd-sysext(8)
+DefaultDependencies=no
+After=local-fs.target
+Before=sockets.target
+ConditionCapability=CAP_SYS_ADMIN
+
+[Socket]
+ListenStream=/run/systemd/sysupdate/notify/io.systemd.sysext
+FileDescriptorName=varlink
+SocketMode=0644
+Accept=yes
+MaxConnectionsPerSource=16
+XAttrEntryPoint=user.varlink=entrypoint
+XAttrListen=user.varlink=listen
+XAttrAccept=user.varlink=server
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/systemd-sysupdate-notify-sysext@.service b/units/systemd-sysupdate-notify-sysext@.service
new file mode 100644
index 0000000000..c0efe542dd
--- /dev/null
+++ b/units/systemd-sysupdate-notify-sysext@.service
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=System Extension and Configuration Refresh on Completed System Update
+Documentation=man:systemd-sysext(8)
+DefaultDependencies=no
+Conflicts=shutdown.target initrd-switch-root.target
+After=local-fs.target
+Before=shutdown.target initrd-switch-root.target
+
+[Service]
+ExecStart=-systemd-sysext