From 890bdd1d77f773744612a0da53ce9411f71a13b0 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Mon, 28 Oct 2024 19:58:58 +0000 Subject: [PATCH] core: add read-only flag for exec directories When an exec directory is shared between services, this allows one of the service to be the producer of files, and the other the consumer, without letting the consumer modify the shared files. This will be especially useful in conjunction with id-mapped exec directories so that fully sandboxed services can share directories in one direction, safely. --- man/org.freedesktop.systemd1.xml | 8 +++- man/systemd.exec.xml | 18 +++++--- src/core/dbus-execute.c | 54 ++++++++++++++++-------- src/core/exec-invoke.c | 13 +++--- src/core/execute-serialize.c | 23 +++++++--- src/core/execute.c | 21 +++++++-- src/core/execute.h | 5 ++- src/core/load-fragment.c | 27 +++++++----- src/shared/bus-unit-util.c | 49 +++++++++++++++++---- src/shared/bus-unit-util.h | 10 +++++ test/units/TEST-23-UNIT-FILE.statedir.sh | 5 +++ test/units/TEST-34-DYNAMICUSERMIGRATE.sh | 16 ++++++- 12 files changed, 187 insertions(+), 62 deletions(-) diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index f484f28a70b..7ade8c3e8bd 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 f84204d247e..d5f85ed85c7 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 4f627a11e24..a9a73b599b4 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 feea7897eff..968e9bd7c4d 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 39369e22b83..6fa0b219683 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); + r = parse_boolean(only_create); if (r < 0) return r; + if (r > 0) + exec_directory_flags |= EXEC_DIRECTORY_ONLY_CREATE; - r = parse_boolean(only_create); + r = parse_boolean(read_only); + if (r < 0) + return r; + if (r > 0) + exec_directory_flags |= EXEC_DIRECTORY_READ_ONLY; + + r = exec_directory_add(&c->directories[dt], path, /* symlink= */ NULL, exec_directory_flags); if (r < 0) return r; - c->directories[dt].items[c->directories[dt].n_items - 1].only_create = r; if (isempty(p)) continue; diff --git a/src/core/execute.c b/src/core/execute.c index a081c429380..1c41b39a2fd 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 1fc7e6d7853..2f5abc9785b 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 b059e515ef6..1d813332b10 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 2e17bae51af..90b6f233e21 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); - - r = strv_consume_pair(&symlinks, TAKE_PTR(source), TAKE_PTR(destination)); + ExecDirectoryFlags exec_directory_flags = exec_directory_flags_from_string(flags); + if (exec_directory_flags < 0 || (exec_directory_flags & ~_EXEC_DIRECTORY_FLAGS_PUBLIC) != 0) + return log_error_errno(r, "Failed to parse flags: %s", flags); + + if (!isempty(dest)) { + path_simplify(dest); + r = strv_consume_pair(&symlinks_ro, TAKE_PTR(source), TAKE_PTR(dest)); + } else + r = strv_consume(&sources_ro, TAKE_PTR(source)); if (r < 0) return log_oom(); } @@ -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 6f0d55971ce..8090b36e3d5 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 b592314a09c..cc30f0a27df 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 d15b675e265..d6bdfb07416 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 <