From ef23a1e3fd58ea61bbd98368975862dc2c0013d2 Mon Sep 17 00:00:00 2001 From: umutcagand Date: Fri, 16 Oct 2026 16:58:14 +0300 Subject: [PATCH] core,shared: support ConfigurationDirectory flags over D-Bus Expose ConfigurationDirectorySymlink with the same a(sst) signature as the other directory types and route transient ConfigurationDirectory= assignments through the tuple parser. Keep the name and signature for uniform client handling; its destination is always empty because configuration directories do not support symlinks. Preserve the plain as property for ordinary names and empty assignments. Emit empty arrays for resets of all five directory types, including the four types whose client previously discarded empty assignments. Show tuple flags in systemctl, retaining unknown 64-bit values numerically for compatibility with newer managers. Document literal-colon escaping, which now needs an extra level for transient ConfigurationDirectory=, and the changed output/reset behavior in NEWS. Read client messages back to check property names, signatures, fields and empty arrays. Cover flags, raw D-Bus validation and reload/restart path round-trips in TEST-23, resetting failed transient units during cleanup. Fixes #43611. --- NEWS | 19 ++++ man/org.freedesktop.systemd1.xml | 37 +++++-- man/systemd.exec.xml | 8 ++ src/core/dbus-execute.c | 1 + src/shared/bus-unit-util.c | 40 +++---- src/systemctl/systemctl-show.c | 16 ++- src/test/test-bus-unit-util.c | 128 ++++++++++++++++++++++- test/units/TEST-07-PID1.exec-context.sh | 10 +- test/units/TEST-23-UNIT-FILE.statedir.sh | 111 ++++++++++++++++++++ 9 files changed, 335 insertions(+), 35 deletions(-) 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