summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--man/org.freedesktop.systemd1.xml17
-rw-r--r--man/systemd.exec.xml28
-rw-r--r--src/core/dbus-execute.c6
-rw-r--r--src/core/execute.c7
-rw-r--r--src/core/execute.h1
-rw-r--r--src/core/load-fragment-gperf.gperf.in1
-rw-r--r--src/core/namespace.c130
-rw-r--r--src/core/namespace.h1
-rw-r--r--src/shared/bus-unit-util.c1
-rw-r--r--src/test/test-namespace.c1
-rw-r--r--src/test/test-ns.c1
-rw-r--r--test/fuzz/fuzz-unit-file/directives-all.service1
-rw-r--r--test/fuzz/fuzz-unit-file/directives.mount1
-rw-r--r--test/fuzz/fuzz-unit-file/directives.service1
-rw-r--r--test/fuzz/fuzz-unit-file/directives.socket1
-rw-r--r--test/fuzz/fuzz-unit-file/directives.swap1
-rwxr-xr-xtest/units/testsuite-50.sh30
17 files changed, 216 insertions, 13 deletions
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index bd69a00b57..97a8c98b39 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -2682,6 +2682,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RootVerity = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly as ExtensionDirectories = ['...', ...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sba(ss)) ExtensionImages = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ssba(ss)) MountImages = [...];
@@ -3827,6 +3829,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
<variablelist class="dbus-property" generated="True" extra-ref="RootVerity"/>
+ <variablelist class="dbus-property" generated="True" extra-ref="ExtensionDirectories"/>
+
<variablelist class="dbus-property" generated="True" extra-ref="ExtensionImages"/>
<variablelist class="dbus-property" generated="True" extra-ref="MountImages"/>
@@ -4185,6 +4189,7 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
<varname>RootHashSignature</varname>
<varname>MountImages</varname>
<varname>ExtensionImages</varname>
+ <varname>ExtensionDirectories</varname>
see systemd.exec(5) for their meaning.</para>
<para><varname>MemoryAvailable</varname> indicates how much unused memory is available to the unit before
@@ -4559,6 +4564,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RootVerity = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly as ExtensionDirectories = ['...', ...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sba(ss)) ExtensionImages = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ssba(ss)) MountImages = [...];
@@ -5722,6 +5729,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
<variablelist class="dbus-property" generated="True" extra-ref="RootVerity"/>
+ <variablelist class="dbus-property" generated="True" extra-ref="ExtensionDirectories"/>
+
<variablelist class="dbus-property" generated="True" extra-ref="ExtensionImages"/>
<variablelist class="dbus-property" generated="True" extra-ref="MountImages"/>
@@ -6344,6 +6353,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RootVerity = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly as ExtensionDirectories = ['...', ...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sba(ss)) ExtensionImages = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ssba(ss)) MountImages = [...];
@@ -7353,6 +7364,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
<variablelist class="dbus-property" generated="True" extra-ref="RootVerity"/>
+ <variablelist class="dbus-property" generated="True" extra-ref="ExtensionDirectories"/>
+
<variablelist class="dbus-property" generated="True" extra-ref="ExtensionImages"/>
<variablelist class="dbus-property" generated="True" extra-ref="MountImages"/>
@@ -8102,6 +8115,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s RootVerity = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly as ExtensionDirectories = ['...', ...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(sba(ss)) ExtensionImages = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly a(ssba(ss)) MountImages = [...];
@@ -9083,6 +9098,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
<variablelist class="dbus-property" generated="True" extra-ref="RootVerity"/>
+ <variablelist class="dbus-property" generated="True" extra-ref="ExtensionDirectories"/>
+
<variablelist class="dbus-property" generated="True" extra-ref="ExtensionImages"/>
<variablelist class="dbus-property" generated="True" extra-ref="MountImages"/>
diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index 079ff14aea..36a884c9f4 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -459,6 +459,34 @@
<xi:include href="system-only.xml" xpointer="singular"/></listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><varname>ExtensionDirectories=</varname></term>
+
+ <listitem><para>This setting is similar to <varname>BindReadOnlyPaths=</varname> in that it mounts a file
+ system hierarchy from a directory, but instead of providing a destination path, an overlay will be set
+ up. This option expects a whitespace separated list of source directories.</para>
+
+ <para>A read-only OverlayFS will be set up on top of <filename>/usr/</filename> and
+ <filename>/opt/</filename> hierarchies. The order in which the directories are listed will determine
+ the order in which the overlay is laid down: directories specified first to last will result in overlayfs
+ layers bottom to top.</para>
+
+ <para>Each directory listed in <varname>ExtensionDirectories=</varname> may be prefixed with <literal>-</literal>,
+ in which case it will be ignored when its source path does not exist. Any mounts created with this option are
+ specific to the unit, and are not visible in the host's mount table.</para>
+
+ <para>These settings may be used more than once, each usage appends to the unit's list of directories
+ paths. If the empty string is assigned, the entire list of mount paths defined prior to this is
+ reset.</para>
+
+ <para>Each directory must contain a <filename>/usr/lib/extension-release.d/extension-release.IMAGE</filename>
+ file, with the appropriate metadata which matches <varname>RootImage=</varname>/<varname>RootDirectory=</varname>
+ or the host. See:
+ <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
+
+ <xi:include href="system-only.xml" xpointer="singular"/></listitem>
+ </varlistentry>
</variablelist>
</refsect1>
diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c
index 5c499e5d06..8a8d9c9b2e 100644
--- a/src/core/dbus-execute.c
+++ b/src/core/dbus-execute.c
@@ -1204,6 +1204,7 @@ const sd_bus_vtable bus_exec_vtable[] = {
SD_BUS_PROPERTY("RootHashSignature", "ay", property_get_root_hash_sig, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RootHashSignaturePath", "s", NULL, offsetof(ExecContext, root_hash_sig_path), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RootVerity", "s", NULL, offsetof(ExecContext, root_verity), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("ExtensionDirectories", "as", NULL, offsetof(ExecContext, extension_directories), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ExtensionImages", "a(sba(ss))", property_get_extension_images, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("MountImages", "a(ssba(ss))", property_get_mount_images, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("OOMScoreAdjust", "i", property_get_oom_score_adjust, 0, SD_BUS_VTABLE_PROPERTY_CONST),
@@ -3261,7 +3262,8 @@ int bus_exec_context_set_transient_property(
return 1;
} else if (STR_IN_SET(name, "ReadWriteDirectories", "ReadOnlyDirectories", "InaccessibleDirectories",
- "ReadWritePaths", "ReadOnlyPaths", "InaccessiblePaths", "ExecPaths", "NoExecPaths")) {
+ "ReadWritePaths", "ReadOnlyPaths", "InaccessiblePaths", "ExecPaths", "NoExecPaths",
+ "ExtensionDirectories")) {
_cleanup_strv_free_ char **l = NULL;
char ***dirs;
char **p;
@@ -3291,6 +3293,8 @@ int bus_exec_context_set_transient_property(
dirs = &c->exec_paths;
else if (streq(name, "NoExecPaths"))
dirs = &c->no_exec_paths;
+ else if (streq(name, "ExtensionDirectories"))
+ dirs = &c->extension_directories;
else /* "InaccessiblePaths" */
dirs = &c->inaccessible_paths;
diff --git a/src/core/execute.c b/src/core/execute.c
index eb25c98925..78c8d966df 100644
--- a/src/core/execute.c
+++ b/src/core/execute.c
@@ -2065,6 +2065,9 @@ bool exec_needs_mount_namespace(
if (context->n_extension_images > 0)
return true;
+ if (!strv_isempty(context->extension_directories))
+ return true;
+
if (!IN_SET(context->mount_flags, 0, MS_SHARED))
return true;
@@ -3566,6 +3569,7 @@ static int apply_mount_namespace(
context->root_verity,
context->extension_images,
context->n_extension_images,
+ context->extension_directories,
propagate_dir,
incoming_dir,
root_dir || root_image ? params->notify_socket : NULL,
@@ -5244,6 +5248,7 @@ void exec_context_done(ExecContext *c) {
c->root_hash_sig_path = mfree(c->root_hash_sig_path);
c->root_verity = mfree(c->root_verity);
c->extension_images = mount_image_free_many(c->extension_images, &c->n_extension_images);
+ c->extension_directories = strv_free(c->extension_directories);
c->tty_path = mfree(c->tty_path);
c->syslog_identifier = mfree(c->syslog_identifier);
c->user = mfree(c->user);
@@ -6120,6 +6125,8 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
strempty(o->options));
fprintf(f, "\n");
}
+
+ strv_dump(f, prefix, "ExtensionDirectories", c->extension_directories);
}
bool exec_context_maintains_privileges(const ExecContext *c) {
diff --git a/src/core/execute.h b/src/core/execute.h
index 805e9b4765..4aff50b442 100644
--- a/src/core/execute.h
+++ b/src/core/execute.h
@@ -273,6 +273,7 @@ struct ExecContext {
size_t n_mount_images;
MountImage *extension_images;
size_t n_extension_images;
+ char **extension_directories;
uint64_t capability_bounding_set;
uint64_t capability_ambient_set;
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index deea540e10..cc04393c1e 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -9,6 +9,7 @@
{{type}}.RootHash, config_parse_exec_root_hash, 0, offsetof({{type}}, exec_context)
{{type}}.RootHashSignature, config_parse_exec_root_hash_sig, 0, offsetof({{type}}, exec_context)
{{type}}.RootVerity, config_parse_unit_path_printf, true, offsetof({{type}}, exec_context.root_verity)
+{{type}}.ExtensionDirectories, config_parse_namespace_path_strv, 0, offsetof({{type}}, exec_context.extension_directories)
{{type}}.ExtensionImages, config_parse_extension_images, 0, offsetof({{type}}, exec_context)
{{type}}.MountImages, config_parse_mount_images, 0, offsetof({{type}}, exec_context)
{{type}}.User, config_parse_user_group_compat, 0, offsetof({{type}}, exec_context.user)
diff --git a/src/core/namespace.c b/src/core/namespace.c
index ecbd23833c..088cb09ac9 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -63,6 +63,7 @@ typedef enum MountMode {
EXEC,
TMPFS,
RUN,
+ EXTENSION_DIRECTORIES, /* Bind-mounted outside the root directory, and used by subsequent mounts */
EXTENSION_IMAGES, /* Mounted outside the root directory, and used by subsequent mounts */
MQUEUEFS,
READWRITE_IMPLICIT, /* Should have the lowest priority. */
@@ -408,22 +409,23 @@ static int append_mount_images(MountEntry **p, const MountImage *mount_images, s
return 0;
}
-static int append_extension_images(
+static int append_extensions(
MountEntry **p,
const char *root,
const char *extension_dir,
char **hierarchies,
const MountImage *mount_images,
- size_t n) {
+ size_t n,
+ char **extension_directories) {
_cleanup_strv_free_ char **overlays = NULL;
- char **hierarchy;
+ char **hierarchy, **extension_directory;
int r;
assert(p);
assert(extension_dir);
- if (n == 0)
+ if (n == 0 && strv_isempty(extension_directories))
return 0;
/* Prepare a list of overlays, that will have as each element a string suitable for being
@@ -482,6 +484,62 @@ static int append_extension_images(
};
}
+ /* Secondly, extend the lowerdir= parameters with each ExtensionDirectory.
+ * Bind mount them in the same location as the ExtensionImages, so that we
+ * can check that they are valid trees (extension-release.d). */
+ STRV_FOREACH(extension_directory, extension_directories) {
+ _cleanup_free_ char *mount_point = NULL, *source = NULL;
+ const char *e = *extension_directory;
+ bool ignore_enoent = false;
+
+ /* Pick up the counter where the ExtensionImages left it. */
+ r = asprintf(&mount_point, "%s/%zu", extension_dir, n++);
+ if (r < 0)
+ return -ENOMEM;
+
+ /* Look for any prefixes */
+ if (startswith(e, "-")) {
+ e++;
+ ignore_enoent = true;
+ }
+ /* Ignore this for now */
+ if (startswith(e, "+"))
+ e++;
+
+ source = strdup(e);
+ if (!source)
+ return -ENOMEM;
+
+ for (size_t j = 0; hierarchies && hierarchies[j]; ++j) {
+ _cleanup_free_ char *prefixed_hierarchy = NULL, *escaped = NULL, *lowerdir = NULL;
+
+ prefixed_hierarchy = path_join(mount_point, hierarchies[j]);
+ if (!prefixed_hierarchy)
+ return -ENOMEM;
+
+ escaped = shell_escape(prefixed_hierarchy, ",:");
+ if (!escaped)
+ return -ENOMEM;
+
+ /* Note that lowerdir= parameters are in 'reverse' order, so the
+ * top-most directory in the overlay comes first in the list. */
+ lowerdir = strjoin(escaped, ":", overlays[j]);
+ if (!lowerdir)
+ return -ENOMEM;
+
+ free_and_replace(overlays[j], lowerdir);
+ }
+
+ *((*p)++) = (MountEntry) {
+ .path_malloc = TAKE_PTR(mount_point),
+ .source_const = TAKE_PTR(source),
+ .mode = EXTENSION_DIRECTORIES,
+ .ignore = ignore_enoent,
+ .has_prefix = true,
+ .read_only = true,
+ };
+ }
+
/* Then, for each hierarchy, prepare an overlay with the list of lowerdir= strings
* set up earlier. */
for (size_t i = 0; hierarchies && hierarchies[i]; ++i) {
@@ -605,11 +663,14 @@ static int append_protect_system(MountEntry **p, ProtectSystem protect_system, b
static int mount_path_compare(const MountEntry *a, const MountEntry *b) {
int d;
- /* EXTENSION_IMAGES will be used by other mounts as a base, so sort them first
+ /* ExtensionImages/Directories will be used by other mounts as a base, so sort them first
* regardless of the prefix - they are set up in the propagate directory anyway */
d = -CMP(a->mode == EXTENSION_IMAGES, b->mode == EXTENSION_IMAGES);
if (d != 0)
return d;
+ d = -CMP(a->mode == EXTENSION_DIRECTORIES, b->mode == EXTENSION_DIRECTORIES);
+ if (d != 0)
+ return d;
/* If the paths are not equal, then order prefixes first */
d = path_compare(mount_entry_path(a), mount_entry_path(b));
@@ -757,8 +818,8 @@ static void drop_outside_root(const char *root_directory, MountEntry *m, size_t
for (f = m, t = m; f < m + *n; f++) {
- /* ExtensionImages bases are opened in /run/systemd/unit-extensions on the host */
- if (f->mode != EXTENSION_IMAGES && !path_startswith(mount_entry_path(f), root_directory)) {
+ /* ExtensionImages/Directories bases are opened in /run/systemd/unit-extensions on the host */
+ if (!IN_SET(f->mode, EXTENSION_IMAGES, EXTENSION_DIRECTORIES) && !path_startswith(mount_entry_path(f), root_directory)) {
log_debug("%s is outside of root directory.", mount_entry_path(f));
mount_entry_done(f);
continue;
@@ -1296,6 +1357,47 @@ static int apply_one_mount(
what = mount_entry_path(m);
break;
+ case EXTENSION_DIRECTORIES: {
+ _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL,
+ *host_os_release_sysext_level = NULL, *extension_name = NULL;
+ _cleanup_strv_free_ char **extension_release = NULL;
+
+ r = path_extract_filename(mount_entry_source(m), &extension_name);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to extract extension name from %s: %m", mount_entry_source(m));
+
+ r = parse_os_release(
+ empty_to_root(root_directory),
+ "ID", &host_os_release_id,
+ "VERSION_ID", &host_os_release_version_id,
+ "SYSEXT_LEVEL", &host_os_release_sysext_level,
+ NULL);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(root_directory));
+ if (isempty(host_os_release_id))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "'ID' field not found or empty in 'os-release' data of OS tree '%s': %m", empty_to_root(root_directory));
+
+ r = load_extension_release_pairs(mount_entry_source(m), extension_name, &extension_release);
+ if (r == -ENOENT && m->ignore)
+ return 0;
+ if (r < 0)
+ return log_debug_errno(r, "Failed to parse directory %s extension-release metadata: %m", extension_name);
+
+ r = extension_release_validate(
+ extension_name,
+ host_os_release_id,
+ host_os_release_version_id,
+ host_os_release_sysext_level,
+ /* host_sysext_scope */ NULL, /* Leave empty, we need to accept both system and portable */
+ extension_release);
+ if (r == 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Directory %s extension-release metadata does not match the root's", extension_name);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to compare directory %s extension-release metadata with the root's os-release: %m", extension_name);
+
+ _fallthrough_;
+ }
+
case BIND_MOUNT:
rbind = false;
@@ -1525,6 +1627,7 @@ static size_t namespace_calculate_mounts(
size_t n_temporary_filesystems,
size_t n_mount_images,
size_t n_extension_images,
+ size_t n_extension_directories,
size_t n_hierarchies,
const char* tmp_dir,
const char* var_tmp_dir,
@@ -1559,7 +1662,8 @@ static size_t namespace_calculate_mounts(
strv_length(empty_directories) +
n_bind_mounts +
n_mount_images +
- (n_extension_images > 0 ? n_hierarchies + n_extension_images : 0) + /* Mount each image plus an overlay per hierarchy */
+ (n_extension_images > 0 || n_extension_directories > 0 ? /* Mount each image and directory plus an overlay per hierarchy */
+ n_hierarchies + n_extension_images + n_extension_directories: 0) +
n_temporary_filesystems +
ns_info->private_dev +
(ns_info->protect_kernel_tunables ?
@@ -1655,8 +1759,8 @@ static int apply_mounts(
if (m->applied)
continue;
- /* ExtensionImages are first opened in the propagate directory, not in the root_directory */
- r = follow_symlink(m->mode != EXTENSION_IMAGES ? root : NULL, m);
+ /* ExtensionImages/Directories are first opened in the propagate directory, not in the root_directory */
+ r = follow_symlink(!IN_SET(m->mode, EXTENSION_IMAGES, EXTENSION_DIRECTORIES) ? root : NULL, m);
if (r < 0) {
if (error_path && mount_entry_path(m))
*error_path = strdup(mount_entry_path(m));
@@ -1879,6 +1983,7 @@ int setup_namespace(
const char *verity_data_path,
const MountImage *extension_images,
size_t n_extension_images,
+ char **extension_directories,
const char *propagate_dir,
const char *incoming_dir,
const char *notify_socket,
@@ -1992,7 +2097,7 @@ int setup_namespace(
require_prefix = true;
}
- if (n_extension_images > 0) {
+ if (n_extension_images > 0 || !strv_isempty(extension_directories)) {
r = parse_env_extension_hierarchies(&hierarchies);
if (r < 0)
return r;
@@ -2010,6 +2115,7 @@ int setup_namespace(
n_temporary_filesystems,
n_mount_images,
n_extension_images,
+ strv_length(extension_directories),
strv_length(hierarchies),
tmp_dir, var_tmp_dir,
creds_path,
@@ -2078,7 +2184,7 @@ int setup_namespace(
if (r < 0)
goto finish;
- r = append_extension_images(&m, root, extension_dir, hierarchies, extension_images, n_extension_images);
+ r = append_extensions(&m, root, extension_dir, hierarchies, extension_images, n_extension_images, extension_directories);
if (r < 0)
goto finish;
diff --git a/src/core/namespace.h b/src/core/namespace.h
index 62f05d7585..ae84d2b03b 100644
--- a/src/core/namespace.h
+++ b/src/core/namespace.h
@@ -142,6 +142,7 @@ int setup_namespace(
const char *root_verity,
const MountImage *extension_images,
size_t n_extension_images,
+ char **extension_directories,
const char *propagate_dir,
const char *incoming_dir,
const char *notify_socket,
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index dcce530c99..c35dd286e6 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -973,6 +973,7 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
"ExecPaths",
"NoExecPaths",
"ExecSearchPath",
+ "ExtensionDirectories",
"ConfigurationDirectory",
"SupplementaryGroups",
"SystemCallArchitectures"))
diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c
index 8df5533d6e..09c3091641 100644
--- a/src/test/test-namespace.c
+++ b/src/test/test-namespace.c
@@ -207,6 +207,7 @@ TEST(protect_kernel_logs) {
NULL,
NULL,
NULL,
+ NULL,
NULL);
assert_se(r == 0);
diff --git a/src/test/test-ns.c b/src/test/test-ns.c
index b03eabb59b..cd455a3a5b 100644
--- a/src/test/test-ns.c
+++ b/src/test/test-ns.c
@@ -108,6 +108,7 @@ int main(int argc, char *argv[]) {
NULL,
NULL,
NULL,
+ NULL,
NULL);
if (r < 0) {
log_error_errno(r, "Failed to set up namespace: %m");
diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service
index 78ddaf5ec8..186557f8a5 100644
--- a/test/fuzz/fuzz-unit-file/directives-all.service
+++ b/test/fuzz/fuzz-unit-file/directives-all.service
@@ -217,6 +217,7 @@ RootImage=
RootHash=
RootHashSignature=
RootVerity=
+ExtensionDirectories=
ExtensionImages=
RuntimeMaxSec=
SELinuxContextFromNet=
diff --git a/test/fuzz/fuzz-unit-file/directives.mount b/test/fuzz/fuzz-unit-file/directives.mount
index 67421444cc..0a44328e5c 100644
--- a/test/fuzz/fuzz-unit-file/directives.mount
+++ b/test/fuzz/fuzz-unit-file/directives.mount
@@ -40,6 +40,7 @@ DynamicUser=
Environment=
EnvironmentFile=
ExecPaths=
+ExtensionDirectories=
ExtensionImages=
FinalKillSignal=
ForceUnmount=
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index ca9959538b..6be65062d3 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -168,6 +168,7 @@ ExecStartPre=
ExecStop=
ExecStopPost=
ExitType=
+ExtensionDirectories=
ExtensionImages=
FailureAction=
FileDescriptorStoreMax=
diff --git a/test/fuzz/fuzz-unit-file/directives.socket b/test/fuzz/fuzz-unit-file/directives.socket
index 865fd83adc..90358fc11a 100644
--- a/test/fuzz/fuzz-unit-file/directives.socket
+++ b/test/fuzz/fuzz-unit-file/directives.socket
@@ -50,6 +50,7 @@ ExecStartPost=
ExecStartPre=
ExecStopPost=
ExecStopPre=
+ExtensionDirectories=
ExtensionImages=
FileDescriptorName=
FinalKillSignal=
diff --git a/test/fuzz/fuzz-unit-file/directives.swap b/test/fuzz/fuzz-unit-file/directives.swap
index f538ba8b60..5d057fa630 100644
--- a/test/fuzz/fuzz-unit-file/directives.swap
+++ b/test/fuzz/fuzz-unit-file/directives.swap
@@ -39,6 +39,7 @@ DynamicUser=
Environment=
EnvironmentFile=
ExecPaths=
+ExtensionDirectories=
ExtensionImages=
FinalKillSignal=
Group=
diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh
index b35527f761..ff4f77def2 100755
--- a/test/units/testsuite-50.sh
+++ b/test/units/testsuite-50.sh
@@ -321,6 +321,36 @@ EOF
systemctl start testservice-50e.service
systemctl is-active testservice-50e.service
+# ExtensionDirectories will set up an overlay
+mkdir -p "${image_dir}/app0" "${image_dir}/app1"
+systemd-run -P --property ExtensionDirectories="${image_dir}/nonexistant" --property RootImage="${image}.raw" cat /opt/script0.sh && { echo 'unexpected success'; exit 1; }
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0" --property RootImage="${image}.raw" cat /opt/script0.sh && { echo 'unexpected success'; exit 1; }
+systemd-dissect --mount /usr/share/app0.raw "${image_dir}/app0"
+systemd-dissect --mount /usr/share/app1.raw "${image_dir}/app1"
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0" --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0"
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1"
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /opt/script0.sh | grep -q -F "extension-release.app0"
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/some_file | grep -q -F "MARKER=1"
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /opt/script1.sh | grep -q -F "extension-release.app2"
+systemd-run -P --property ExtensionDirectories="${image_dir}/app0 ${image_dir}/app1" --property RootImage="${image}.raw" cat /usr/lib/systemd/system/other_file | grep -q -F "MARKER=1"
+cat >/run/systemd/system/testservice-50f.service <<EOF
+[Service]
+MountAPIVFS=yes
+TemporaryFileSystem=/run
+RootImage=${image}.raw
+ExtensionDirectories=${image_dir}/app0 ${image_dir}/app1
+# Relevant only for sanitizer runs
+UnsetEnvironment=LD_PRELOAD
+ExecStart=/bin/bash -c '/opt/script0.sh | grep ID'
+ExecStart=/bin/bash -c '/opt/script1.sh | grep ID'
+Type=oneshot
+RemainAfterExit=yes
+EOF
+systemctl start testservice-50f.service
+systemctl is-active testservice-50f.service
+umount "${image_dir}/app0"
+umount "${image_dir}/app1"
+
echo OK >/testok
exit 0