sysext: make some calls available via varlink

This commit is contained in:
Lennart Poettering 2023-10-09 18:57:41 +02:00
parent c7fda70716
commit f5151fb459
11 changed files with 593 additions and 75 deletions

View File

@ -1353,6 +1353,24 @@ bool image_in_search_path(
return false; return false;
} }
int image_to_json(const struct Image *img, JsonVariant **ret) {
assert(img);
return json_build(ret,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR_STRING("Type", image_type_to_string(img->type)),
JSON_BUILD_PAIR_STRING("Class", image_class_to_string(img->class)),
JSON_BUILD_PAIR_STRING("Name", img->name),
JSON_BUILD_PAIR_CONDITION(img->path, "Path", JSON_BUILD_STRING(img->path)),
JSON_BUILD_PAIR_BOOLEAN("ReadOnly", img->read_only),
JSON_BUILD_PAIR_CONDITION(img->crtime != 0, "CreationTimestamp", JSON_BUILD_UNSIGNED(img->crtime)),
JSON_BUILD_PAIR_CONDITION(img->mtime != 0, "ModificationTimestamp", JSON_BUILD_UNSIGNED(img->mtime)),
JSON_BUILD_PAIR_CONDITION(img->usage != UINT64_MAX, "Usage", JSON_BUILD_UNSIGNED(img->usage)),
JSON_BUILD_PAIR_CONDITION(img->usage_exclusive != UINT64_MAX, "UsageExclusive", JSON_BUILD_UNSIGNED(img->usage_exclusive)),
JSON_BUILD_PAIR_CONDITION(img->limit != UINT64_MAX, "Limit", JSON_BUILD_UNSIGNED(img->limit)),
JSON_BUILD_PAIR_CONDITION(img->limit_exclusive != UINT64_MAX, "LimitExclusive", JSON_BUILD_UNSIGNED(img->limit_exclusive))));
}
static const char* const image_type_table[_IMAGE_TYPE_MAX] = { static const char* const image_type_table[_IMAGE_TYPE_MAX] = {
[IMAGE_DIRECTORY] = "directory", [IMAGE_DIRECTORY] = "directory",
[IMAGE_SUBVOLUME] = "subvolume", [IMAGE_SUBVOLUME] = "subvolume",

View File

@ -8,6 +8,7 @@
#include "hashmap.h" #include "hashmap.h"
#include "image-policy.h" #include "image-policy.h"
#include "json.h"
#include "lock-util.h" #include "lock-util.h"
#include "macro.h" #include "macro.h"
#include "os-util.h" #include "os-util.h"
@ -116,4 +117,6 @@ static inline bool IMAGE_IS_HOST(const struct Image *i) {
return false; return false;
} }
int image_to_json(const struct Image *i, JsonVariant **ret);
extern const struct hash_ops image_hash_ops; extern const struct hash_ops image_hash_ops;

View File

@ -175,6 +175,7 @@ shared_sources = files(
'varlink-io.systemd.Resolve.c', 'varlink-io.systemd.Resolve.c',
'varlink-io.systemd.UserDatabase.c', 'varlink-io.systemd.UserDatabase.c',
'varlink-io.systemd.oom.c', 'varlink-io.systemd.oom.c',
'varlink-io.systemd.sysext.c',
'varlink-org.varlink.service.c', 'varlink-org.varlink.service.c',
'verb-log-control.c', 'verb-log-control.c',
'verbs.c', 'verbs.c',

View File

@ -0,0 +1,67 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.sysext.h"
static VARLINK_DEFINE_ENUM_TYPE(
ImageClass,
VARLINK_DEFINE_ENUM_VALUE(sysext),
VARLINK_DEFINE_ENUM_VALUE(confext));
static VARLINK_DEFINE_ENUM_TYPE(
ImageType,
VARLINK_DEFINE_ENUM_VALUE(directory),
VARLINK_DEFINE_ENUM_VALUE(subvolume),
VARLINK_DEFINE_ENUM_VALUE(raw),
VARLINK_DEFINE_ENUM_VALUE(block));
static VARLINK_DEFINE_METHOD(
Merge,
VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(force, VARLINK_BOOL, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(noReload, VARLINK_BOOL, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(noexec, VARLINK_BOOL, VARLINK_NULLABLE));
static VARLINK_DEFINE_METHOD(
Unmerge,
VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(noReload, VARLINK_BOOL, VARLINK_NULLABLE));
static VARLINK_DEFINE_METHOD(
Refresh,
VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(force, VARLINK_BOOL, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(noReload, VARLINK_BOOL, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(noexec, VARLINK_BOOL, VARLINK_NULLABLE));
static VARLINK_DEFINE_METHOD(
List,
VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT_BY_TYPE(Class, ImageClass, 0),
VARLINK_DEFINE_OUTPUT_BY_TYPE(Type, ImageType, 0),
VARLINK_DEFINE_OUTPUT(Name, VARLINK_STRING, 0),
VARLINK_DEFINE_OUTPUT(Path, VARLINK_STRING, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT(ReadOnly, VARLINK_BOOL, 0),
VARLINK_DEFINE_OUTPUT(CreationTimestamp, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT(ModificationTimestamp, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT(Usage, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT(UsageExclusive, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT(Limit, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT(LimitExclusive, VARLINK_INT, VARLINK_NULLABLE));
static VARLINK_DEFINE_ERROR(NoImagesFound);
static VARLINK_DEFINE_ERROR(
AlreadyMerged,
VARLINK_DEFINE_FIELD(hierarchy, VARLINK_STRING, 0));
VARLINK_DEFINE_INTERFACE(
io_systemd_sysext,
"io.systemd.sysext",
&vl_type_ImageClass,
&vl_type_ImageType,
&vl_method_Merge,
&vl_method_Unmerge,
&vl_method_Refresh,
&vl_method_List,
&vl_error_NoImagesFound,
&vl_error_AlreadyMerged);

View File

@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "varlink-idl.h"
extern const VarlinkInterface vl_interface_io_systemd_sysext;

View File

@ -44,6 +44,8 @@
#include "sort-util.h" #include "sort-util.h"
#include "terminal-util.h" #include "terminal-util.h"
#include "user-util.h" #include "user-util.h"
#include "varlink.h"
#include "varlink-io.systemd.sysext.h"
#include "verbs.h" #include "verbs.h"
static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */ static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */
@ -55,6 +57,7 @@ static bool arg_force = false;
static bool arg_no_reload = false; static bool arg_no_reload = false;
static int arg_noexec = -1; static int arg_noexec = -1;
static ImagePolicy *arg_image_policy = NULL; static ImagePolicy *arg_image_policy = NULL;
static bool arg_varlink = false;
/* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */ /* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */
static ImageClass arg_image_class = IMAGE_SYSEXT; static ImageClass arg_image_class = IMAGE_SYSEXT;
@ -99,12 +102,17 @@ static const struct {
} }
}; };
static int is_our_mount_point(const char *p) { static int is_our_mount_point(
ImageClass image_class,
const char *p) {
_cleanup_free_ char *buf = NULL, *f = NULL; _cleanup_free_ char *buf = NULL, *f = NULL;
struct stat st; struct stat st;
dev_t dev; dev_t dev;
int r; int r;
assert(p);
r = path_is_mount_point(p, NULL, 0); r = path_is_mount_point(p, NULL, 0);
if (r == -ENOENT) { if (r == -ENOENT) {
log_debug_errno(r, "Hierarchy '%s' doesn't exist.", p); log_debug_errno(r, "Hierarchy '%s' doesn't exist.", p);
@ -125,21 +133,21 @@ static int is_our_mount_point(const char *p) {
* confused if people tar up one of our merged trees and untar them elsewhere where we might mistake * confused if people tar up one of our merged trees and untar them elsewhere where we might mistake
* them for a live sysext tree. */ * them for a live sysext tree. */
f = path_join(p, image_class_info[arg_image_class].dot_directory_name, "dev"); f = path_join(p, image_class_info[image_class].dot_directory_name, "dev");
if (!f) if (!f)
return log_oom(); return log_oom();
r = read_one_line_file(f, &buf); r = read_one_line_file(f, &buf);
if (r == -ENOENT) { if (r == -ENOENT) {
log_debug("Hierarchy '%s' does not carry a %s/dev file, not a merged tree.", p, image_class_info[arg_image_class].dot_directory_name); log_debug("Hierarchy '%s' does not carry a %s/dev file, not a merged tree.", p, image_class_info[image_class].dot_directory_name);
return false; return false;
} }
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '%s/dev': %m", p, image_class_info[arg_image_class].dot_directory_name); return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '%s/dev': %m", p, image_class_info[image_class].dot_directory_name);
r = parse_devnum(buf, &dev); r = parse_devnum(buf, &dev);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse device major/minor stored in '%s/dev' file on '%s': %m", image_class_info[arg_image_class].dot_directory_name, p); return log_error_errno(r, "Failed to parse device major/minor stored in '%s/dev' file on '%s': %m", image_class_info[image_class].dot_directory_name, p);
if (lstat(p, &st) < 0) if (lstat(p, &st) < 0)
return log_error_errno(r, "Failed to stat %s: %m", p); return log_error_errno(r, "Failed to stat %s: %m", p);
@ -152,15 +160,18 @@ static int is_our_mount_point(const char *p) {
return true; return true;
} }
static int need_reload(void) { static int need_reload(
/* Parse the mounted images to find out if we need ImageClass image_class,
to reload the daemon. */ char **hierarchies,
bool no_reload) {
/* Parse the mounted images to find out if we need to reload the daemon. */
int r; int r;
if (arg_no_reload) if (no_reload)
return false; return false;
STRV_FOREACH(p, arg_hierarchies) { STRV_FOREACH(p, hierarchies) {
_cleanup_free_ char *f = NULL, *buf = NULL, *resolved = NULL; _cleanup_free_ char *f = NULL, *buf = NULL, *resolved = NULL;
_cleanup_strv_free_ char **mounted_extensions = NULL; _cleanup_strv_free_ char **mounted_extensions = NULL;
@ -174,13 +185,13 @@ static int need_reload(void) {
continue; continue;
} }
r = is_our_mount_point(resolved); r = is_our_mount_point(image_class, resolved);
if (r < 0) if (r < 0)
return r; return r;
if (!r) if (!r)
continue; continue;
f = path_join(resolved, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural); f = path_join(resolved, image_class_info[image_class].dot_directory_name, image_class_info[image_class].short_identifier_plural);
if (!f) if (!f)
return log_oom(); return log_oom();
@ -197,7 +208,7 @@ static int need_reload(void) {
const char *extension_reload_manager = NULL; const char *extension_reload_manager = NULL;
int b; int b;
r = load_extension_release_pairs(arg_root, arg_image_class, *extension, /* relax_extension_release_check */ true, &extension_release); r = load_extension_release_pairs(arg_root, image_class, *extension, /* relax_extension_release_check */ true, &extension_release);
if (r < 0) { if (r < 0) {
log_debug_errno(r, "Failed to parse extension-release metadata of %s, ignoring: %m", *extension); log_debug_errno(r, "Failed to parse extension-release metadata of %s, ignoring: %m", *extension);
continue; continue;
@ -233,14 +244,19 @@ static int daemon_reload(void) {
return bus_service_manager_reload(bus); return bus_service_manager_reload(bus);
} }
static int unmerge_hierarchy(const char *p) { static int unmerge_hierarchy(
ImageClass image_class,
const char *p) {
int r; int r;
assert(p);
for (;;) { for (;;) {
/* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break
* systems where /usr/ is a mount point of its own already. */ * systems where /usr/ is a mount point of its own already. */
r = is_our_mount_point(p); r = is_our_mount_point(image_class, p);
if (r < 0) if (r < 0)
return r; return r;
if (r == 0) if (r == 0)
@ -256,16 +272,20 @@ static int unmerge_hierarchy(const char *p) {
return 0; return 0;
} }
static int unmerge(void) { static int unmerge(
ImageClass image_class,
char **hierarchies,
bool no_reload) {
int r, ret = 0; int r, ret = 0;
bool need_to_reload; bool need_to_reload;
r = need_reload(); r = need_reload(image_class, hierarchies, no_reload);
if (r < 0) if (r < 0)
return r; return r;
need_to_reload = r > 0; need_to_reload = r > 0;
STRV_FOREACH(p, arg_hierarchies) { STRV_FOREACH(p, hierarchies) {
_cleanup_free_ char *resolved = NULL; _cleanup_free_ char *resolved = NULL;
r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL); r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
@ -281,7 +301,7 @@ static int unmerge(void) {
continue; continue;
} }
r = unmerge_hierarchy(resolved); r = unmerge_hierarchy(image_class, resolved);
if (r < 0 && ret == 0) if (r < 0 && ret == 0)
ret = r; ret = r;
} }
@ -304,7 +324,74 @@ static int verb_unmerge(int argc, char **argv, void *userdata) {
if (r == 0) if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged."); return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
return unmerge(); return unmerge(arg_image_class,
arg_hierarchies,
arg_no_reload);
}
static int parse_image_class_parameter(Varlink *link, const char *value, ImageClass *image_class, char ***hierarchies) {
_cleanup_strv_free_ char **h = NULL;
ImageClass c;
int r;
assert(link);
assert(image_class);
if (!value)
return 0;
c = image_class_from_string(value);
if (!IN_SET(c, IMAGE_SYSEXT, IMAGE_CONFEXT))
return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "class")));
if (hierarchies) {
r = parse_env_extension_hierarchies(&h, image_class_info[c].name_env);
if (r < 0)
return log_error_errno(r, "Failed to parse environment variable: %m");
strv_free_and_replace(*hierarchies, h);
}
*image_class = c;
return 0;
}
typedef struct MethodUnmergeParameters {
const char *class;
int no_reload;
} MethodUnmergeParameters;
static int vl_method_unmerge(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "class", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodUnmergeParameters, class), 0 },
{ "noReload", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodUnmergeParameters, no_reload), 0 },
{}
};
MethodUnmergeParameters p = {
.no_reload = -1,
};
_cleanup_strv_free_ char **hierarchies = NULL;
ImageClass image_class = arg_image_class;
int r;
assert(link);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
if (r < 0)
return r;
r = unmerge(image_class,
hierarchies ?: arg_hierarchies,
p.no_reload >= 0 ? p.no_reload : arg_no_reload);
if (r < 0)
return r;
return varlink_reply(link, NULL);
} }
static int verb_status(int argc, char **argv, void *userdata) { static int verb_status(int argc, char **argv, void *userdata) {
@ -332,7 +419,7 @@ static int verb_status(int argc, char **argv, void *userdata) {
goto inner_fail; goto inner_fail;
} }
r = is_our_mount_point(resolved); r = is_our_mount_point(arg_image_class, resolved);
if (r < 0) if (r < 0)
goto inner_fail; goto inner_fail;
if (r == 0) { if (r == 0) {
@ -388,6 +475,8 @@ static int verb_status(int argc, char **argv, void *userdata) {
} }
static int mount_overlayfs( static int mount_overlayfs(
ImageClass image_class,
int noexec,
const char *where, const char *where,
char **layers) { char **layers) {
@ -415,12 +504,12 @@ static int mount_overlayfs(
separator = true; separator = true;
} }
flags = image_class_info[arg_image_class].default_mount_flags; flags = image_class_info[image_class].default_mount_flags;
if (arg_noexec >= 0) if (noexec >= 0)
SET_FLAG(flags, MS_NOEXEC, arg_noexec); SET_FLAG(flags, MS_NOEXEC, noexec);
/* Now mount the actual overlayfs */ /* Now mount the actual overlayfs */
r = mount_nofollow_verbose(LOG_ERR, image_class_info[arg_image_class].short_identifier, where, "overlay", flags, options); r = mount_nofollow_verbose(LOG_ERR, image_class_info[image_class].short_identifier, where, "overlay", flags, options);
if (r < 0) if (r < 0)
return r; return r;
@ -428,7 +517,9 @@ static int mount_overlayfs(
} }
static int merge_hierarchy( static int merge_hierarchy(
ImageClass image_class,
const char *hierarchy, const char *hierarchy,
int noexec,
char **extensions, char **extensions,
char **paths, char **paths,
const char *meta_path, const char *meta_path,
@ -463,7 +554,7 @@ static int merge_hierarchy(
/* Let's generate a metadata file that lists all extensions we took into account for this /* Let's generate a metadata file that lists all extensions we took into account for this
* hierarchy. We include this in the final fs, to make things nicely discoverable and * hierarchy. We include this in the final fs, to make things nicely discoverable and
* recognizable. */ * recognizable. */
f = path_join(meta_path, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural); f = path_join(meta_path, image_class_info[image_class].dot_directory_name, image_class_info[image_class].short_identifier_plural);
if (!f) if (!f)
return log_oom(); return log_oom();
@ -519,7 +610,7 @@ static int merge_hierarchy(
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to make directory '%s': %m", overlay_path); return log_error_errno(r, "Failed to make directory '%s': %m", overlay_path);
r = mount_overlayfs(overlay_path, layers); r = mount_overlayfs(image_class, noexec, overlay_path, layers);
if (r < 0) if (r < 0)
return r; return r;
@ -536,7 +627,7 @@ static int merge_hierarchy(
return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path); return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path);
free(f); free(f);
f = path_join(meta_path, image_class_info[arg_image_class].dot_directory_name, "dev"); f = path_join(meta_path, image_class_info[image_class].dot_directory_name, "dev");
if (!f) if (!f)
return log_oom(); return log_oom();
@ -574,7 +665,14 @@ static const ImagePolicy *pick_image_policy(const Image *img) {
return image_class_info[img->class].default_image_policy; return image_class_info[img->class].default_image_policy;
} }
static int merge_subprocess(Hashmap *images, const char *workspace) { static int merge_subprocess(
ImageClass image_class,
char **hierarchies,
bool force,
int noexec,
Hashmap *images,
const char *workspace) {
_cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_api_level = NULL, *buf = NULL; _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_api_level = NULL, *buf = NULL;
_cleanup_strv_free_ char **extensions = NULL, **paths = NULL; _cleanup_strv_free_ char **extensions = NULL, **paths = NULL;
size_t n_extensions = 0; size_t n_extensions = 0;
@ -597,7 +695,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
* but let the kernel do that entirely automatically, once our namespace dies. Note that this file * but let the kernel do that entirely automatically, once our namespace dies. Note that this file
* system won't be visible to anyone but us, since we opened our own namespace and then made the * system won't be visible to anyone but us, since we opened our own namespace and then made the
* /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */ * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */
r = mount_nofollow_verbose(LOG_ERR, image_class_info[arg_image_class].short_identifier, workspace, "tmpfs", 0, "mode=0700"); r = mount_nofollow_verbose(LOG_ERR, image_class_info[image_class].short_identifier, workspace, "tmpfs", 0, "mode=0700");
if (r < 0) if (r < 0)
return r; return r;
@ -606,7 +704,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
arg_root, arg_root,
"ID", &host_os_release_id, "ID", &host_os_release_id,
"VERSION_ID", &host_os_release_version_id, "VERSION_ID", &host_os_release_version_id,
image_class_info[arg_image_class].level_env, &host_os_release_api_level); image_class_info[image_class].level_env, &host_os_release_api_level);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root)); return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root));
if (isempty(host_os_release_id)) if (isempty(host_os_release_id))
@ -618,7 +716,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
HASHMAP_FOREACH(img, images) { HASHMAP_FOREACH(img, images) {
_cleanup_free_ char *p = NULL; _cleanup_free_ char *p = NULL;
p = path_join(workspace, image_class_info[arg_image_class].short_identifier_plural, img->name); p = path_join(workspace, image_class_info[image_class].short_identifier_plural, img->name);
if (!p) if (!p)
return log_oom(); return log_oom();
@ -630,7 +728,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
case IMAGE_DIRECTORY: case IMAGE_DIRECTORY:
case IMAGE_SUBVOLUME: case IMAGE_SUBVOLUME:
if (!arg_force) { if (!force) {
r = extension_has_forbidden_content(p); r = extension_has_forbidden_content(p);
if (r < 0) if (r < 0)
return r; return r;
@ -672,7 +770,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
if (verity_settings.data_path) if (verity_settings.data_path)
flags |= DISSECT_IMAGE_NO_PARTITION_TABLE; flags |= DISSECT_IMAGE_NO_PARTITION_TABLE;
if (!arg_force) if (!force)
flags |= DISSECT_IMAGE_VALIDATE_OS_EXT; flags |= DISSECT_IMAGE_VALIDATE_OS_EXT;
r = loop_device_make_by_path( r = loop_device_make_by_path(
@ -718,7 +816,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
flags); flags);
if (r < 0 && r != -ENOMEDIUM) if (r < 0 && r != -ENOMEDIUM)
return r; return r;
if (r == -ENOMEDIUM && !arg_force) { if (r == -ENOMEDIUM && !force) {
n_ignored++; n_ignored++;
continue; continue;
} }
@ -732,7 +830,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
assert_not_reached(); assert_not_reached();
} }
if (arg_force) if (force)
log_debug("Force mode enabled, skipping version validation."); log_debug("Force mode enabled, skipping version validation.");
else { else {
r = extension_release_validate( r = extension_release_validate(
@ -741,8 +839,8 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
host_os_release_version_id, host_os_release_version_id,
host_os_release_api_level, host_os_release_api_level,
in_initrd() ? "initrd" : "system", in_initrd() ? "initrd" : "system",
image_extension_release(img, arg_image_class), image_extension_release(img, image_class),
arg_image_class); image_class);
if (r < 0) if (r < 0)
return r; return r;
if (r == 0) { if (r == 0) {
@ -787,7 +885,7 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k])); assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k]));
p = path_join(workspace, image_class_info[arg_image_class].short_identifier_plural, img->name); p = path_join(workspace, image_class_info[image_class].short_identifier_plural, img->name);
if (!p) if (!p)
return log_oom(); return log_oom();
@ -796,20 +894,20 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
/* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the
* underlying fs. */ * underlying fs. */
STRV_FOREACH(h, arg_hierarchies) { STRV_FOREACH(h, hierarchies) {
_cleanup_free_ char *resolved = NULL; _cleanup_free_ char *resolved = NULL;
r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL); r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h); return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
r = unmerge_hierarchy(resolved); r = unmerge_hierarchy(image_class, resolved);
if (r < 0) if (r < 0)
return r; return r;
} }
/* Create overlayfs mounts for all hierarchies */ /* Create overlayfs mounts for all hierarchies */
STRV_FOREACH(h, arg_hierarchies) { STRV_FOREACH(h, hierarchies) {
_cleanup_free_ char *meta_path = NULL, *overlay_path = NULL; _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL;
meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */ meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */
@ -820,13 +918,20 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
if (!overlay_path) if (!overlay_path)
return log_oom(); return log_oom();
r = merge_hierarchy(*h, extensions, paths, meta_path, overlay_path); r = merge_hierarchy(
image_class,
*h,
noexec,
extensions,
paths,
meta_path,
overlay_path);
if (r < 0) if (r < 0)
return r; return r;
} }
/* And move them all into place. This is where things appear in the host namespace */ /* And move them all into place. This is where things appear in the host namespace */
STRV_FOREACH(h, arg_hierarchies) { STRV_FOREACH(h, hierarchies) {
_cleanup_free_ char *p = NULL, *resolved = NULL; _cleanup_free_ char *p = NULL, *resolved = NULL;
p = path_join(workspace, "overlay", *h); p = path_join(workspace, "overlay", *h);
@ -859,7 +964,12 @@ static int merge_subprocess(Hashmap *images, const char *workspace) {
return 1; return 1;
} }
static int merge(Hashmap *images) { static int merge(ImageClass image_class,
char **hierarchies,
bool force,
bool no_reload,
int noexec,
Hashmap *images) {
pid_t pid; pid_t pid;
int r; int r;
@ -869,7 +979,7 @@ static int merge(Hashmap *images) {
if (r == 0) { if (r == 0) {
/* Child with its own mount namespace */ /* Child with its own mount namespace */
r = merge_subprocess(images, "/run/systemd/sysext"); r = merge_subprocess(image_class, hierarchies, force, noexec, images, "/run/systemd/sysext");
if (r < 0) if (r < 0)
_exit(EXIT_FAILURE); _exit(EXIT_FAILURE);
@ -886,7 +996,7 @@ static int merge(Hashmap *images) {
if (r == 123) /* exit code 123 means: didn't do anything */ if (r == 123) /* exit code 123 means: didn't do anything */
return 0; return 0;
r = need_reload(); r = need_reload(image_class, hierarchies, no_reload);
if (r < 0) if (r < 0)
return r; return r;
if (r > 0) { if (r > 0) {
@ -898,7 +1008,9 @@ static int merge(Hashmap *images) {
return 1; return 1;
} }
static int image_discover_and_read_metadata(Hashmap **ret_images) { static int image_discover_and_read_metadata(
ImageClass image_class,
Hashmap **ret_images) {
_cleanup_hashmap_free_ Hashmap *images = NULL; _cleanup_hashmap_free_ Hashmap *images = NULL;
Image *img; Image *img;
int r; int r;
@ -909,12 +1021,12 @@ static int image_discover_and_read_metadata(Hashmap **ret_images) {
if (!images) if (!images)
return log_oom(); return log_oom();
r = image_discover(arg_image_class, arg_root, images); r = image_discover(image_class, arg_root, images);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to discover images: %m"); return log_error_errno(r, "Failed to discover images: %m");
HASHMAP_FOREACH(img, images) { HASHMAP_FOREACH(img, images) {
r = image_read_metadata(img, image_class_info[arg_image_class].default_image_policy); r = image_read_metadata(img, image_class_info[image_class].default_image_policy);
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name); return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
} }
@ -924,23 +1036,17 @@ static int image_discover_and_read_metadata(Hashmap **ret_images) {
return 0; return 0;
} }
static int verb_merge(int argc, char **argv, void *userdata) { static int look_for_merged_hierarchies(
_cleanup_hashmap_free_ Hashmap *images = NULL; ImageClass image_class,
char **hierarchies,
const char **ret_which) {
int r; int r;
r = have_effective_cap(CAP_SYS_ADMIN); assert(ret_which);
if (r < 0)
return log_error_errno(r, "Failed to check if we have enough privileges: %m");
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
r = image_discover_and_read_metadata(&images);
if (r < 0)
return r;
/* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find
* things are already merged...) */ * things are already merged...) */
STRV_FOREACH(p, arg_hierarchies) { STRV_FOREACH(p, hierarchies) {
_cleanup_free_ char *resolved = NULL; _cleanup_free_ char *resolved = NULL;
r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL); r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
@ -951,19 +1057,22 @@ static int verb_merge(int argc, char **argv, void *userdata) {
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p); return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
r = is_our_mount_point(resolved); r = is_our_mount_point(image_class, resolved);
if (r < 0) if (r < 0)
return r; return r;
if (r > 0) if (r > 0) {
return log_error_errno(SYNTHETIC_ERRNO(EBUSY), *ret_which = *p;
"Hierarchy '%s' is already merged.", *p); return 1;
}
} }
return merge(images); *ret_which = NULL;
return 0;
} }
static int verb_refresh(int argc, char **argv, void *userdata) { static int verb_merge(int argc, char **argv, void *userdata) {
_cleanup_hashmap_free_ Hashmap *images = NULL; _cleanup_hashmap_free_ Hashmap *images = NULL;
const char *which;
int r; int r;
r = have_effective_cap(CAP_SYS_ADMIN); r = have_effective_cap(CAP_SYS_ADMIN);
@ -972,19 +1081,123 @@ static int verb_refresh(int argc, char **argv, void *userdata) {
if (r == 0) if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged."); return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
r = image_discover_and_read_metadata(&images); r = image_discover_and_read_metadata(arg_image_class, &images);
if (r < 0) if (r < 0)
return r; return r;
r = merge(images); /* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it r = look_for_merged_hierarchies(arg_image_class, arg_hierarchies, &which);
* does so it implicitly unmounts any overlayfs placed there before. Returns == 0 if (r < 0)
* if it did nothing, i.e. no extension images found. In this case the old return r;
* overlayfs remains in place if there was one. */ if (r > 0)
return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Hierarchy '%s' is already merged.", which);
return merge(arg_image_class,
arg_hierarchies,
arg_force,
arg_no_reload,
arg_noexec,
images);
}
typedef struct MethodMergeParameters {
const char *class;
int force;
int no_reload;
int noexec;
} MethodMergeParameters;
static int parse_merge_parameters(Varlink *link, JsonVariant *parameters, MethodMergeParameters *p) {
static const JsonDispatch dispatch_table[] = {
{ "class", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodMergeParameters, class), 0 },
{ "force", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodMergeParameters, force), 0 },
{ "noReload", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodMergeParameters, no_reload), 0 },
{ "noexec", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodMergeParameters, noexec), 0 },
{}
};
int r;
assert(link);
assert(parameters);
assert(p);
r = json_dispatch(parameters, dispatch_table, NULL, 0, p);
if (r < 0)
return r;
return 0;
}
static int vl_method_merge(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
_cleanup_hashmap_free_ Hashmap *images = NULL;
MethodMergeParameters p = {
.force = -1,
.no_reload = -1,
.noexec = -1,
};
_cleanup_strv_free_ char **hierarchies = NULL;
ImageClass image_class = arg_image_class;
int r;
assert(link);
r = parse_merge_parameters(link, parameters, &p);
if (r < 0)
return r;
r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
if (r < 0)
return r;
r = image_discover_and_read_metadata(image_class, &images);
if (r < 0)
return r;
const char *which;
r = look_for_merged_hierarchies(
image_class,
hierarchies ?: arg_hierarchies,
&which);
if (r < 0)
return r;
if (r > 0)
return varlink_errorb(link, "io.systemd.sysext.AlreadyMerged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("hierarchy", which)));
r = merge(image_class,
hierarchies ?: arg_hierarchies,
p.force >= 0 ? p.force : arg_force,
p.no_reload >= 0 ? p.no_reload : arg_no_reload,
p.noexec >= 0 ? p.noexec : arg_noexec,
images);
if (r < 0)
return r;
return varlink_reply(link, NULL);
}
static int refresh(
ImageClass image_class,
char **hierarchies,
bool force,
bool no_reload,
int noexec) {
_cleanup_hashmap_free_ Hashmap *images = NULL;
int r;
r = image_discover_and_read_metadata(image_class, &images);
if (r < 0)
return r;
/* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it does so it
* implicitly unmounts any overlayfs placed there before. Returns == 0 if it did nothing, i.e. no
* extension images found. In this case the old overlayfs remains in place if there was one. */
r = merge(image_class, hierarchies, force, no_reload, noexec, images);
if (r < 0) if (r < 0)
return r; return r;
if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having
* called there's a guarantee that the merge status matches the installed extensions. */ * called there's a guarantee that the merge status matches the installed extensions. */
r = unmerge(); r = unmerge(image_class, hierarchies, no_reload);
/* Net result here is that: /* Net result here is that:
* *
@ -1002,6 +1215,54 @@ static int verb_refresh(int argc, char **argv, void *userdata) {
return 0; return 0;
} }
static int verb_refresh(int argc, char **argv, void *userdata) {
int r;
r = have_effective_cap(CAP_SYS_ADMIN);
if (r < 0)
return log_error_errno(r, "Failed to check if we have enough privileges: %m");
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
return refresh(arg_image_class,
arg_hierarchies,
arg_force,
arg_no_reload,
arg_noexec);
}
static int vl_method_refresh(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
MethodMergeParameters p = {
.force = -1,
.no_reload = -1,
.noexec = -1,
};
_cleanup_strv_free_ char **hierarchies = NULL;
ImageClass image_class = arg_image_class;
int r;
assert(link);
r = parse_merge_parameters(link, parameters, &p);
if (r < 0)
return r;
r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
if (r < 0)
return r;
r = refresh(image_class,
hierarchies ?: arg_hierarchies,
p.force >= 0 ? p.force : arg_force,
p.no_reload >= 0 ? p.no_reload : arg_no_reload,
p.noexec >= 0 ? p.noexec : arg_noexec);
if (r < 0)
return r;
return varlink_reply(link, NULL);
}
static int verb_list(int argc, char **argv, void *userdata) { static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_hashmap_free_ Hashmap *images = NULL; _cleanup_hashmap_free_ Hashmap *images = NULL;
_cleanup_(table_unrefp) Table *t = NULL; _cleanup_(table_unrefp) Table *t = NULL;
@ -1041,6 +1302,63 @@ static int verb_list(int argc, char **argv, void *userdata) {
return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
} }
typedef struct MethodListParameters {
const char *class;
} MethodListParameters;
static int vl_method_list(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "class", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodListParameters, class), 0 },
{}
};
MethodListParameters p = {
};
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_hashmap_free_ Hashmap *images = NULL;
ImageClass image_class = arg_image_class;
Image *img;
int r;
assert(link);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
r = parse_image_class_parameter(link, p.class, &image_class, NULL);
if (r < 0)
return r;
images = hashmap_new(&image_hash_ops);
if (!images)
return -ENOMEM;
r = image_discover(image_class, arg_root, images);
if (r < 0)
return r;
HASHMAP_FOREACH(img, images) {
if (v) {
/* Send previous item with more=true */
r = varlink_notify(link, v);
if (r < 0)
return r;
}
v = json_variant_unref(v);
r = image_to_json(img, &v);
if (r < 0)
return r;
}
if (v) /* Send final item with more=false */
return varlink_reply(link, v);
return varlink_error(link, "io.systemd.sysext.NoImagesFound", NULL);
}
static int verb_help(int argc, char **argv, void *userdata) { static int verb_help(int argc, char **argv, void *userdata) {
_cleanup_free_ char *link = NULL; _cleanup_free_ char *link = NULL;
int r; int r;
@ -1176,6 +1494,12 @@ static int parse_argv(int argc, char *argv[]) {
assert_not_reached(); assert_not_reached();
} }
r = varlink_invocation(VARLINK_ALLOW_ACCEPT);
if (r < 0)
return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
if (r > 0)
arg_varlink = true;
return 1; return 1;
} }
@ -1196,10 +1520,11 @@ static int sysext_main(int argc, char *argv[]) {
static int run(int argc, char *argv[]) { static int run(int argc, char *argv[]) {
int r; int r;
arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT;
log_setup(); log_setup();
arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT;
r = parse_argv(argc, argv); r = parse_argv(argc, argv);
if (r <= 0) if (r <= 0)
return r; return r;
@ -1211,6 +1536,37 @@ static int run(int argc, char *argv[]) {
if (r < 0) if (r < 0)
return log_error_errno(r, "Failed to parse environment variable: %m"); return log_error_errno(r, "Failed to parse environment variable: %m");
if (arg_varlink) {
_cleanup_(varlink_server_unrefp) VarlinkServer *varlink_server = NULL;
/* Invocation as Varlink service */
r = varlink_server_new(&varlink_server, VARLINK_SERVER_ROOT_ONLY);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
r = varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_sysext);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = 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);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = varlink_server_loop_auto(varlink_server);
if (r == -EPERM)
return log_error_errno(r, "Invoked by unprivileged Varlink peer, refusing.");
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return EXIT_SUCCESS;
}
return sysext_main(argc, argv); return sysext_main(argc, argv);
} }

View File

@ -15,6 +15,7 @@
#include "varlink-io.systemd.Resolve.h" #include "varlink-io.systemd.Resolve.h"
#include "varlink-io.systemd.UserDatabase.h" #include "varlink-io.systemd.UserDatabase.h"
#include "varlink-io.systemd.oom.h" #include "varlink-io.systemd.oom.h"
#include "varlink-io.systemd.sysext.h"
#include "varlink-org.varlink.service.h" #include "varlink-org.varlink.service.h"
static VARLINK_DEFINE_ENUM_TYPE( static VARLINK_DEFINE_ENUM_TYPE(
@ -137,6 +138,8 @@ TEST(parse_format) {
print_separator(); print_separator();
test_parse_format_one(&vl_interface_io_systemd_PCRExtend); test_parse_format_one(&vl_interface_io_systemd_PCRExtend);
print_separator(); print_separator();
test_parse_format_one(&vl_interface_io_systemd_sysext);
print_separator();
test_parse_format_one(&vl_interface_xyz_test); test_parse_format_one(&vl_interface_xyz_test);
} }

View File

@ -457,6 +457,16 @@ test ! -e /usr/lib/systemd/system/some_file
systemd-sysext unmerge systemd-sysext unmerge
rmdir /etc/extensions/app-nodistro rmdir /etc/extensions/app-nodistro
# Similar, but go via varlink
varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.List '{}'
(! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file )
varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Merge '{}'
grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file
varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Refresh '{}'
grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file
varlinkctl call /run/systemd/io.systemd.sysext io.systemd.sysext.Unmerge '{}'
(! grep -q -F "MARKER=1" /usr/lib/systemd/system/some_file )
# Check that extensions cannot contain os-release # Check that extensions cannot contain os-release
mkdir -p /run/extensions/app-reject/usr/lib/{extension-release.d/,systemd/system} mkdir -p /run/extensions/app-reject/usr/lib/{extension-release.d/,systemd/system}
echo "ID=_any" >/run/extensions/app-reject/usr/lib/extension-release.d/extension-release.app-reject echo "ID=_any" >/run/extensions/app-reject/usr/lib/extension-release.d/extension-release.app-reject

View File

@ -529,6 +529,15 @@ units = [
'file' : 'systemd-sysext.service', 'file' : 'systemd-sysext.service',
'conditions' : ['ENABLE_SYSEXT'], 'conditions' : ['ENABLE_SYSEXT'],
}, },
{
'file' : 'systemd-sysext.socket',
'conditions' : ['ENABLE_SYSEXT'],
'symlinks' : ['sockets.target.wants/'],
},
{
'file' : 'systemd-sysext@.service',
'conditions' : ['ENABLE_SYSEXT'],
},
{ {
'file' : 'systemd-sysupdate-reboot.service.in', 'file' : 'systemd-sysupdate-reboot.service.in',
'conditions' : ['ENABLE_SYSUPDATE'], 'conditions' : ['ENABLE_SYSUPDATE'],

View File

@ -0,0 +1,25 @@
# 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 Image Management (Varlink)
Documentation=man:systemd-sysext(8)
DefaultDependencies=no
After=local-fs.target
Before=sockets.target
ConditionCapability=CAP_SYS_ADMIN
[Socket]
ListenStream=/run/systemd/io.systemd.sysext
FileDescriptorName=varlink
SocketMode=0600
Accept=yes
[Install]
WantedBy=sockets.target

View File

@ -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=System Extension Image Management (Varlink)
Documentation=man:systemd-sysext(8)
DefaultDependencies=no
After=local-fs.target
Conflicts=shutdown.target initrd-switch-root.target
Before=shutdown.target initrd-switch-root.target
[Service]
Environment=LISTEN_FDNAMES=varlink
ExecStart=-systemd-sysext