diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index f484f28a70..7ade8c3e8b 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -4847,8 +4847,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
CacheDirectorySymlink and LogsDirectorySymlink respectively
implement the destination parameter of the unit files settings RuntimeDirectory,
StateDirectory, CacheDirectory and LogsDirectory,
- which will create a symlink of the given name to the respective directory. The messages take an unused
- flags parameter, reserved for future backward-compatible changes.
+ which will create a symlink of the given name to the respective directory. The messages take a
+ flags parameter that make the directory read only:
+
+
+#define SD_EXEC_DIRECTORY_READ_ONLY (UINT64_C(1) << 0)
+
ExtraFileDescriptorNames contains file descriptor names passed to the service via
the ExtraFileDescriptors property in the StartTransientUnit()
diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index f84204d247..d5f85ed85c 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -1561,12 +1561,18 @@ CapabilityBoundingSet=~CAP_B CAP_C
configuration or lifetime guarantees, please consider using
tmpfiles.d5.
- RuntimeDirectory=, StateDirectory=, CacheDirectory=
- and LogsDirectory= optionally support a second parameter, separated by :.
- 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 BindPaths= or TemporaryFileSystem=
- 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.
+ RuntimeDirectory=, StateDirectory=,
+ CacheDirectory= and LogsDirectory= optionally support two
+ more parameters, separated by :. 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 BindPaths= or TemporaryFileSystem= 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 ro to make the directory read only for the
+ service. This is also supported for ConfigurationDirectory=. 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:
+ ConfigurationDirectory=foo::ro
The directories defined by these options are always created under the standard paths used by systemd
(/var/, /run/, /etc/, …). If the service needs
diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c
index 4f627a11e2..a9a73b599b 100644
--- a/src/core/dbus-execute.c
+++ b/src/core/dbus-execute.c
@@ -5,6 +5,7 @@
#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"
@@ -981,11 +982,18 @@ static int property_get_exec_dir_symlink(
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);
}
@@ -3444,7 +3452,7 @@ int bus_exec_context_set_transient_property(
_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();
}
@@ -3862,7 +3870,7 @@ int bus_exec_context_set_transient_property(
} 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);
@@ -3873,40 +3881,50 @@ int bus_exec_context_set_transient_property(
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)
diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c
index feea7897ef..968e9bd7c4 100644
--- a/src/core/exec-invoke.c
+++ b/src/core/exec-invoke.c
@@ -2452,7 +2452,7 @@ static int setup_exec_directory(
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
@@ -2643,7 +2643,7 @@ static int compile_bind_mounts(
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) {
@@ -2691,8 +2691,10 @@ static int compile_bind_mounts(
_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))
@@ -2718,6 +2720,7 @@ static int compile_bind_mounts(
.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),
};
}
}
@@ -2766,7 +2769,7 @@ static int compile_symlinks(
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);
diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c
index 39369e22b8..6fa0b21968 100644
--- a/src/core/execute-serialize.c
+++ b/src/core/execute-serialize.c
@@ -1998,7 +1998,10 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) {
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) {
@@ -2893,7 +2896,8 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) {
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 */
@@ -2904,20 +2908,27 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) {
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);
- if (r < 0)
- return r;
-
r = parse_boolean(only_create);
if (r < 0)
return r;
- c->directories[dt].items[c->directories[dt].n_items - 1].only_create = r;
+ if (r > 0)
+ exec_directory_flags |= EXEC_DIRECTORY_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;
if (isempty(p))
continue;
diff --git a/src/core/execute.c b/src/core/execute.c
index a081c42938..1c41b39a2f 100644
--- a/src/core/execute.c
+++ b/src/core/execute.c
@@ -331,6 +331,11 @@ bool exec_needs_mount_namespace(
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;
}
@@ -1118,7 +1123,12 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
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);
@@ -2731,7 +2741,7 @@ static ExecDirectoryItem *exec_directory_find(ExecDirectory *d, const char *path
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;
@@ -2746,6 +2756,8 @@ int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink)
if (r < 0)
return r;
+ existing->flags |= flags;
+
return 0; /* existing item is updated */
}
@@ -2765,6 +2777,7 @@ int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink)
d->items[d->n_items++] = (ExecDirectoryItem) {
.path = TAKE_PTR(p),
.symlinks = TAKE_PTR(s),
+ .flags = flags,
};
return 1; /* new item is added */
@@ -2782,7 +2795,7 @@ void exec_directory_sort(ExecDirectory *d) {
/* 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)
@@ -2793,7 +2806,7 @@ void exec_directory_sort(ExecDirectory *d) {
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;
}
}
diff --git a/src/core/execute.h b/src/core/execute.h
index 1fc7e6d785..2f5abc9785 100644
--- a/src/core/execute.h
+++ b/src/core/execute.h
@@ -15,6 +15,7 @@ typedef struct Manager Manager;
#include
#include
+#include "bus-unit-util.h"
#include "cgroup-util.h"
#include "coredump-util.h"
#include "cpu-set-util.h"
@@ -161,7 +162,7 @@ static inline bool EXEC_DIRECTORY_TYPE_SHALL_CHOWN(ExecDirectoryType t) {
typedef struct ExecDirectoryItem {
char *path;
char **symlinks;
- bool only_create;
+ ExecDirectoryFlags flags;
} ExecDirectoryItem;
typedef struct ExecDirectory {
@@ -585,7 +586,7 @@ void exec_params_deep_clear(ExecParameters *p);
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);
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index b059e515ef..1d813332b1 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -4698,9 +4698,9 @@ int config_parse_exec_directories(
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) {
@@ -4727,20 +4727,20 @@ int config_parse_exec_directories(
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;
}
@@ -4749,7 +4749,14 @@ int config_parse_exec_directories(
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();
}
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index 2e17bae51a..90b6f233e2 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -2127,7 +2127,7 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
}
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
@@ -2135,7 +2135,7 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
* 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)
@@ -2144,20 +2144,31 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
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);
+ 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);
- r = strv_consume_pair(&symlinks, TAKE_PTR(source), TAKE_PTR(destination));
+ 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();
}
@@ -2192,7 +2203,7 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
/* 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");
@@ -2228,6 +2239,18 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
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);
@@ -3078,3 +3101,13 @@ int unit_freezer_freeze(UnitFreezer *f) {
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;
+}
diff --git a/src/shared/bus-unit-util.h b/src/shared/bus-unit-util.h
index 6f0d55971c..8090b36e3d 100644
--- a/src/shared/bus-unit-util.h
+++ b/src/shared/bus-unit-util.h
@@ -7,6 +7,16 @@
#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;
diff --git a/test/units/TEST-23-UNIT-FILE.statedir.sh b/test/units/TEST-23-UNIT-FILE.statedir.sh
index b592314a09..cc30f0a27d 100755
--- a/test/units/TEST-23-UNIT-FILE.statedir.sh
+++ b/test/units/TEST-23-UNIT-FILE.statedir.sh
@@ -56,5 +56,10 @@ test "$(readlink "$HOME"/.local/state/foo)" = ../../.config/foo
# 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
diff --git a/test/units/TEST-34-DYNAMICUSERMIGRATE.sh b/test/units/TEST-34-DYNAMICUSERMIGRATE.sh
index d15b675e26..d6bdfb0741 100755
--- a/test/units/TEST-34-DYNAMICUSERMIGRATE.sh
+++ b/test/units/TEST-34-DYNAMICUSERMIGRATE.sh
@@ -23,6 +23,9 @@ test_directory() {
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
@@ -47,6 +50,9 @@ test_directory() {
-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
@@ -70,6 +76,9 @@ test_directory() {
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
@@ -84,6 +93,8 @@ test_directory() {
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 <