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