From cd48e23f6a33c9acb47a06b99d9bdc84ee42cebe Mon Sep 17 00:00:00 2001 From: Richard Phibel Date: Mon, 7 Nov 2022 17:13:15 +0100 Subject: [PATCH] core: add OpenFile setting --- man/org.freedesktop.systemd1.xml | 6 + man/systemd.service.xml | 31 +++++ src/core/dbus-service.c | 80 +++++++++++ src/core/execute.c | 154 +++++++++++++++++++- src/core/execute.h | 3 + src/core/load-fragment-gperf.gperf.in | 1 + src/core/load-fragment.c | 37 +++++ src/core/load-fragment.h | 1 + src/core/service.c | 20 +++ src/core/service.h | 3 + src/shared/bus-unit-util.c | 21 +++ src/shared/meson.build | 2 + src/shared/open-file.c | 150 ++++++++++++++++++++ src/shared/open-file.h | 36 +++++ src/systemctl/systemctl-show.c | 33 +++++ src/test/meson.build | 2 + src/test/test-load-fragment.c | 45 ++++++ src/test/test-open-file.c | 185 +++++++++++++++++++++++++ test/TEST-77-OPENFILE/Makefile | 1 + test/TEST-77-OPENFILE/test.sh | 16 +++ test/units/testsuite-77-netcat.service | 7 + test/units/testsuite-77-netcat.sh | 4 + test/units/testsuite-77-run.sh | 14 ++ test/units/testsuite-77-socket.service | 9 ++ test/units/testsuite-77-socket.sh | 14 ++ test/units/testsuite-77.service | 10 ++ test/units/testsuite-77.sh | 35 +++++ 27 files changed, 914 insertions(+), 6 deletions(-) create mode 100644 src/shared/open-file.c create mode 100644 src/shared/open-file.h create mode 100644 src/test/test-open-file.c create mode 120000 test/TEST-77-OPENFILE/Makefile create mode 100755 test/TEST-77-OPENFILE/test.sh create mode 100644 test/units/testsuite-77-netcat.service create mode 100755 test/units/testsuite-77-netcat.sh create mode 100755 test/units/testsuite-77-run.sh create mode 100644 test/units/testsuite-77-socket.service create mode 100755 test/units/testsuite-77-socket.sh create mode 100644 test/units/testsuite-77.service create mode 100755 test/units/testsuite-77.sh diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 32ead7f272d..5154638c337 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2576,6 +2576,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { readonly u NRestarts = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s OOMPolicy = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly a(sst) OpenFile = [...]; readonly t ExecMainStartTimestamp = ...; readonly t ExecMainStartTimestampMonotonic = ...; readonly t ExecMainExitTimestamp = ...; @@ -3173,6 +3175,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -3729,6 +3733,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + diff --git a/man/systemd.service.xml b/man/systemd.service.xml index 1c9e59f7229..e327f688f43 100644 --- a/man/systemd.service.xml +++ b/man/systemd.service.xml @@ -1156,6 +1156,37 @@ kills, this setting determines the state of the unit after systemd-oomd kills a cgroup associated with it. + + OpenFile= + Takes an argument of the form path:fd-name:options, + where: + + path is a path to a file or an AF_UNIX socket in the file system; + fd-name is a name that will be associated with the file descriptor; + the name may contain any ASCII character, but must exclude control characters and ":", and must be at most 255 characters in length; + it is optional and, if not provided, defaults to the file name; + options is a comma-separated list of access options; + possible values are + read-only, + append, + truncate, + graceful; + if not specified, files will be opened in rw mode; + if graceful is specified, errors during file/socket opening are ignored. + Specifying the same option several times is treated as an error. + + The file or socket is opened by the service manager and the file descriptor is passed to the service. + If the path is a socket, we call connect() on it. + See sd_listen_fds3 + for more details on how to retrieve these file descriptors. + + This setting is useful to allow services to access files/sockets that they can't access themselves + (due to running in a separate mount namespace, not having privileges, ...). + + This setting can be specified multiple times, in which case all the specified paths are opened and the file descriptors passed to the service. + If the empty string is assigned, the entire list of open files defined prior to this is reset. + + Check diff --git a/src/core/dbus-service.c b/src/core/dbus-service.c index 6e4bc0bd1a3..0d437afe6b6 100644 --- a/src/core/dbus-service.c +++ b/src/core/dbus-service.c @@ -17,6 +17,7 @@ #include "fileio.h" #include "locale-util.h" #include "mount-util.h" +#include "open-file.h" #include "parse-util.h" #include "path-util.h" #include "selinux-access.h" @@ -36,6 +37,34 @@ static BUS_DEFINE_PROPERTY_GET(property_get_timeout_abort_usec, "t", Service, se static BUS_DEFINE_PROPERTY_GET(property_get_watchdog_usec, "t", Service, service_get_watchdog_usec); static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_timeout_failure_mode, service_timeout_failure_mode, ServiceTimeoutFailureMode); +static int property_get_open_files( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + OpenFile **open_files = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "(sst)"); + if (r < 0) + return r; + + LIST_FOREACH(open_files, of, *open_files) { + r = sd_bus_message_append(reply, "(sst)", of->path, of->fdname, of->flags); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + static int property_get_exit_status_set( sd_bus *bus, const char *path, @@ -228,6 +257,7 @@ const sd_bus_vtable bus_service_vtable[] = { SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(Unit, ref_gid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("NRestarts", "u", bus_property_get_unsigned, offsetof(Service, n_restarts), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY("OOMPolicy", "s", bus_property_get_oom_policy, offsetof(Service, oom_policy), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("OpenFile", "a(sst)", property_get_open_files, offsetof(Service, open_files), SD_BUS_VTABLE_PROPERTY_CONST), BUS_EXEC_STATUS_VTABLE("ExecMain", offsetof(Service, main_exec_status), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), BUS_EXEC_COMMAND_LIST_VTABLE("ExecCondition", offsetof(Service, exec_command[SERVICE_EXEC_CONDITION]), SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION), @@ -532,6 +562,56 @@ static int bus_service_set_transient_property( if (streq(name, "StandardErrorFileDescriptor")) return bus_set_transient_std_fd(u, name, &s->stderr_fd, &s->exec_context.stdio_as_fds, message, flags, error); + if (streq(name, "OpenFile")) { + const char *path, *fdname; + uint64_t offlags; + + r = sd_bus_message_enter_container(message, 'a', "(sst)"); + if (r < 0) + return r; + + while ((r = sd_bus_message_read(message, "(sst)", &path, &fdname, &offlags)) > 0) { + _cleanup_(open_file_freep) OpenFile *of = NULL; + _cleanup_free_ char *ofs = NULL; + + of = new(OpenFile, 1); + if (!of) + return -ENOMEM; + + *of = (OpenFile) { + .path = strdup(path), + .fdname = strdup(fdname), + .flags = offlags, + }; + + if (!of->path || !of->fdname) + return -ENOMEM; + + r = open_file_validate(of); + if (r < 0) + return r; + + if (UNIT_WRITE_FLAGS_NOOP(flags)) + continue; + + r = open_file_to_string(of, &ofs); + if (r < 0) + return sd_bus_error_set_errnof( + error, r, "Failed to convert OpenFile= value to string: %m"); + + LIST_APPEND(open_files, s->open_files, TAKE_PTR(of)); + unit_write_settingf(u, flags | UNIT_ESCAPE_SPECIFIERS, name, "OpenFile=%s", ofs); + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + return 1; + } + return 0; } diff --git a/src/core/execute.c b/src/core/execute.c index 439f491d024..4c440895c19 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -151,11 +151,14 @@ static int shift_fds(int fds[], size_t n_fds) { return 0; } -static int flags_fds(const int fds[], size_t n_socket_fds, size_t n_storage_fds, bool nonblock) { - size_t n_fds; +static int flags_fds( + const int fds[], + size_t n_socket_fds, + size_t n_fds, + bool nonblock) { + int r; - n_fds = n_socket_fds + n_storage_fds; if (n_fds <= 0) return 0; @@ -1805,6 +1808,7 @@ static int build_environment( const ExecContext *c, const ExecParameters *p, size_t n_fds, + char **fdnames, const char *home, const char *username, const char *shell, @@ -1837,7 +1841,7 @@ static int build_environment( return -ENOMEM; our_env[n_env++] = x; - joined = strv_join(p->fd_names, ":"); + joined = strv_join(fdnames, ":"); if (!joined) return -ENOMEM; @@ -4105,6 +4109,123 @@ static int add_shifted_fd(int *fds, size_t fds_size, size_t *n_fds, int fd, int return 1; } +static int connect_unix_harder(Unit *u, const OpenFile *of, int ofd) { + union sockaddr_union addr = { + .un.sun_family = AF_UNIX, + }; + socklen_t sa_len; + static const int socket_types[] = { SOCK_DGRAM, SOCK_STREAM, SOCK_SEQPACKET }; + int r; + + assert(u); + assert(of); + assert(ofd >= 0); + + r = sockaddr_un_set_path(&addr.un, FORMAT_PROC_FD_PATH(ofd)); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to set sockaddr for %s: %m", of->path); + + sa_len = r; + + for (size_t i = 0; i < ELEMENTSOF(socket_types); i++) { + _cleanup_close_ int fd = -EBADF; + + fd = socket(AF_UNIX, socket_types[i] | SOCK_CLOEXEC, 0); + if (fd < 0) + return log_unit_error_errno(u, errno, "Failed to create socket for %s: %m", of->path); + + r = RET_NERRNO(connect(fd, &addr.sa, sa_len)); + if (r == -EPROTOTYPE) + continue; + if (r < 0) + return log_unit_error_errno(u, r, "Failed to connect socket for %s: %m", of->path); + + return TAKE_FD(fd); + } + + return log_unit_error_errno(u, SYNTHETIC_ERRNO(EPROTOTYPE), "Failed to connect socket for \"%s\".", of->path); +} + +static int get_open_file_fd(Unit *u, const OpenFile *of) { + struct stat st; + _cleanup_close_ int fd = -EBADF, ofd = -EBADF; + + assert(u); + assert(of); + + ofd = open(of->path, O_PATH | O_CLOEXEC); + if (ofd < 0) + return log_error_errno(errno, "Could not open \"%s\": %m", of->path); + if (fstat(ofd, &st) < 0) + return log_error_errno(errno, "Failed to stat %s: %m", of->path); + + if (S_ISSOCK(st.st_mode)) { + fd = connect_unix_harder(u, of, ofd); + if (fd < 0) + return fd; + + if (FLAGS_SET(of->flags, OPENFILE_READ_ONLY) && shutdown(fd, SHUT_WR) < 0) + return log_error_errno(errno, "Failed to shutdown send for socket %s: %m", of->path); + + log_unit_debug(u, "socket %s opened (fd=%d)", of->path, fd); + } else { + int flags = FLAGS_SET(of->flags, OPENFILE_READ_ONLY) ? O_RDONLY : O_RDWR; + if (FLAGS_SET(of->flags, OPENFILE_APPEND)) + flags |= O_APPEND; + else if (FLAGS_SET(of->flags, OPENFILE_TRUNCATE)) + flags |= O_TRUNC; + + fd = fd_reopen(ofd, flags | O_CLOEXEC); + if (fd < 0) + return log_unit_error_errno(u, fd, "Failed to open file %s: %m", of->path); + + log_unit_debug(u, "file %s opened (fd=%d)", of->path, fd); + } + + return TAKE_FD(fd); +} + +static int collect_open_file_fds( + Unit *u, + OpenFile* open_files, + int **fds, + char ***fdnames, + size_t *n_fds) { + int r; + + assert(u); + assert(fds); + assert(fdnames); + assert(n_fds); + + LIST_FOREACH(open_files, of, open_files) { + _cleanup_close_ int fd = -EBADF; + + fd = get_open_file_fd(u, of); + if (fd < 0) { + if (FLAGS_SET(of->flags, OPENFILE_GRACEFUL)) { + log_unit_debug_errno(u, fd, "Failed to get OpenFile= file descriptor for %s, ignoring: %m", of->path); + continue; + } + + return fd; + } + + if (!GREEDY_REALLOC(*fds, *n_fds + 1)) + return -ENOMEM; + + r = strv_extend(fdnames, of->fdname); + if (r < 0) + return r; + + (*fds)[*n_fds] = TAKE_FD(fd); + + (*n_fds)++; + } + + return 0; +} + static int exec_child( Unit *unit, const ExecCommand *command, @@ -4114,7 +4235,7 @@ static int exec_child( DynamicCreds *dcreds, int socket_fd, const int named_iofds[static 3], - int *fds, + int *params_fds, size_t n_socket_fds, size_t n_storage_fds, char **files_env, @@ -4154,6 +4275,8 @@ static int exec_child( int secure_bits; _cleanup_free_ gid_t *gids_after_pam = NULL; int ngids_after_pam = 0; + _cleanup_free_ int *fds = NULL; + _cleanup_strv_free_ char **fdnames = NULL; assert(unit); assert(command); @@ -4196,6 +4319,24 @@ static int exec_child( /* In case anything used libc syslog(), close this here, too */ closelog(); + fds = newdup(int, params_fds, n_fds); + if (!fds) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + + fdnames = strv_copy((char**) params->fd_names); + if (!fdnames) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + + r = collect_open_file_fds(unit, params->open_files, &fds, &fdnames, &n_fds); + if (r < 0) { + *exit_status = EXIT_FDS; + return log_unit_error_errno(unit, r, "Failed to get OpenFile= file descriptors: %m"); + } + int keep_fds[n_fds + 3]; memcpy_safe(keep_fds, fds, n_fds * sizeof(int)); n_keep_fds = n_fds; @@ -4551,6 +4692,7 @@ static int exec_child( context, params, n_fds, + fdnames, home, username, shell, @@ -4843,7 +4985,7 @@ static int exec_child( if (r >= 0) r = shift_fds(fds, n_fds); if (r >= 0) - r = flags_fds(fds, n_socket_fds, n_storage_fds, context->non_blocking); + r = flags_fds(fds, n_socket_fds, n_fds, context->non_blocking); if (r < 0) { *exit_status = EXIT_FDS; return log_unit_error_errno(unit, r, "Failed to adjust passed file descriptors: %m"); diff --git a/src/core/execute.h b/src/core/execute.h index 24cd4640d7d..62ad6d2eb2a 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -23,6 +23,7 @@ typedef struct Manager Manager; #include "namespace.h" #include "nsflags.h" #include "numa-util.h" +#include "open-file.h" #include "path-util.h" #include "set.h" #include "time-util.h" @@ -427,6 +428,8 @@ struct ExecParameters { int exec_fd; const char *notify_socket; + + LIST_HEAD(OpenFile, open_files); }; #include "unit.h" diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 2850da5cc10..81eb586cfee 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -426,6 +426,7 @@ Service.BusPolicy, config_parse_warn_compat, Service.USBFunctionDescriptors, config_parse_unit_path_printf, 0, offsetof(Service, usb_function_descriptors) Service.USBFunctionStrings, config_parse_unit_path_printf, 0, offsetof(Service, usb_function_strings) Service.OOMPolicy, config_parse_oom_policy, 0, offsetof(Service, oom_policy) +Service.OpenFile, config_parse_open_file, 0, offsetof(Service, open_files) {{ EXEC_CONTEXT_CONFIG_ITEMS('Service') }} {{ CGROUP_CONTEXT_CONFIG_ITEMS('Service') }} {{ KILL_CONTEXT_CONFIG_ITEMS('Service') }} diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index e115aa62706..6c55bc5187c 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -49,6 +49,7 @@ #include "missing_ioprio.h" #include "mountpoint-util.h" #include "nulstr-util.h" +#include "open-file.h" #include "parse-helpers.h" #include "parse-util.h" #include "path-util.h" @@ -6532,3 +6533,39 @@ int config_parse_log_filter_patterns( return 0; } + +int config_parse_open_file( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(open_file_freep) OpenFile *of = NULL; + OpenFile **head = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + open_file_free_many(head); + return 0; + } + + r = open_file_parse(rvalue, &of); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse OpenFile= setting, ignoring: %s", rvalue); + return 0; + } + + LIST_APPEND(open_files, *head, TAKE_PTR(of)); + + return 0; +} diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index 74b36336950..11d43dda923 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -151,6 +151,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_restrict_network_interfaces); CONFIG_PARSER_PROTOTYPE(config_parse_watchdog_sec); CONFIG_PARSER_PROTOTYPE(config_parse_tty_size); CONFIG_PARSER_PROTOTYPE(config_parse_log_filter_patterns); +CONFIG_PARSER_PROTOTYPE(config_parse_open_file); /* gperf prototypes */ const struct ConfigPerfItem* load_fragment_gperf_lookup(const char *key, GPERF_LEN_TYPE length); diff --git a/src/core/service.c b/src/core/service.c index 4993fdf5e7e..0efcb9a0817 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -26,6 +26,7 @@ #include "load-fragment.h" #include "log.h" #include "manager.h" +#include "open-file.h" #include "parse-util.h" #include "path-util.h" #include "process-util.h" @@ -360,6 +361,8 @@ static void service_done(Unit *u) { assert(s); + open_file_free_many(&s->open_files); + s->pid_file = mfree(s->pid_file); s->status_text = mfree(s->status_text); @@ -925,6 +928,21 @@ static void service_dump(Unit *u, FILE *f, const char *prefix) { prefix, s->n_fd_store_max, prefix, s->n_fd_store); + if (s->open_files) + LIST_FOREACH(open_files, of, s->open_files) { + _cleanup_free_ char *ofs = NULL; + int r; + + r = open_file_to_string(of, &ofs); + if (r < 0) { + log_debug_errno(r, + "Failed to convert OpenFile= setting to string, ignoring: %m"); + continue; + } + + fprintf(f, "%sOpen File: %s\n", prefix, ofs); + } + cgroup_context_dump(UNIT(s), f, prefix); } @@ -1528,6 +1546,8 @@ static int service_spawn_internal( if (r < 0) return r; + exec_params.open_files = s->open_files; + log_unit_debug(UNIT(s), "Passing %zu fds to service", exec_params.n_socket_fds + exec_params.n_storage_fds); } diff --git a/src/core/service.h b/src/core/service.h index 91e02e6d7ee..763398e3150 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -6,6 +6,7 @@ typedef struct ServiceFDStore ServiceFDStore; #include "exit-status.h" #include "kill.h" +#include "open-file.h" #include "path.h" #include "ratelimit.h" #include "socket.h" @@ -215,6 +216,8 @@ struct Service { bool flush_n_restarts; OOMPolicy oom_policy; + + LIST_HEAD(OpenFile, open_files); }; static inline usec_t service_timeout_abort_usec(Service *s) { diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 7154c9b4c08..0fa2dea2d4e 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -29,6 +29,7 @@ #include "mountpoint-util.h" #include "nsflags.h" #include "numa-util.h" +#include "open-file.h" #include "parse-helpers.h" #include "parse-util.h" #include "path-util.h" @@ -410,6 +411,23 @@ static int bus_append_exec_command(sd_bus_message *m, const char *field, const c return 1; } +static int bus_append_open_file(sd_bus_message *m, const char *field, const char *eq) { + _cleanup_(open_file_freep) OpenFile *of = NULL; + int r; + + assert(m); + + r = open_file_parse(eq, &of); + if (r < 0) + return log_error_errno(r, "Failed to parse OpenFile= setting: %m"); + + r = sd_bus_message_append(m, "(sv)", field, "a(sst)", (size_t) 1, of->path, of->fdname, of->flags); + if (r < 0) + return bus_log_create_error(r); + + return 1; +} + static int bus_append_ip_address_access(sd_bus_message *m, int family, const union in_addr_union *prefix, unsigned char prefixlen) { int r; @@ -2300,6 +2318,9 @@ static int bus_append_service_property(sd_bus_message *m, const char *field, con return 1; } + if (streq(field, "OpenFile")) + return bus_append_open_file(m, field, eq); + return 0; } diff --git a/src/shared/meson.build b/src/shared/meson.build index 0d5e9f2dbbb..bc1cfa5b4b5 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -243,6 +243,8 @@ shared_sources = files( 'nsflags.h', 'numa-util.c', 'numa-util.h', + 'open-file.c', + 'open-file.h', 'openssl-util.c', 'openssl-util.h', 'output-mode.c', diff --git a/src/shared/open-file.c b/src/shared/open-file.c new file mode 100644 index 00000000000..dedf067c4c1 --- /dev/null +++ b/src/shared/open-file.c @@ -0,0 +1,150 @@ + +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "escape.h" +#include "extract-word.h" +#include "fd-util.h" +#include "open-file.h" +#include "path-util.h" +#include "string-table.h" +#include "string-util.h" + +int open_file_parse(const char *v, OpenFile **ret) { + _cleanup_free_ char *options = NULL; + _cleanup_(open_file_freep) OpenFile *of = NULL; + int r; + + assert(v); + assert(ret); + + of = new0(OpenFile, 1); + if (!of) + return -ENOMEM; + + r = extract_many_words(&v, ":", EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_CUNESCAPE, &of->path, &of->fdname, &options, NULL); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + /* Enforce that at most 3 colon-separated words are present */ + if (!isempty(v)) + return -EINVAL; + + for (const char *p = options;;) { + OpenFileFlag flag; + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r < 0) + return r; + if (r == 0) + break; + + flag = open_file_flags_from_string(word); + if (flag < 0) + return flag; + + if ((flag & of->flags) != 0) + return -EINVAL; + + of->flags |= flag; + } + + if (isempty(of->fdname)) { + free(of->fdname); + r = path_extract_filename(of->path, &of->fdname); + if (r < 0) + return r; + } + + r = open_file_validate(of); + if (r < 0) + return r; + + *ret = TAKE_PTR(of); + + return 0; +} + +int open_file_validate(const OpenFile *of) { + assert(of); + + if (!path_is_valid(of->path) || !path_is_absolute(of->path)) + return -EINVAL; + + if (!fdname_is_valid(of->fdname)) + return -EINVAL; + + if ((FLAGS_SET(of->flags, OPENFILE_READ_ONLY) + FLAGS_SET(of->flags, OPENFILE_APPEND) + + FLAGS_SET(of->flags, OPENFILE_TRUNCATE)) > 1) + return -EINVAL; + + if ((of->flags & ~_OPENFILE_MASK_PUBLIC) != 0) + return -EINVAL; + + return 0; +} + +int open_file_to_string(const OpenFile *of, char **ret) { + _cleanup_free_ char *options = NULL, *fname = NULL, *s = NULL; + bool has_fdname = false; + int r; + + assert(of); + assert(ret); + + s = shell_escape(of->path, ":"); + if (!s) + return -ENOMEM; + + r = path_extract_filename(of->path, &fname); + if (r < 0) + return r; + + has_fdname = !streq(fname, of->fdname); + if (has_fdname) + if (!strextend(&s, ":", of->fdname)) + return -ENOMEM; + + for (OpenFileFlag flag = OPENFILE_READ_ONLY; flag < _OPENFILE_MAX; flag <<= 1) + if (FLAGS_SET(of->flags, flag) && !strextend_with_separator(&options, ",", open_file_flags_to_string(flag))) + return -ENOMEM; + + if (options) + if (!(has_fdname ? strextend(&s, ":", options) : strextend(&s, "::", options))) + return -ENOMEM; + + *ret = TAKE_PTR(s); + + return 0; +} + +OpenFile *open_file_free(OpenFile *of) { + if (!of) + return NULL; + + free(of->path); + free(of->fdname); + return mfree(of); +} + +void open_file_free_many(OpenFile **head) { + OpenFile *of; + + while ((of = *head)) { + LIST_REMOVE(open_files, *head, of); + of = open_file_free(of); + } +} + +static const char * const open_file_flags_table[_OPENFILE_MAX] = { + [OPENFILE_READ_ONLY] = "read-only", + [OPENFILE_APPEND] = "append", + [OPENFILE_TRUNCATE] = "truncate", + [OPENFILE_GRACEFUL] = "graceful", +}; + +DEFINE_STRING_TABLE_LOOKUP(open_file_flags, OpenFileFlag); diff --git a/src/shared/open-file.h b/src/shared/open-file.h new file mode 100644 index 00000000000..bb63ec8f9c3 --- /dev/null +++ b/src/shared/open-file.h @@ -0,0 +1,36 @@ + +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "list.h" + +typedef enum OpenFileFlag { + OPENFILE_READ_ONLY = 1 << 0, + OPENFILE_APPEND = 1 << 1, + OPENFILE_TRUNCATE = 1 << 2, + OPENFILE_GRACEFUL = 1 << 3, + _OPENFILE_MAX, + _OPENFILE_INVALID = -EINVAL, + _OPENFILE_MASK_PUBLIC = OPENFILE_READ_ONLY | OPENFILE_APPEND | OPENFILE_TRUNCATE | OPENFILE_GRACEFUL, +} OpenFileFlag; + +typedef struct OpenFile { + char *path; + char *fdname; + OpenFileFlag flags; + LIST_FIELDS(struct OpenFile, open_files); +} OpenFile; + +int open_file_parse(const char *v, OpenFile **ret); + +int open_file_validate(const OpenFile *of); + +int open_file_to_string(const OpenFile *of, char **ret); + +OpenFile *open_file_free(OpenFile *of); +DEFINE_TRIVIAL_CLEANUP_FUNC(OpenFile*, open_file_free); + +void open_file_free_many(OpenFile **head); + +const char *open_file_flags_to_string(OpenFileFlag t) _const_; +OpenFileFlag open_file_flags_from_string(const char *t) _pure_; diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index f78cf307cac..4166b361dda 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -23,6 +23,7 @@ #include "locale-util.h" #include "memory-util.h" #include "numa-util.h" +#include "open-file.h" #include "parse-util.h" #include "path-util.h" #include "pretty-print.h" @@ -1857,6 +1858,38 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m if (r < 0) return bus_log_parse_error(r); + return 1; + } else if (contents[0] == SD_BUS_TYPE_STRUCT_BEGIN && streq(name, "OpenFile")) { + char *path, *fdname; + uint64_t offlags; + + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "(sst)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_read(m, "(sst)", &path, &fdname, &offlags)) > 0) { + _cleanup_free_ char *ofs = NULL; + + r = open_file_to_string( + &(OpenFile){ + .path = path, + .fdname = fdname, + .flags = offlags, + }, + &ofs); + if (r < 0) + return log_error_errno( + r, "Failed to convert OpenFile= value to string: %m"); + + bus_print_property_value(name, expected_value, flags, ofs); + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(m); + if (r < 0) + return bus_log_parse_error(r); + return 1; } diff --git a/src/test/meson.build b/src/test/meson.build index f9ee9190195..3aa98e1ffdf 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -695,6 +695,8 @@ tests += [ [files('test-hmac.c')], [files('test-sha256.c')], + + [files('test-open-file.c')], ] ############################################################ diff --git a/src/test/test-load-fragment.c b/src/test/test-load-fragment.c index 8d3c58a800e..44643731322 100644 --- a/src/test/test-load-fragment.c +++ b/src/test/test-load-fragment.c @@ -22,6 +22,7 @@ #include "load-fragment.h" #include "macro.h" #include "memory-util.h" +#include "open-file.h" #include "pcre2-util.h" #include "rm-rf.h" #include "specifier.h" @@ -1048,6 +1049,50 @@ TEST(config_parse_log_filter_patterns) { } } +TEST(config_parse_open_file) { + _cleanup_(manager_freep) Manager *m = NULL; + _cleanup_(unit_freep) Unit *u = NULL; + _cleanup_(open_file_freep) OpenFile *of = NULL; + int r; + + r = manager_new(LOOKUP_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m); + if (manager_errno_skip_test(r)) { + log_notice_errno(r, "Skipping test: manager_new: %m"); + return; + } + + assert_se(r >= 0); + assert_se(manager_startup(m, NULL, NULL, NULL) >= 0); + + assert_se(u = unit_new(m, sizeof(Service))); + assert_se(unit_add_name(u, "foobar.service") == 0); + + r = config_parse_open_file(NULL, "fake", 1, "section", 1, + "OpenFile", 0, "/proc/1/ns/mnt:host-mount-namespace:read-only", + &of, u); + assert_se(r >= 0); + assert_se(of); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == OPENFILE_READ_ONLY); + + of = open_file_free(of); + r = config_parse_open_file(NULL, "fake", 1, "section", 1, + "OpenFile", 0, "/proc/1/ns/mnt::read-only", + &of, u); + assert_se(r >= 0); + assert_se(of); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "mnt")); + assert_se(of->flags == OPENFILE_READ_ONLY); + + r = config_parse_open_file(NULL, "fake", 1, "section", 1, + "OpenFile", 0, "", + &of, u); + assert_se(r >= 0); + assert_se(!of); +} + static int intro(void) { if (enter_cgroup_subroot(NULL) == -ENOMEDIUM) return log_tests_skipped("cgroupfs not available"); diff --git a/src/test/test-open-file.c b/src/test/test-open-file.c new file mode 100644 index 00000000000..1b938ec5f73 --- /dev/null +++ b/src/test/test-open-file.c @@ -0,0 +1,185 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "open-file.h" +#include "string-util.h" +#include "tests.h" + +TEST(open_file_parse) { + _cleanup_(open_file_freep) OpenFile *of = NULL; + int r; + + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == OPENFILE_READ_ONLY); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "mnt")); + assert_se(of->flags == 0); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == 0); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt::read-only", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "mnt")); + assert_se(of->flags == OPENFILE_READ_ONLY); + + of = open_file_free(of); + r = open_file_parse("../file.dat:file:read-only", &of); + + assert_se(r == -EINVAL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:rw", &of); + + assert_se(r == -EINVAL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:append", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == OPENFILE_APPEND); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:truncate", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == OPENFILE_TRUNCATE); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,append", &of); + + assert_se(r == -EINVAL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,truncate", &of); + + assert_se(r == -EINVAL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:append,truncate", &of); + + assert_se(r == -EINVAL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,read-only", &of); + + assert_se(r == -EINVAL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:graceful", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == OPENFILE_GRACEFUL); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,graceful", &of); + + assert_se(r >= 0); + assert_se(streq(of->path, "/proc/1/ns/mnt")); + assert_se(streq(of->fdname, "host-mount-namespace")); + assert_se(of->flags == (OPENFILE_READ_ONLY | OPENFILE_GRACEFUL)); + + of = open_file_free(of); + r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only:other", &of); + + assert_se(r == -EINVAL); +} + +TEST(open_file_to_string) { + _cleanup_free_ char *s = NULL; + _cleanup_(open_file_freep) OpenFile *of = NULL; + int r; + + assert_se(of = new (OpenFile, 1)); + *of = (OpenFile){ .path = strdup("/proc/1/ns/mnt"), + .fdname = strdup("host-mount-namespace"), + .flags = OPENFILE_READ_ONLY }; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:read-only")); + + s = mfree(s); + of->flags = OPENFILE_APPEND; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:append")); + + s = mfree(s); + of->flags = OPENFILE_TRUNCATE; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:truncate")); + + s = mfree(s); + of->flags = OPENFILE_GRACEFUL; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:graceful")); + + s = mfree(s); + of->flags = OPENFILE_READ_ONLY | OPENFILE_GRACEFUL; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:read-only,graceful")); + + s = mfree(s); + of->flags = 0; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace")); + + s = mfree(s); + assert_se(free_and_strdup(&of->fdname, "mnt")); + of->flags = OPENFILE_READ_ONLY; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/proc/1/ns/mnt::read-only")); + + s = mfree(s); + assert_se(free_and_strdup(&of->path, "/path:with:colon")); + assert_se(free_and_strdup(&of->fdname, "path:with:colon")); + of->flags = 0; + + r = open_file_to_string(of, &s); + + assert_se(r >= 0); + assert_se(streq(s, "/path\\:with\\:colon")); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/test/TEST-77-OPENFILE/Makefile b/test/TEST-77-OPENFILE/Makefile new file mode 120000 index 00000000000..e9f93b1104c --- /dev/null +++ b/test/TEST-77-OPENFILE/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile \ No newline at end of file diff --git a/test/TEST-77-OPENFILE/test.sh b/test/TEST-77-OPENFILE/test.sh new file mode 100755 index 00000000000..e4349997a0a --- /dev/null +++ b/test/TEST-77-OPENFILE/test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -e + +TEST_DESCRIPTION="Openfile tests" + +# shellcheck source=test/test-functions +. "${TEST_BASE_DIR:?}/test-functions" + +test_append_files() { + local workspace="${1:?}" + echo "Open" > "$workspace/test-77-open.dat" + echo "File" > "$workspace/test-77-file.dat" +} + +do_test "$@" diff --git a/test/units/testsuite-77-netcat.service b/test/units/testsuite-77-netcat.service new file mode 100644 index 00000000000..8ae399a26ff --- /dev/null +++ b/test/units/testsuite-77-netcat.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-77-OPENFILE + +[Service] +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=simple diff --git a/test/units/testsuite-77-netcat.sh b/test/units/testsuite-77-netcat.sh new file mode 100755 index 00000000000..73b4c8794d6 --- /dev/null +++ b/test/units/testsuite-77-netcat.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later + +echo "Socket" | nc -lkU /tmp/test.sock diff --git a/test/units/testsuite-77-run.sh b/test/units/testsuite-77-run.sh new file mode 100755 index 00000000000..086044a2bc2 --- /dev/null +++ b/test/units/testsuite-77-run.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/assert.sh +. "$(dirname "$0")"/assert.sh + +export SYSTEMD_LOG_LEVEL=debug + +assert_eq "$LISTEN_FDS" "1" +assert_eq "$LISTEN_FDNAMES" "new-file" +read -r -u 3 text +assert_eq "$text" "New" diff --git a/test/units/testsuite-77-socket.service b/test/units/testsuite-77-socket.service new file mode 100644 index 00000000000..9b6cfc6e6bb --- /dev/null +++ b/test/units/testsuite-77-socket.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-77-OPENFILE + +[Service] +OpenFile=/tmp/test.sock:socket:read-only +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-77-socket.sh b/test/units/testsuite-77-socket.sh new file mode 100755 index 00000000000..0f88a6d835c --- /dev/null +++ b/test/units/testsuite-77-socket.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/assert.sh +. "$(dirname "$0")"/assert.sh + +export SYSTEMD_LOG_LEVEL=debug + +assert_eq "$LISTEN_FDS" "1" +assert_eq "$LISTEN_FDNAMES" "socket" +read -r -u 3 text +assert_eq "$text" "Socket" diff --git a/test/units/testsuite-77.service b/test/units/testsuite-77.service new file mode 100644 index 00000000000..6ed8addd770 --- /dev/null +++ b/test/units/testsuite-77.service @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=TEST-77-OPENFILE + +[Service] +OpenFile=/test-77-open.dat:open:read-only +OpenFile=/test-77-file.dat +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-77.sh b/test/units/testsuite-77.sh new file mode 100755 index 00000000000..2675f054dc9 --- /dev/null +++ b/test/units/testsuite-77.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/assert.sh +. "$(dirname "$0")"/assert.sh + +export SYSTEMD_LOG_LEVEL=debug + +assert_eq "$LISTEN_FDS" "2" +assert_eq "$LISTEN_FDNAMES" "open:test-77-file.dat" +read -r -u 3 text +assert_eq "$text" "Open" +read -r -u 4 text +assert_eq "$text" "File" + +# Test for socket +systemctl start testsuite-77-netcat.service +systemctl start testsuite-77-socket.service + +# Tests for D-Bus +diff <(systemctl show -p OpenFile testsuite-77) - < /test-77-new-file.dat +systemd-run --wait -p OpenFile=/test-77-new-file.dat:new-file:read-only "$(dirname "$0")"/testsuite-77-run.sh + +assert_rc 202 systemd-run --wait -p OpenFile=/test-77-new-file.dat:new-file:read-only -p OpenFile=/test-77-mssing-file.dat:missing-file:read-only "$(dirname "$0")"/testsuite-77-run.sh + +assert_rc 0 systemd-run --wait -p OpenFile=/test-77-new-file.dat:new-file:read-only -p OpenFile=/test-77-mssing-file.dat:missing-file:read-only,graceful "$(dirname "$0")"/testsuite-77-run.sh + +# End +touch /testok