<varname>CacheDirectorySymlink</varname> and <varname>LogsDirectorySymlink</varname> respectively
implement the destination parameter of the unit files settings <varname>RuntimeDirectory</varname>,
<varname>StateDirectory</varname>, <varname>CacheDirectory</varname> and <varname>LogsDirectory</varname>,
- which will create a symlink of the given name to the respective directory. The messages take an unused
- <varname>flags</varname> parameter, reserved for future backward-compatible changes.</para>
+ which will create a symlink of the given name to the respective directory. The messages take a
+ <varname>flags</varname> parameter that make the directory read only:</para>
+
+ <programlisting>
+#define SD_EXEC_DIRECTORY_READ_ONLY (UINT64_C(1) << 0)
+ </programlisting>
<para><varname>ExtraFileDescriptorNames</varname> contains file descriptor names passed to the service via
the <varname>ExtraFileDescriptors</varname> property in the <function>StartTransientUnit()</function>
configuration or lifetime guarantees, please consider using
<citerefentry><refentrytitle>tmpfiles.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
- <para><varname>RuntimeDirectory=</varname>, <varname>StateDirectory=</varname>, <varname>CacheDirectory=</varname>
- and <varname>LogsDirectory=</varname> optionally support a second parameter, separated by <literal>:</literal>.
- The second parameter will be interpreted as a destination path that will be created as a symlink to the directory.
- The symlinks will be created after any <varname>BindPaths=</varname> or <varname>TemporaryFileSystem=</varname>
- options have been set up, to make ephemeral symlinking possible. The same source can have multiple symlinks, by
- using the same first parameter, but a different second parameter.</para>
+ <para><varname>RuntimeDirectory=</varname>, <varname>StateDirectory=</varname>,
+ <varname>CacheDirectory=</varname> and <varname>LogsDirectory=</varname> optionally support two
+ more parameters, separated by <literal>:</literal>. The second parameter will be interpreted as a
+ destination path that will be created as a symlink to the directory. The symlinks will be created
+ after any <varname>BindPaths=</varname> or <varname>TemporaryFileSystem=</varname> options have been
+ set up, to make ephemeral symlinking possible. The same source can have multiple symlinks, by using
+ the same first parameter, but a different second parameter. The third parameter is a flags field,
+ and since v257 can take a value of <constant>ro</constant> to make the directory read only for the
+ service. This is also supported for <varname>ConfigurationDirectory=</varname>. If multiple symlinks
+ are set up, the directory will be read only if at least one is configured to be read only. To pass a
+ flag without a destination symlink, the second parameter can be empty, for example:
+ <programlisting>ConfigurationDirectory=foo::ro</programlisting></para>
<para>The directories defined by these options are always created under the standard paths used by systemd
(<filename>/var/</filename>, <filename>/run/</filename>, <filename>/etc/</filename>, …). If the service needs
#include "af-list.h"
#include "alloc-util.h"
#include "bus-get-properties.h"
+#include "bus-unit-util.h"
#include "bus-util.h"
#include "cap-list.h"
#include "capability-util.h"
return r;
FOREACH_ARRAY(i, d->items, d->n_items)
- STRV_FOREACH(dst, i->symlinks) {
- r = sd_bus_message_append(reply, "(sst)", i->path, *dst, UINT64_C(0) /* flags, unused for now */);
+ if (strv_isempty(i->symlinks)) {
+ /* The old exec directory properties cannot represent flags, so list them here with no
+ * destination */
+ r = sd_bus_message_append(reply, "(sst)", i->path, "", (uint64_t) (i->flags & _EXEC_DIRECTORY_FLAGS_PUBLIC));
if (r < 0)
return r;
- }
+ } else
+ STRV_FOREACH(dst, i->symlinks) {
+ r = sd_bus_message_append(reply, "(sst)", i->path, *dst, (uint64_t) (i->flags & _EXEC_DIRECTORY_FLAGS_PUBLIC));
+ if (r < 0)
+ return r;
+ }
return sd_bus_message_close_container(reply);
}
_cleanup_free_ char *joined = NULL;
STRV_FOREACH(source, l) {
- r = exec_directory_add(d, *source, NULL);
+ r = exec_directory_add(d, *source, /* symlink= */ NULL, /* flags= */ 0);
if (r < 0)
return log_oom();
}
} else if (STR_IN_SET(name, "StateDirectorySymlink", "RuntimeDirectorySymlink", "CacheDirectorySymlink", "LogsDirectorySymlink")) {
char *source, *destination;
ExecDirectory *directory;
- uint64_t symlink_flags; /* No flags for now, reserved for future uses. */
+ uint64_t symlink_flags;
ExecDirectoryType i;
assert_se((i = exec_directory_type_symlink_from_string(name)) >= 0);
return r;
while ((r = sd_bus_message_read(message, "(sst)", &source, &destination, &symlink_flags)) > 0) {
+ if ((symlink_flags & ~_EXEC_DIRECTORY_FLAGS_PUBLIC) != 0)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid 'flags' parameter '%" PRIu64 "'", symlink_flags);
if (!path_is_valid(source))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Source path %s is not valid.", source);
if (path_is_absolute(source))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Source path %s is absolute.", source);
if (!path_is_normalized(source))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Source path %s is not normalized.", source);
- if (!path_is_valid(destination))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is not valid.", destination);
- if (path_is_absolute(destination))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is absolute.", destination);
- if (!path_is_normalized(destination))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is not normalized.", destination);
- if (symlink_flags != 0)
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be zero.");
+ if (isempty(destination))
+ destination = NULL;
+ else {
+ if (!path_is_valid(destination))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is not valid.", destination);
+ if (path_is_absolute(destination))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is absolute.", destination);
+ if (!path_is_normalized(destination))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Destination path %s is not normalized.", destination);
+ }
if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
_cleanup_free_ char *destination_escaped = NULL, *source_escaped = NULL;
- r = exec_directory_add(directory, source, destination);
+ r = exec_directory_add(directory, source, destination, symlink_flags);
if (r < 0)
return r;
/* Need to store them in the unit with the escapes, so that they can be parsed again */
source_escaped = xescape(source, ":");
- destination_escaped = xescape(destination, ":");
- if (!source_escaped || !destination_escaped)
+ if (!source_escaped)
return -ENOMEM;
+ if (destination) {
+ destination_escaped = xescape(destination, ":");
+ if (!destination_escaped)
+ return -ENOMEM;
+ }
unit_write_settingf(
u, flags|UNIT_ESCAPE_SPECIFIERS, exec_directory_type_to_string(i),
- "%s=%s:%s",
+ "%s=%s%s%s%s",
exec_directory_type_to_string(i),
source_escaped,
- destination_escaped);
+ destination_escaped || FLAGS_SET(symlink_flags, EXEC_DIRECTORY_READ_ONLY) ? ":" : "",
+ destination_escaped,
+ FLAGS_SET(symlink_flags, EXEC_DIRECTORY_READ_ONLY) ? ":ro" : "");
}
}
if (r < 0)
goto fail;
}
- if (!i->only_create) {
+ if (!FLAGS_SET(i->flags, EXEC_DIRECTORY_ONLY_CREATE)) {
/* And link it up from the original place.
* Notes
* 1) If a mount namespace is going to be used, then this symlink remains on
continue;
FOREACH_ARRAY(i, context->directories[t].items, context->directories[t].n_items)
- n += !i->only_create;
+ n += !FLAGS_SET(i->flags, EXEC_DIRECTORY_ONLY_CREATE) || FLAGS_SET(i->flags, EXEC_DIRECTORY_READ_ONLY);
}
if (n <= 0) {
_cleanup_free_ char *s = NULL, *d = NULL;
/* When one of the parent directories is in the list, we cannot create the symlink
- * for the child directory. See also the comments in setup_exec_directory(). */
- if (i->only_create)
+ * for the child directory. See also the comments in setup_exec_directory().
+ * But if it needs to be read only, then we have to create a bind mount anyway to
+ * make it so. */
+ if (FLAGS_SET(i->flags, EXEC_DIRECTORY_ONLY_CREATE) && !FLAGS_SET(i->flags, EXEC_DIRECTORY_READ_ONLY))
continue;
if (exec_directory_is_private(context, t))
.destination = TAKE_PTR(d),
.nosuid = context->dynamic_user, /* don't allow suid/sgid when DynamicUser= is on */
.recursive = true,
+ .read_only = FLAGS_SET(i->flags, EXEC_DIRECTORY_READ_ONLY),
};
}
}
if (!exec_directory_is_private(context, dt) ||
exec_context_with_rootfs(context) ||
- i->only_create)
+ FLAGS_SET(i->flags, EXEC_DIRECTORY_ONLY_CREATE))
continue;
private_path = path_join(params->prefix[dt], "private", i->path);
if (!strextend(&value, " ", path_escaped))
return log_oom_debug();
- if (!strextend(&value, ":", yes_no(i->only_create)))
+ if (!strextend(&value, ":", yes_no(FLAGS_SET(i->flags, EXEC_DIRECTORY_ONLY_CREATE))))
+ return log_oom_debug();
+
+ if (!strextend(&value, ":", yes_no(FLAGS_SET(i->flags, EXEC_DIRECTORY_READ_ONLY))))
return log_oom_debug();
STRV_FOREACH(d, i->symlinks) {
return r;
for (;;) {
- _cleanup_free_ char *tuple = NULL, *path = NULL, *only_create = NULL;
+ _cleanup_free_ char *tuple = NULL, *path = NULL, *only_create = NULL, *read_only = NULL;
+ ExecDirectoryFlags exec_directory_flags = 0;
const char *p;
/* Use EXTRACT_UNESCAPE_RELAX here, as we unescape the colons in subsequent calls */
break;
p = tuple;
- r = extract_many_words(&p, ":", EXTRACT_UNESCAPE_SEPARATORS, &path, &only_create);
+ r = extract_many_words(&p, ":", EXTRACT_UNESCAPE_SEPARATORS, &path, &only_create, &read_only);
if (r < 0)
return r;
if (r < 2)
continue;
- r = exec_directory_add(&c->directories[dt], path, NULL);
+ r = parse_boolean(only_create);
if (r < 0)
return r;
+ if (r > 0)
+ exec_directory_flags |= EXEC_DIRECTORY_ONLY_CREATE;
- r = parse_boolean(only_create);
+ r = parse_boolean(read_only);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ exec_directory_flags |= EXEC_DIRECTORY_READ_ONLY;
+
+ r = exec_directory_add(&c->directories[dt], path, /* symlink= */ NULL, exec_directory_flags);
if (r < 0)
return r;
- c->directories[dt].items[c->directories[dt].n_items - 1].only_create = r;
if (isempty(p))
continue;
if (exec_context_get_effective_bind_log_sockets(context))
return true;
+ for (ExecDirectoryType t = 0; t < _EXEC_DIRECTORY_TYPE_MAX; t++)
+ FOREACH_ARRAY(i, context->directories[t].items, context->directories[t].n_items)
+ if (FLAGS_SET(i->flags, EXEC_DIRECTORY_READ_ONLY))
+ return true;
+
return false;
}
fprintf(f, "%s%sMode: %04o\n", prefix, exec_directory_type_to_string(dt), c->directories[dt].mode);
for (size_t i = 0; i < c->directories[dt].n_items; i++) {
- fprintf(f, "%s%s: %s\n", prefix, exec_directory_type_to_string(dt), c->directories[dt].items[i].path);
+ fprintf(f,
+ "%s%s: %s%s\n",
+ prefix,
+ exec_directory_type_to_string(dt),
+ c->directories[dt].items[i].path,
+ FLAGS_SET(c->directories[dt].items[i].flags, EXEC_DIRECTORY_READ_ONLY) ? " (ro)" : "");
STRV_FOREACH(d, c->directories[dt].items[i].symlinks)
fprintf(f, "%s%s: %s:%s\n", prefix, exec_directory_type_symlink_to_string(dt), c->directories[dt].items[i].path, *d);
return NULL;
}
-int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink) {
+int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink, ExecDirectoryFlags flags) {
_cleanup_strv_free_ char **s = NULL;
_cleanup_free_ char *p = NULL;
ExecDirectoryItem *existing;
if (r < 0)
return r;
+ existing->flags |= flags;
+
return 0; /* existing item is updated */
}
d->items[d->n_items++] = (ExecDirectoryItem) {
.path = TAKE_PTR(p),
.symlinks = TAKE_PTR(s),
+ .flags = flags,
};
return 1; /* new item is added */
/* Sort the exec directories to make always parent directories processed at first in
* setup_exec_directory(), e.g., even if StateDirectory=foo/bar foo, we need to create foo at first,
- * then foo/bar. Also, set .only_create flag if one of the parent directories is contained in the
+ * then foo/bar. Also, set the ONLY_CREATE flag if one of the parent directories is contained in the
* list. See also comments in setup_exec_directory() and issue #24783. */
if (d->n_items <= 1)
for (size_t i = 1; i < d->n_items; i++)
for (size_t j = 0; j < i; j++)
if (path_startswith(d->items[i].path, d->items[j].path)) {
- d->items[i].only_create = true;
+ d->items[i].flags |= EXEC_DIRECTORY_ONLY_CREATE;
break;
}
}
#include <stdio.h>
#include <sys/capability.h>
+#include "bus-unit-util.h"
#include "cgroup-util.h"
#include "coredump-util.h"
#include "cpu-set-util.h"
typedef struct ExecDirectoryItem {
char *path;
char **symlinks;
- bool only_create;
+ ExecDirectoryFlags flags;
} ExecDirectoryItem;
typedef struct ExecDirectory {
bool exec_context_get_cpu_affinity_from_numa(const ExecContext *c);
void exec_directory_done(ExecDirectory *d);
-int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink);
+int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink, ExecDirectoryFlags flags);
void exec_directory_sort(ExecDirectory *d);
bool exec_directory_is_private(const ExecContext *context, ExecDirectoryType type);
if (r == 0)
return 0;
- _cleanup_free_ char *src = NULL, *dest = NULL;
+ _cleanup_free_ char *src = NULL, *dest = NULL, *flags = NULL;
const char *q = tuple;
- r = extract_many_words(&q, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS, &src, &dest);
+ r = extract_many_words(&q, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS|EXTRACT_DONT_COALESCE_SEPARATORS, &src, &dest, &flags);
if (r == -ENOMEM)
return log_oom();
if (r <= 0) {
continue;
}
+ if (!isempty(dest) && streq(lvalue, "ConfigurationDirectory")) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Additional parameter is not supported for ConfigurationDirectory, ignoring: %s", tuple);
+ continue;
+ }
+
/* For State and Runtime directories we support an optional destination parameter, which
* will be used to create a symlink to the source. */
_cleanup_free_ char *dresolved = NULL;
if (!isempty(dest)) {
- if (streq(lvalue, "ConfigurationDirectory")) {
- log_syntax(unit, LOG_WARNING, filename, line, 0,
- "Destination parameter is not supported for ConfigurationDirectory, ignoring: %s", tuple);
- continue;
- }
-
r = unit_path_printf(u, dest, &dresolved);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r,
- "Failed to resolve unit specifiers in \"%s\", ignoring: %m", dest);
+ "Failed to resolve unit specifiers in \"%s\", ignoring: %m", dest);
continue;
}
continue;
}
- r = exec_directory_add(ed, sresolved, dresolved);
+ ExecDirectoryFlags exec_directory_flags = exec_directory_flags_from_string(flags);
+ if (exec_directory_flags < 0 || (exec_directory_flags & ~_EXEC_DIRECTORY_FLAGS_PUBLIC) != 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Invalid flags for %s=, ignoring: %s", lvalue, flags);
+ continue;
+ }
+
+ r = exec_directory_add(ed, sresolved, dresolved, exec_directory_flags);
if (r < 0)
return log_oom();
}
}
if (STR_IN_SET(field, "StateDirectory", "RuntimeDirectory", "CacheDirectory", "LogsDirectory")) {
- _cleanup_strv_free_ char **symlinks = NULL, **sources = NULL;
+ _cleanup_strv_free_ char **symlinks = NULL, **symlinks_ro = NULL, **sources = NULL, **sources_ro = NULL;
const char *p = eq;
/* Adding new directories is supported from both *DirectorySymlink methods and the
* tuple use the new method, else use the old one. */
for (;;) {
- _cleanup_free_ char *tuple = NULL, *source = NULL, *destination = NULL;
+ _cleanup_free_ char *tuple = NULL, *source = NULL, *dest = NULL, *flags = NULL;
r = extract_first_word(&p, &tuple, NULL, EXTRACT_UNQUOTE);
if (r < 0)
break;
const char *t = tuple;
- r = extract_many_words(&t, ":", EXTRACT_UNQUOTE|EXTRACT_DONT_COALESCE_SEPARATORS, &source, &destination);
+ r = extract_many_words(&t, ":", EXTRACT_UNQUOTE|EXTRACT_DONT_COALESCE_SEPARATORS, &source, &dest, &flags);
if (r <= 0)
return log_error_errno(r ?: SYNTHETIC_ERRNO(EINVAL), "Failed to parse argument: %m");
path_simplify(source);
- if (isempty(destination)) {
+ if (isempty(dest) && isempty(flags)) {
r = strv_consume(&sources, TAKE_PTR(source));
if (r < 0)
return bus_log_create_error(r);
+ } else if (isempty(flags)) {
+ path_simplify(dest);
+ r = strv_consume_pair(&symlinks, TAKE_PTR(source), TAKE_PTR(dest));
+ if (r < 0)
+ return log_oom();
} else {
- path_simplify(destination);
-
- r = strv_consume_pair(&symlinks, TAKE_PTR(source), TAKE_PTR(destination));
+ ExecDirectoryFlags exec_directory_flags = exec_directory_flags_from_string(flags);
+ if (exec_directory_flags < 0 || (exec_directory_flags & ~_EXEC_DIRECTORY_FLAGS_PUBLIC) != 0)
+ return log_error_errno(r, "Failed to parse flags: %s", flags);
+
+ if (!isempty(dest)) {
+ path_simplify(dest);
+ r = strv_consume_pair(&symlinks_ro, TAKE_PTR(source), TAKE_PTR(dest));
+ } else
+ r = strv_consume(&sources_ro, TAKE_PTR(source));
if (r < 0)
return log_oom();
}
/* For State and Runtime directories we support an optional destination parameter, which
* will be used to create a symlink to the source. But it is new so we cannot change the
* old DBUS signatures, so append a new message type. */
- if (!strv_isempty(symlinks)) {
+ if (!strv_isempty(symlinks) || !strv_isempty(symlinks_ro) || !strv_isempty(sources_ro)) {
const char *symlink_field;
r = sd_bus_message_open_container(m, SD_BUS_TYPE_STRUCT, "sv");
return bus_log_create_error(r);
}
+ STRV_FOREACH_PAIR(source, destination, symlinks_ro) {
+ r = sd_bus_message_append(m, "(sst)", *source, *destination, (uint64_t) EXEC_DIRECTORY_READ_ONLY);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
+ STRV_FOREACH(source, sources_ro) {
+ r = sd_bus_message_append(m, "(sst)", *source, "", (uint64_t) EXEC_DIRECTORY_READ_ONLY);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
+
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
int unit_freezer_thaw(UnitFreezer *f) {
return unit_freezer_action(f, false);
}
+
+ExecDirectoryFlags exec_directory_flags_from_string(const char *s) {
+ if (isempty(s))
+ return 0;
+
+ if (streq(s, "ro"))
+ return EXEC_DIRECTORY_READ_ONLY;
+
+ return _EXEC_DIRECTORY_FLAGS_INVALID;
+}
#include "pidref.h"
#include "unit-def.h"
+typedef enum ExecDirectoryFlags {
+ EXEC_DIRECTORY_READ_ONLY = 1 << 0, /* Public API via DBUS, do not change */
+ EXEC_DIRECTORY_ONLY_CREATE = 1 << 1, /* Only the private directory will be created, not the symlink to it */
+ _EXEC_DIRECTORY_FLAGS_MAX,
+ _EXEC_DIRECTORY_FLAGS_PUBLIC = EXEC_DIRECTORY_READ_ONLY,
+ _EXEC_DIRECTORY_FLAGS_INVALID = -EINVAL,
+} ExecDirectoryFlags;
+
+ExecDirectoryFlags exec_directory_flags_from_string(const char *s) _pure_;
+
typedef struct UnitInfo {
const char *machine;
const char *id;
# Check that this will work safely a second time
systemd-run --user -p StateDirectory=foo -p ConfigurationDirectory=foo --wait /bin/true
+( ! systemd-run --user -p StateDirectory=foo::ro --wait sh -c "echo foo > $HOME/.local/state/foo/baz")
+( ! systemd-run --user -p StateDirectory=foo:bar:ro --wait sh -c "echo foo > $HOME/.local/state/foo/baz")
+( ! test -f "$HOME"/.local/state/foo/baz)
+test -L "$HOME"/.local/state/bar
+
rm "$HOME"/.local/state/foo
rmdir "$HOME"/.config/foo
systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}=zzz:xxx zzz:xxx2" -p TemporaryFileSystem="${path}" bash -c "test -f ${path}/xxx/test && test -f ${path}/xxx2/test"
systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}":ro test -f "${path}"/xxx/test
(! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz test -f "${path}"/zzz/test-missing)
+ systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"="www::ro www:ro:ro" test -d "${path}"/www
+ systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"="www::ro www:ro:ro" test -L "${path}"/ro
+ (! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"="www::ro www:ro:ro" sh -c "echo foo > ${path}/www/test-missing")
test -d "${path}"/zzz
test ! -L "${path}"/zzz
-p TemporaryFileSystem="${path}" -p EnvironmentFile=-/usr/lib/systemd/systemd-asan-env bash -c "test -f ${path}/xxx/test && test -f ${path}/xxx2/test"
systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}":ro test -f "${path}"/xxx/test
(! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"=zzz test -f "${path}"/zzz/test-missing)
+ systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"="www::ro www:ro:ro" test -d "${path}"/www
+ systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"="www::ro www:ro:ro" test -L "${path}"/ro
+ (! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=1 -p "${directory}"="www::ro www:ro:ro" sh -c "echo foo > ${path}/www/test-missing")
test -L "${path}"/zzz
test -d "${path}"/private/zzz
systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}=zzz:xxx zzz:xxx2" -p TemporaryFileSystem="${path}" bash -c "test -f ${path}/xxx/test && test -f ${path}/xxx2/test"
systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz:xxx -p TemporaryFileSystem="${path}":ro test -f "${path}"/xxx/test
(! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"=zzz test -f "${path}"/zzz/test-missing)
+ systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"="www::ro www:ro:ro" test -d "${path}"/www
+ systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"="www::ro www:ro:ro" test -L "${path}"/ro
+ (! systemd-run --wait -p RuntimeDirectoryPreserve=yes -p DynamicUser=0 -p "${directory}"="www::ro www:ro:ro" sh -c "echo foo > ${path}/www/test-missing")
test -d "${path}"/zzz
test ! -L "${path}"/zzz
test -f "${path}"/zzz/test
test ! -e "${path}"/zzz/test-missing
+ test -d "${path}"/www
+ test ! -e "${path}"/www/test-missing
# Exercise the unit parsing paths too
cat >/run/systemd/system/testservice-34.service <<EOF
Type=oneshot
TemporaryFileSystem=${path}
RuntimeDirectoryPreserve=yes
-${directory}=zzz:x\:yz zzz:x\:yz2
+${directory}=zzz:x\:yz zzz:x\:yz2 www::ro www:ro:ro
ExecStart=test -f ${path}/x:yz2/test
ExecStart=test -f ${path}/x:yz/test
ExecStart=test -f ${path}/zzz/test
+ExecStart=test -d ${path}/www
+ExecStart=test -L ${path}/ro
+ExecStart=sh -c "! test -w ${path}/www"
EOF
systemctl daemon-reload
systemctl start --wait testservice-34.service