diff --git a/NEWS b/NEWS
index e21b3fb08fd..e3ff88d7a8b 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,20 @@ CHANGES WITH 263 in spe:
Feature Removals and Incompatible Changes:
+ * ConfigurationDirectory= passed as a transient-unit property over
+ D-Bus now uses the same directory:symlink:flags parser as the other
+ *Directory= settings, allowing the read-only flag with an empty
+ symlink field. Literal colons need an additional level of escaping
+ in the client value. For example, use the shell-quoted argument
+ --property='ConfigurationDirectory=foo\\:bar' for a directory named
+ foo:bar. Unit file syntax and the signature of the plain D-Bus
+ string array property are unchanged.
+
+ * Empty transient assignments to RuntimeDirectory=, StateDirectory=,
+ CacheDirectory= and LogsDirectory= now reset the corresponding
+ directory list instead of being ignored. ConfigurationDirectory=
+ continues to support resetting the list with an empty assignment.
+
* Transient *Directory= drop-ins now escape colons and quotes in
paths set through either D-Bus signature, preserving the paths
when the unit is reloaded.
@@ -20,6 +34,11 @@ CHANGES WITH 263 in spe:
DynamicUser= directories. Unit files reject these symlink destinations
too, matching the existing restriction on source paths.
+ * systemctl show now includes directory flags in *DirectorySymlink
+ values. Read-only entries are shown as source:destination:ro instead
+ of source:destination. Unknown flags are shown numerically so newer
+ manager properties can still be displayed.
+
* systemd-networkd and networkctl have been merged into a single
multi-call binary, with the former becoming a symlink to the latter.
This should generally be invisible to users, but some packaging and
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index 20ed6f31bcb..bcc8cb45a9b 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -3574,6 +3574,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as LogsDirectory = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(sst) ConfigurationDirectorySymlink = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly u ConfigurationDirectoryMode = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as ConfigurationDirectory = ['...', ...];
@@ -4209,8 +4211,6 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
-
-
@@ -4967,6 +4967,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -5203,6 +5205,15 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
#define SD_EXEC_DIRECTORY_READ_ONLY (UINT64_C(1) << 0)
+ ConfigurationDirectory lists the configuration directories of the unit, as
+ configured by the unit file setting of the same name, and
+ ConfigurationDirectorySymlink exposes the same flags parameter
+ for them. Unlike the other directory types, ConfigurationDirectory does not
+ support symlinks, so the destination member of ConfigurationDirectorySymlink
+ must be left empty and is always returned as an empty string. The property retains the same
+ tuple signature and naming convention as the other directory types so clients can handle their
+ flags uniformly.
+
ExtraFileDescriptorNames contains file descriptor names passed to the service via
the ExtraFileDescriptors property in the StartTransientUnit()
method. See sd_listen_fds3
@@ -5884,6 +5895,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as LogsDirectory = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(sst) ConfigurationDirectorySymlink = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly u ConfigurationDirectoryMode = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as ConfigurationDirectory = ['...', ...];
@@ -6531,8 +6544,6 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
-
-
@@ -7265,6 +7276,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -8010,6 +8023,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as LogsDirectory = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(sst) ConfigurationDirectorySymlink = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly u ConfigurationDirectoryMode = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as ConfigurationDirectory = ['...', ...];
@@ -8581,8 +8596,6 @@ node /org/freedesktop/systemd1/unit/home_2emount {
-
-
@@ -9217,6 +9230,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -10084,6 +10099,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as LogsDirectory = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+ readonly a(sst) ConfigurationDirectorySymlink = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly u ConfigurationDirectoryMode = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as ConfigurationDirectory = ['...', ...];
@@ -10637,8 +10654,6 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
-
-
@@ -11255,6 +11270,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -13013,6 +13030,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
OOMRules were added in version 261.
LUOSession was added in version 262.
RestartRandomizedDelayUSec was added in version 262.
+ ConfigurationDirectorySymlink was added in version 263.
Socket Unit Objects
@@ -13092,6 +13110,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
XAttrEntryPoint,
XAttrListen, and
XAttrAccept were added in version 262.
+ ConfigurationDirectorySymlink was added in version 263.
Mount Unit Objects
@@ -13163,6 +13182,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
IOPressureWatch,
CPUSetPartition, and
OOMRules were added in version 261.
+ ConfigurationDirectorySymlink was added in version 263.
Swap Unit Objects
@@ -13232,6 +13252,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
IOPressureWatch,
CPUSetPartition, and
OOMRules were added in version 261.
+ ConfigurationDirectorySymlink was added in version 263.
Slice Unit Objects
diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml
index 64676bc9547..342c9e91490 100644
--- a/man/systemd.exec.xml
+++ b/man/systemd.exec.xml
@@ -1676,6 +1676,14 @@ CapabilityBoundingSet=~CAP_B CAP_C
flag without a destination symlink, the second parameter can be empty, for example:
ConfigurationDirectory=foo::ro
+ Because : separates the parameters, a directory name that contains a
+ literal colon must have it escaped as \:. Note that when these settings are
+ instead passed over D-Bus to create a transient unit, for example with
+ systemd-run --property=, the value is unescaped once more by the client, so a
+ literal colon requires \\: in the value received by the client after shell
+ quoting. For example:
+ systemd-run --property='ConfigurationDirectory=foo\\:bar' /usr/bin/true
+
The directories defined by these options are always created under the standard paths used by systemd
(/var/, /run/, /etc/, …). If the service needs
directories in a different location, a different mechanism has to be used to create them.
diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c
index 6e597e590ab..f3410df4f7e 100644
--- a/src/core/dbus-execute.c
+++ b/src/core/dbus-execute.c
@@ -1375,6 +1375,7 @@ const sd_bus_vtable bus_exec_vtable[] = {
SD_BUS_PROPERTY("LogsDirectoryAccounting", "b", bus_property_get_bool, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS].exec_quota.quota_accounting), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("LogsDirectoryQuota", "(tus)", property_get_exec_quota, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS].exec_quota), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("LogsDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS]), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("ConfigurationDirectorySymlink", "a(sst)", property_get_exec_dir_symlink, offsetof(ExecContext, directories[EXEC_DIRECTORY_CONFIGURATION]), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ConfigurationDirectoryMode", "u", bus_property_get_mode, offsetof(ExecContext, directories[EXEC_DIRECTORY_CONFIGURATION].mode), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ConfigurationDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_CONFIGURATION]), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("TimeoutCleanUSec", "t", bus_property_get_usec, offsetof(ExecContext, timeout_clean_usec), SD_BUS_VTABLE_PROPERTY_CONST),
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index a35dcc6f393..1e13e6739d4 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -2079,9 +2079,12 @@ static int bus_append_extension_images(sd_bus_message *m, const char *field, con
static int bus_append_directory(sd_bus_message *m, const char *field, const char *eq) {
_cleanup_strv_free_ char **symlinks = NULL, **symlinks_ro = NULL, **sources = NULL, **sources_ro = NULL;
+ ExecDirectoryType type;
const char *p = eq;
int r;
+ assert_se((type = exec_directory_type_from_string(field)) >= 0);
+
/* Adding new directories is supported from both *DirectorySymlink methods and the
* older ones, so first parse the input, and if we are given a new-style src:dst
* tuple use the new method, else use the old one. */
@@ -2102,6 +2105,10 @@ static int bus_append_directory(sd_bus_message *m, const char *field, const char
path_simplify(source);
+ if (!isempty(dest) && type == EXEC_DIRECTORY_CONFIGURATION)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Symlink destination is not supported for %s=: '%s'", field, tuple);
+
if (isempty(dest) && isempty(flags)) {
r = strv_consume(&sources, TAKE_PTR(source));
if (r < 0)
@@ -2114,7 +2121,8 @@ static int bus_append_directory(sd_bus_message *m, const char *field, const char
} else {
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(SYNTHETIC_ERRNO(EINVAL), "Failed to parse flags for %s=: '%s'", field, flags);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse flags for %s=: '%s'", field, flags);
if (!isempty(dest)) {
path_simplify(dest);
@@ -2126,7 +2134,11 @@ static int bus_append_directory(sd_bus_message *m, const char *field, const char
}
}
- if (!strv_isempty(sources)) {
+ bool have_new = !strv_isempty(symlinks) || !strv_isempty(symlinks_ro) || !strv_isempty(sources_ro);
+
+ /* Send the old property if we have plain sources, and also if we parsed nothing at all: an empty
+ * assignment resets the list, and only the old property can express that. */
+ if (!strv_isempty(sources) || !have_new) {
r = sd_bus_message_open_container(m, SD_BUS_TYPE_STRUCT, "sv");
if (r < 0)
return bus_log_create_error(r);
@@ -2152,27 +2164,17 @@ static int bus_append_directory(sd_bus_message *m, const char *field, const char
return bus_log_create_error(r);
}
- /* 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) || !strv_isempty(symlinks_ro) || !strv_isempty(sources_ro)) {
- const char *symlink_field;
+ /* For State, Runtime, Cache and Logs directories we support an optional destination parameter,
+ * which will be used to create a symlink to the source. Additionally all directory types support
+ * a flags parameter. But these are new so we cannot change the old D-Bus signatures, hence append
+ * a new message type. */
+ if (have_new) {
+ const char *symlink_field = exec_directory_type_symlink_to_string(type);
r = sd_bus_message_open_container(m, SD_BUS_TYPE_STRUCT, "sv");
if (r < 0)
return bus_log_create_error(r);
- if (streq(field, "StateDirectory"))
- symlink_field = "StateDirectorySymlink";
- else if (streq(field, "RuntimeDirectory"))
- symlink_field = "RuntimeDirectorySymlink";
- else if (streq(field, "CacheDirectory"))
- symlink_field = "CacheDirectorySymlink";
- else if (streq(field, "LogsDirectory"))
- symlink_field = "LogsDirectorySymlink";
- else
- assert_not_reached();
-
r = sd_bus_message_append_basic(m, SD_BUS_TYPE_STRING, symlink_field);
if (r < 0)
return bus_log_create_error(r);
@@ -2636,7 +2638,6 @@ static const BusProperty execute_properties[] = {
{ "NoExecPaths", bus_append_strv },
{ "ExecSearchPath", bus_append_strv_colon },
{ "ExtensionDirectories", bus_append_strv },
- { "ConfigurationDirectory", bus_append_strv },
{ "SupplementaryGroups", bus_append_strv },
{ "SystemCallArchitectures", bus_append_strv },
{ "SyslogLevel", bus_append_log_level_from_string },
@@ -2706,6 +2707,7 @@ static const BusProperty execute_properties[] = {
{ "RuntimeDirectory", bus_append_directory },
{ "CacheDirectory", bus_append_directory },
{ "LogsDirectory", bus_append_directory },
+ { "ConfigurationDirectory", bus_append_directory },
{ "ProtectHostname", bus_append_protect_hostname },
{ "ProtectHostnameEx", bus_append_protect_hostname }, /* compat */
{ "PrivateTmp", bus_append_boolean_or_ex_string },
diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c
index 88e0f591154..07350ad21df 100644
--- a/src/systemctl/systemctl-show.c
+++ b/src/systemctl/systemctl-show.c
@@ -2102,7 +2102,9 @@ static int print_property(
return bus_log_parse_error(r);
return 1;
- } else if (STR_IN_SET(name, "StateDirectorySymlink", "RuntimeDirectorySymlink", "CacheDirectorySymlink", "LogsDirectorySymlink")) {
+ } else if (STR_IN_SET(name,
+ "StateDirectorySymlink", "RuntimeDirectorySymlink", "CacheDirectorySymlink",
+ "LogsDirectorySymlink", "ConfigurationDirectorySymlink")) {
const char *a, *p;
uint64_t symlink_flags;
@@ -2110,8 +2112,16 @@ static int print_property(
if (r < 0)
return bus_log_parse_error(r);
- while ((r = sd_bus_message_read(m, "(sst)", &a, &p, &symlink_flags)) > 0)
- bus_print_property_valuef(name, expected_value, flags, "%s:%s", a, p);
+ while ((r = sd_bus_message_read(m, "(sst)", &a, &p, &symlink_flags)) > 0) {
+ const char *flags_string = exec_directory_flags_to_string(symlink_flags);
+ if (flags_string)
+ bus_print_property_valuef(name, expected_value, flags, "%s:%s%s%s", a, p,
+ isempty(flags_string) ? "" : ":", flags_string);
+ else
+ /* Preserve unknown bits from a newer manager without aborting the dump. */
+ bus_print_property_valuef(name, expected_value, flags, "%s:%s:%" PRIu64,
+ a, p, symlink_flags);
+ }
if (r < 0)
return bus_log_parse_error(r);
diff --git a/src/test/test-bus-unit-util.c b/src/test/test-bus-unit-util.c
index 1f7ca960fee..d60997c39ce 100644
--- a/src/test/test-bus-unit-util.c
+++ b/src/test/test-bus-unit-util.c
@@ -16,6 +16,112 @@
static sd_bus *arg_bus = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_bus, sd_bus_unrefp);
+static void assert_directory_property(
+ sd_bus_message *m,
+ const char *property,
+ const char* const* sources,
+ const char *destination,
+ uint64_t flags) {
+
+ const char *name;
+
+ ASSERT_OK_POSITIVE(sd_bus_message_enter_container(m, 'r', "sv"));
+ ASSERT_OK_POSITIVE(sd_bus_message_read(m, "s", &name));
+ ASSERT_STREQ(name, property);
+ ASSERT_OK_POSITIVE(sd_bus_message_enter_container(m, 'v', destination ? "a(sst)" : "as"));
+
+ if (destination) {
+ ASSERT_OK_POSITIVE(sd_bus_message_enter_container(m, 'a', "(sst)"));
+ STRV_FOREACH(source, sources) {
+ const char *actual_source, *actual_destination;
+ uint64_t actual_flags;
+
+ ASSERT_OK_POSITIVE(sd_bus_message_read(m, "(sst)",
+ &actual_source, &actual_destination, &actual_flags));
+ ASSERT_STREQ(actual_source, *source);
+ ASSERT_STREQ(actual_destination, destination);
+ ASSERT_EQ(actual_flags, flags);
+ }
+ ASSERT_OK_POSITIVE(sd_bus_message_at_end(m, false));
+ ASSERT_OK(sd_bus_message_exit_container(m));
+ } else {
+ _cleanup_strv_free_ char **actual = NULL;
+
+ ASSERT_OK_POSITIVE(sd_bus_message_read_strv(m, &actual));
+ ASSERT_EQ(strv_length(actual), strv_length((char**) sources));
+ for (size_t i = 0; i < strv_length(actual); i++)
+ ASSERT_STREQ(actual[i], sources[i]);
+ }
+
+ ASSERT_OK_POSITIVE(sd_bus_message_at_end(m, false));
+ ASSERT_OK(sd_bus_message_exit_container(m));
+ ASSERT_OK_POSITIVE(sd_bus_message_at_end(m, false));
+ ASSERT_OK(sd_bus_message_exit_container(m));
+}
+
+static void test_directory_assignment_one(
+ const char *field,
+ const char *value,
+ const char *property,
+ const char* const* sources,
+ const char *destination,
+ uint64_t flags) {
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_free_ char *assignment = NULL;
+
+ ASSERT_NOT_NULL(assignment = strjoin(field, "=", value));
+ ASSERT_OK(sd_bus_message_new(arg_bus, &m, SD_BUS_MESSAGE_METHOD_CALL));
+ ASSERT_EQ(bus_append_unit_property_assignment(m, UNIT_SERVICE, assignment), 1);
+ ASSERT_OK(sd_bus_message_seal(m, 1, 0));
+ ASSERT_OK(sd_bus_message_rewind(m, true));
+ assert_directory_property(m, property, sources, destination, flags);
+ ASSERT_OK_POSITIVE(sd_bus_message_at_end(m, true));
+}
+
+TEST(directory_properties) {
+ static const struct {
+ const char *field;
+ const char *symlink_field;
+ } properties[] = {
+ { "RuntimeDirectory", "RuntimeDirectorySymlink" },
+ { "StateDirectory", "StateDirectorySymlink" },
+ { "CacheDirectory", "CacheDirectorySymlink" },
+ { "LogsDirectory", "LogsDirectorySymlink" },
+ { "ConfigurationDirectory", "ConfigurationDirectorySymlink" },
+ };
+
+ FOREACH_ELEMENT(p, properties) {
+ test_directory_assignment_one(p->field, "myapp subdir", p->field,
+ STRV_MAKE_CONST("myapp", "subdir"), /* destination= */ NULL, 0);
+ test_directory_assignment_one(p->field, "myapp\\\\:colon", p->field,
+ STRV_MAKE_CONST("myapp:colon"), /* destination= */ NULL, 0);
+ test_directory_assignment_one(p->field, "", p->field,
+ /* sources= */ NULL, /* destination= */ NULL, 0);
+ test_directory_assignment_one(p->field, "myapp::ro", p->symlink_field,
+ STRV_MAKE_CONST("myapp"), "", EXEC_DIRECTORY_READ_ONLY);
+
+ if (!streq(p->field, "ConfigurationDirectory")) {
+ test_directory_assignment_one(p->field, "myapp:link", p->symlink_field,
+ STRV_MAKE_CONST("myapp"), "link", 0);
+ test_directory_assignment_one(p->field, "myapp:link:ro", p->symlink_field,
+ STRV_MAKE_CONST("myapp"), "link", EXEC_DIRECTORY_READ_ONLY);
+ }
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ _cleanup_free_ char *assignment = NULL;
+
+ ASSERT_NOT_NULL(assignment = strjoin(p->field, "=plain myapp::ro"));
+ ASSERT_OK(sd_bus_message_new(arg_bus, &m, SD_BUS_MESSAGE_METHOD_CALL));
+ ASSERT_EQ(bus_append_unit_property_assignment(m, UNIT_SERVICE, assignment), 1);
+ ASSERT_OK(sd_bus_message_seal(m, 1, 0));
+ ASSERT_OK(sd_bus_message_rewind(m, true));
+ assert_directory_property(m, p->field, STRV_MAKE_CONST("plain"), /* destination= */ NULL, 0);
+ assert_directory_property(m, p->symlink_field, STRV_MAKE_CONST("myapp"), "", EXEC_DIRECTORY_READ_ONLY);
+ ASSERT_OK_POSITIVE(sd_bus_message_at_end(m, true));
+ }
+}
+
TEST(exec_directory_flags) {
ASSERT_STREQ(exec_directory_flags_to_string(0), "");
ASSERT_STREQ(exec_directory_flags_to_string(EXEC_DIRECTORY_READ_ONLY), "ro");
@@ -427,7 +533,6 @@ TEST(execute_properties) {
"NoExecPaths=/tmp /var/tmp",
"ExecSearchPath=/usr/bin:/usr/local/bin",
"ExtensionDirectories=/var/lib/extensions /opt/extensions",
- "ConfigurationDirectory=myapp subdir",
"SupplementaryGroups=wheel audio video",
"SystemCallArchitectures=native x86-64",
@@ -607,6 +712,27 @@ TEST(execute_properties) {
"CacheDirectory=myapp subdir",
"LogsDirectory=myapp",
"LogsDirectory=myapp subdir",
+ "ConfigurationDirectory=myapp",
+ "ConfigurationDirectory=myapp subdir",
+
+ "StateDirectory=myapp:link",
+ "StateDirectory=myapp:link:ro",
+ "StateDirectory=myapp::ro",
+ "RuntimeDirectory=myapp::ro",
+ "CacheDirectory=myapp::ro",
+ "LogsDirectory=myapp::ro",
+ /* ConfigurationDirectory= takes flags, but no symlink destination */
+ "ConfigurationDirectory=myapp::ro",
+ /* A literal colon in a directory name needs an extra level of escaping */
+ "StateDirectory=myapp\\\\:colon",
+ "ConfigurationDirectory=myapp\\\\:colon",
+ "-EINVAL ConfigurationDirectory=myapp:link",
+ "-EINVAL ConfigurationDirectory=myapp:link:ro",
+ "-EINVAL StateDirectory=myapp::rw",
+ "-EINVAL ConfigurationDirectory=myapp::rw",
+ /* An empty assignment resets the list */
+ "StateDirectory=",
+ "ConfigurationDirectory=",
"ProtectHostname=true",
"ProtectHostname=false",
diff --git a/test/units/TEST-07-PID1.exec-context.sh b/test/units/TEST-07-PID1.exec-context.sh
index 9552d9477c3..837b60c4f7d 100755
--- a/test/units/TEST-07-PID1.exec-context.sh
+++ b/test/units/TEST-07-PID1.exec-context.sh
@@ -242,20 +242,21 @@ if ! systemd-detect-virt -cq; then
fi
# {Cache,Configuration,Logs,Runtime,State}Directory=
+# All *Directory= directives support tuple syntax (directory:symlink:flags for
+# {Runtime,State,Cache,Logs}Directory=, directory::flags for ConfigurationDirectory=),
+# which requires an additional level of escaping for the colon character.
ARGUMENTS=(
-p CacheDirectory="foo/bar/baz also\ with\ spaces"
-p CacheDirectory="foo"
-p CacheDirectory="context"
-p CacheDirectoryMode="0123"
-p CacheDirectoryMode="0666"
- -p ConfigurationDirectory="context/foo also_context/bar context/nested/baz context/semi\:colon"
+ -p ConfigurationDirectory="context/foo also_context/bar context/nested/baz context/semi\\\:colon"
-p ConfigurationDirectoryMode="0400"
-p LogsDirectory="context/foo"
-p LogsDirectory=""
-p LogsDirectory="context/a/very/nested/logs/dir"
-p RuntimeDirectory="context/with\ spaces"
- # Note: {Runtime,State,Cache,Logs}Directory= directives support the directory:symlink syntax, which
- # requires an additional level of escaping for the colon character
-p RuntimeDirectory="also_context:a\ symlink\ with\ \\\:\ col\\\:ons\ and\ \ spaces"
-p RuntimeDirectoryPreserve=yes
-p StateDirectory="context"
@@ -270,8 +271,9 @@ systemd-run --wait --pipe "${ARGUMENTS[@]}" \
systemd-run --wait --pipe "${ARGUMENTS[@]}" \
bash -xec '[[ $CONFIGURATION_DIRECTORY == /etc/also_context/bar:/etc/context/foo:/etc/context/nested/baz:/etc/context/semi:colon ]];
[[ $(stat -c "%a" "${CONFIGURATION_DIRECTORY%%:*}") == 400 ]]'
+# Note: the empty LogsDirectory= above resets the list, so context/foo is gone
systemd-run --wait --pipe "${ARGUMENTS[@]}" \
- bash -xec '[[ $LOGS_DIRECTORY == /var/log/context/a/very/nested/logs/dir:/var/log/context/foo ]];
+ bash -xec '[[ $LOGS_DIRECTORY == /var/log/context/a/very/nested/logs/dir ]];
[[ $(stat -c "%a" "${LOGS_DIRECTORY##*:}") == 755 ]]'
systemd-run --wait --pipe "${ARGUMENTS[@]}" \
bash -xec '[[ $RUNTIME_DIRECTORY == "/run/also_context:/run/context/with spaces" ]];
diff --git a/test/units/TEST-23-UNIT-FILE.statedir.sh b/test/units/TEST-23-UNIT-FILE.statedir.sh
index b6e5abd6a1f..5d4e6184798 100755
--- a/test/units/TEST-23-UNIT-FILE.statedir.sh
+++ b/test/units/TEST-23-UNIT-FILE.statedir.sh
@@ -6,11 +6,28 @@
set -eux
set -o pipefail
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
# Test unit configuration/state/cache/log/runtime data cleanup
export HOME=/root
export XDG_RUNTIME_DIR=/run/user/0
+at_exit() {
+ set +e
+
+ systemctl --user stop test-execdir-flags.service test-execdir-colon.service test-execdir-raw.service
+ systemctl --user reset-failed test-execdir-flags.service test-execdir-colon.service test-execdir-raw.service
+ rm -fr "$HOME"/.config/corge "$HOME"/.config/foo "$HOME"/.config/link "$HOME"/.config/quux* "$HOME"/.local/state/bar "$HOME"/.local/state/foo \
+ "$HOME"/.local/state/grault* "$HOME"/.local/state/private \
+ "$HOME"/.local/state/waldo "$HOME"/.config/grault* \
+ "$HOME"/.local/state/execdir-test "$HOME"/.config/execdir-test \
+ "$HOME"/.cache/execdir-test "$HOME"/.local/state/log/execdir-test "$XDG_RUNTIME_DIR"/execdir-test
+}
+
+trap at_exit EXIT
+
systemctl start user@0.service
( ! test -d "$HOME"/.local/state/foo)
@@ -63,3 +80,97 @@ test -L "$HOME"/.local/state/bar
rm "$HOME"/.local/state/foo
rmdir "$HOME"/.config/foo
+
+# ConfigurationDirectory= accepts the flags field too, but no symlink destination
+assert_fail systemd-run --user -p ConfigurationDirectory=quux::ro --wait bash -c "echo foo >$HOME/.config/quux/baz"
+test -d "$HOME"/.config/quux
+( ! test -f "$HOME"/.config/quux/baz)
+assert_fail systemd-run --user -p ConfigurationDirectory=quux:link --wait true
+
+# Bypass the client parser to exercise the manager-side validation. Include a valid
+# ExecStart= so an unrelated missing-command error cannot make the test pass.
+if output=$(busctl --user call \
+ org.freedesktop.systemd1 /org/freedesktop/systemd1 \
+ org.freedesktop.systemd1.Manager StartTransientUnit \
+ 'ssa(sv)a(sa(sv))' test-execdir-raw.service fail 2 \
+ ExecStart 'a(sasb)' 1 /usr/bin/true 1 /usr/bin/true false \
+ ConfigurationDirectorySymlink 'a(sst)' 1 execdir-test/source execdir-test/link 0 \
+ 0 2>&1); then
+ echo 'Manager accepted a ConfigurationDirectorySymlink destination' >&2
+ exit 1
+fi
+assert_in 'Symlink destination is not supported for ConfigurationDirectory=' "$output"
+
+# A 'private' source is refused by the manager
+assert_fail systemd-run --user -p StateDirectory=private/waldo::ro --wait true
+( ! test -e "$HOME"/.local/state/private)
+
+# The reserved directory must not be created through a symlink destination either.
+for type in Runtime State Cache Logs; do
+ for destination in private private/execdir-test; do
+ assert_fail systemd-run --user -p "${type}Directory=execdir-test/source:$destination" --wait true
+ done
+done
+
+# An empty assignment resets the list, so nothing is created below
+systemd-run --user -p ConfigurationDirectory=corge -p ConfigurationDirectory= --wait true
+( ! test -e "$HOME"/.config/corge)
+
+# A flags field without a symlink destination must round-trip through the transient
+# unit, i.e. it must not end up serialized with a literal "(null)" destination
+systemd-run --user --unit=test-execdir-flags -p StateDirectory=waldo::ro -p ConfigurationDirectory=quux::ro sleep infinity
+systemctl --user cat test-execdir-flags.service | grep "^StateDirectory=waldo::ro$" >/dev/null
+systemctl --user cat test-execdir-flags.service | grep "^ConfigurationDirectory=quux::ro$" >/dev/null
+systemctl --user daemon-reload
+assert_eq "$(systemctl --user show -P StateDirectorySymlink test-execdir-flags.service)" "waldo::ro"
+assert_eq "$(systemctl --user show -P ConfigurationDirectorySymlink test-execdir-flags.service)" "quux::ro"
+
+# A literal colon in a directory name must survive the transient drop-in round-trip
+systemd-run --user --unit=test-execdir-colon \
+ -p 'StateDirectory=grault\\:garply' -p 'ConfigurationDirectory=grault\\:garply' sleep infinity
+assert_eq "$(systemctl --user show -P StateDirectory test-execdir-colon.service)" "grault:garply"
+assert_eq "$(systemctl --user show -P ConfigurationDirectory test-execdir-colon.service)" "grault:garply"
+systemctl --user daemon-reload
+assert_eq "$(systemctl --user show -P StateDirectory test-execdir-colon.service)" "grault:garply"
+assert_eq "$(systemctl --user show -P ConfigurationDirectory test-execdir-colon.service)" "grault:garply"
+
+# Exercise both D-Bus signatures with names that need escaping at both the transient
+# unit file and executor serialization layers. Read the typed property to avoid
+# confusing systemctl's display escaping with the actual stored path.
+for property in StateDirectory ConfigurationDirectory StateDirectorySymlink ConfigurationDirectorySymlink; do
+ for name in 'with space' 'with"quote' 'with:colon'; do
+ source="execdir-test/$name"
+ destination=''
+ if [[ "$property" == *Symlink ]]; then
+ if [[ "$property" == StateDirectorySymlink ]]; then
+ destination="execdir-test/link-$name"
+ fi
+ value=('a(sst)' 1 "$source" "$destination" 1)
+ else
+ value=(as 1 "$source")
+ fi
+
+ busctl --user call \
+ org.freedesktop.systemd1 /org/freedesktop/systemd1 \
+ org.freedesktop.systemd1.Manager StartTransientUnit \
+ 'ssa(sv)a(sa(sv))' test-execdir-raw.service fail 4 \
+ Type s oneshot RemainAfterExit b true \
+ ExecStart 'a(sasb)' 1 /usr/bin/true 1 /usr/bin/true false \
+ "$property" "${value[@]}" 0
+ systemctl --user start test-execdir-raw.service
+ for phase in before after; do
+ if [[ "$phase" == after ]]; then
+ systemctl --user daemon-reload
+ systemctl --user restart test-execdir-raw.service
+ fi
+ assert_eq "$(busctl --user --json=short get-property \
+ org.freedesktop.systemd1 /org/freedesktop/systemd1/unit/test_2dexecdir_2draw_2eservice \
+ org.freedesktop.systemd1.Service "$property" | jq -r \
+ 'if .type == "as" then .data[0] else .data[0][0] end')" "$source"
+ if [[ "$property" == *Symlink ]]; then
+ assert_eq "$(systemctl --user show -P "$property" test-execdir-raw.service)" "$source:$destination:ro"
+ fi
+ done
+ systemctl --user stop test-execdir-raw.service
+ done
+done