diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index dd144a6cba..3b7cad5b47 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2790,6 +2790,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) OpenFile = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly as ExtraFileDescriptorNames = ['...', ...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i ReloadSignal = ...; readonly t ExecMainStartTimestamp = ...; readonly t ExecMainStartTimestampMonotonic = ...; @@ -4098,6 +4100,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4843,6 +4847,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { 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. + + ExtraFileDescriptorNames contains file descriptor names passed to the service via + the ExtraFileDescriptors property in the StartTransientUnit() + method. See sd_listen_fds3 + for more details on how to retrieve these file descriptors. Unlike the ExtraFileDescriptors + input property, ExtraFileDescriptorNames only contains names and not the file descriptors. @@ -12209,6 +12219,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ LiveMountResult, PrivateTmpEx, ImportCredentialEx, + ExtraFileDescriptorNames, BindLogSockets, and PrivateUsersEx were added in version 257. diff --git a/src/core/dbus-service.c b/src/core/dbus-service.c index 43a8fb0617..a0244c1860 100644 --- a/src/core/dbus-service.c +++ b/src/core/dbus-service.c @@ -69,6 +69,34 @@ static int property_get_open_files( return sd_bus_message_close_container(reply); } +static int property_get_extra_file_descriptors( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ServiceExtraFD **extra_fds = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "s"); + if (r < 0) + return r; + + LIST_FOREACH(extra_fd, efd, *extra_fds) { + r = sd_bus_message_append_basic(reply, 's', efd->fdname); + 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, @@ -339,6 +367,7 @@ const sd_bus_vtable bus_service_vtable[] = { 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), + SD_BUS_PROPERTY("ExtraFileDescriptorNames", "as", property_get_extra_file_descriptors, offsetof(Service, extra_fds), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ReloadSignal", "i", bus_property_get_int, offsetof(Service, reload_signal), SD_BUS_VTABLE_PROPERTY_CONST), BUS_EXEC_STATUS_VTABLE("ExecMain", offsetof(Service, main_exec_status), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), @@ -718,6 +747,58 @@ static int bus_service_set_transient_property( if (streq(name, "ReloadSignal")) return bus_set_transient_reload_signal(u, name, &s->reload_signal, message, flags, error); + if (streq(name, "ExtraFileDescriptors")) { + int fd; + const char *fdname; + + r = sd_bus_message_enter_container(message, 'a', "(hs)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_(service_extra_fd_freep) ServiceExtraFD *efd = NULL; + + r = sd_bus_message_read(message, "(hs)", &fd, &fdname); + if (r < 0) + return r; + if (r == 0) + break; + + /* Disallow empty string for ExtraFileDescriptors. + * Unlike OpenFile, StandardInput and friends, there isn't a good sane + * default for an arbitrary FD. */ + if (fd < 0 || isempty(fdname) || !fdname_is_valid(fdname)) + return -EINVAL; + + if (UNIT_WRITE_FLAGS_NOOP(flags)) + continue; + + efd = new(ServiceExtraFD, 1); + if (!efd) + return -ENOMEM; + + *efd = (ServiceExtraFD) { + .fd = -EBADF, + .fdname = strdup(fdname), + }; + + if (!efd->fdname) + return -ENOMEM; + + efd->fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (efd->fd < 0) + return -errno; + + LIST_APPEND(extra_fd, s->extra_fds, TAKE_PTR(efd)); + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + return 1; + } + return 0; } diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index becc2b92ef..2bd43a95dd 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -3912,7 +3912,7 @@ static int exec_context_named_iofds( for (size_t i = 0; i < 3; i++) stdio_fdname[i] = exec_context_fdname(c, i); - n_fds = p->n_storage_fds + p->n_socket_fds; + n_fds = p->n_storage_fds + p->n_socket_fds + p->n_extra_fds; for (size_t i = 0; i < n_fds && targets > 0; i++) if (named_iofds[STDIN_FILENO] < 0 && @@ -4096,7 +4096,7 @@ int exec_invoke( int ngids_after_pam = 0; int socket_fd = -EBADF, named_iofds[3] = EBADF_TRIPLET; - size_t n_storage_fds, n_socket_fds; + size_t n_storage_fds, n_socket_fds, n_extra_fds; assert(command); assert(context); @@ -4133,12 +4133,13 @@ int exec_invoke( return log_exec_error_errno(context, params, SYNTHETIC_ERRNO(EINVAL), "Got no socket."); socket_fd = params->fds[0]; - n_storage_fds = n_socket_fds = 0; + n_storage_fds = n_socket_fds = n_extra_fds = 0; } else { n_socket_fds = params->n_socket_fds; n_storage_fds = params->n_storage_fds; + n_extra_fds = params->n_extra_fds; } - n_fds = n_socket_fds + n_storage_fds; + n_fds = n_socket_fds + n_storage_fds + n_extra_fds; r = exec_context_named_iofds(context, params, named_iofds); if (r < 0) diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c index b3035c0026..13e7078b1a 100644 --- a/src/core/execute-serialize.c +++ b/src/core/execute-serialize.c @@ -1282,7 +1282,13 @@ static int exec_parameters_serialize(const ExecParameters *p, const ExecContext return r; } - r = serialize_fd_many(f, fds, "exec-parameters-fds", p->fds, p->n_socket_fds + p->n_storage_fds); + if (p->n_extra_fds > 0) { + r = serialize_item_format(f, "exec-parameters-n-extra-fds", "%zu", p->n_extra_fds); + if (r < 0) + return r; + } + + r = serialize_fd_many(f, fds, "exec-parameters-fds", p->fds, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds); if (r < 0) return r; } @@ -1478,27 +1484,37 @@ static int exec_parameters_deserialize(ExecParameters *p, FILE *f, FDSet *fds) { if (p->n_storage_fds > (size_t) nr_open) return -EINVAL; /* too many, someone is playing games with us */ + } else if ((val = startswith(l, "exec-parameters-n-extra-fds="))) { + if (p->fds) + return -EINVAL; /* Already received */ + + r = safe_atozu(val, &p->n_extra_fds); + if (r < 0) + return r; + + if (p->n_extra_fds > (size_t) nr_open) + return -EINVAL; /* too many, someone is playing games with us */ } else if ((val = startswith(l, "exec-parameters-fds="))) { - if (p->n_socket_fds + p->n_storage_fds == 0) + if (p->n_socket_fds + p->n_storage_fds + p->n_extra_fds == 0) return log_warning_errno( SYNTHETIC_ERRNO(EINVAL), "Got exec-parameters-fds= without " - "prior exec-parameters-n-socket-fds= or exec-parameters-n-storage-fds="); - if (p->n_socket_fds + p->n_storage_fds > (size_t) nr_open) + "prior exec-parameters-n-socket-fds= or exec-parameters-n-storage-fds= or exec-parameters-n-extra-fds="); + if (p->n_socket_fds + p->n_storage_fds + p->n_extra_fds > (size_t) nr_open) return -EINVAL; /* too many, someone is playing games with us */ if (p->fds) return -EINVAL; /* duplicated */ - p->fds = new(int, p->n_socket_fds + p->n_storage_fds); + p->fds = new(int, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds); if (!p->fds) return log_oom_debug(); /* Ensure we don't leave any FD uninitialized on error, it makes the fuzzer sad */ - FOREACH_ARRAY(i, p->fds, p->n_socket_fds + p->n_storage_fds) + FOREACH_ARRAY(i, p->fds, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds) *i = -EBADF; - r = deserialize_fd_many(fds, val, p->n_socket_fds + p->n_storage_fds, p->fds); + r = deserialize_fd_many(fds, val, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds, p->fds); if (r < 0) continue; diff --git a/src/core/execute.c b/src/core/execute.c index 30fcffcc5b..0c2c278d69 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -391,7 +391,7 @@ int exec_spawn( assert(context); assert(params); assert(!params->fds || FLAGS_SET(params->flags, EXEC_PASS_FDS)); - assert(params->fds || (params->n_socket_fds + params->n_storage_fds == 0)); + assert(params->fds || (params->n_socket_fds + params->n_storage_fds + params->n_extra_fds == 0)); assert(!params->files_env); /* We fill this field, ensure it comes NULL-initialized to us */ assert(ret); @@ -2632,7 +2632,7 @@ void exec_params_deep_clear(ExecParameters *p) { * to be fully cleaned up to make sanitizers and analyzers happy, as opposed as the shallow clean * function above. */ - close_many_unset(p->fds, p->n_socket_fds + p->n_storage_fds); + close_many_unset(p->fds, p->n_socket_fds + p->n_storage_fds + p->n_extra_fds); p->cgroup_path = mfree(p->cgroup_path); diff --git a/src/core/execute.h b/src/core/execute.h index 01a196748b..29c08d48b9 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -421,6 +421,7 @@ struct ExecParameters { char **fd_names; size_t n_socket_fds; size_t n_storage_fds; + size_t n_extra_fds; ExecFlags flags; bool selinux_context_net:1; diff --git a/src/core/fuzz-execute-serialize.c b/src/core/fuzz-execute-serialize.c index 5b2dc952ad..05abee5a4e 100644 --- a/src/core/fuzz-execute-serialize.c +++ b/src/core/fuzz-execute-serialize.c @@ -58,8 +58,8 @@ static void exec_fuzz_one(FILE *f, FDSet *fdset) { params.user_lookup_fd = -EBADF; params.bpf_restrict_fs_map_fd = -EBADF; if (!params.fds) - params.n_socket_fds = params.n_storage_fds = 0; - for (size_t i = 0; params.fds && i < params.n_socket_fds + params.n_storage_fds; i++) + params.n_socket_fds = params.n_storage_fds = params.n_extra_fds = 0; + for (size_t i = 0; params.fds && i < params.n_socket_fds + params.n_storage_fds + params.n_extra_fds; i++) params.fds[i] = -EBADF; exec_command_done_array(&command, /* n= */ 1); diff --git a/src/core/service.c b/src/core/service.c index e5db7a085b..7162016313 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -454,6 +454,21 @@ static void service_release_fd_store(Service *s) { assert(s->n_fd_store == 0); } +ServiceExtraFD* service_extra_fd_free(ServiceExtraFD *efd) { + if (!efd) + return NULL; + + efd->fd = asynchronous_close(efd->fd); + free(efd->fdname); + return mfree(efd); +} + +static void service_release_extra_fds(Service *s) { + assert(s); + + LIST_CLEAR(extra_fd, s->extra_fds, service_extra_fd_free); +} + static void service_release_stdio_fd(Service *s) { assert(s); @@ -510,6 +525,7 @@ static void service_done(Unit *u) { service_release_socket_fd(s); service_release_stdio_fd(s); service_release_fd_store(s); + service_release_extra_fds(s); s->mount_request = sd_bus_message_unref(s->mount_request); } @@ -903,40 +919,59 @@ static int service_load(Unit *u) { return service_verify(s); } +static int service_dump_fd(int fd, const char *fdname, const char *header, FILE *f, const char *prefix) { + _cleanup_free_ char *path = NULL; + struct stat st; + int flags; + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat fdstore entry: %m"); + + flags = fcntl(fd, F_GETFL); + if (flags < 0) + return log_debug_errno(errno, "Failed to get fdstore entry flags: %m"); + + (void) fd_get_path(fd, &path); + + fprintf(f, + "%s%s '%s' (type=%s; dev=" DEVNUM_FORMAT_STR "; inode=%" PRIu64 "; rdev=" DEVNUM_FORMAT_STR "; path=%s; access=%s)\n", + prefix, + header, + fdname, + strna(inode_type_to_string(st.st_mode)), + DEVNUM_FORMAT_VAL(st.st_dev), + (uint64_t) st.st_ino, + DEVNUM_FORMAT_VAL(st.st_rdev), + strna(path), + strna(accmode_to_string(flags))); + + return 0; +} + static void service_dump_fdstore(Service *s, FILE *f, const char *prefix) { assert(s); assert(f); assert(prefix); - LIST_FOREACH(fd_store, i, s->fd_store) { - _cleanup_free_ char *path = NULL; - struct stat st; - int flags; + LIST_FOREACH(fd_store, i, s->fd_store) + (void) service_dump_fd(i->fd, + i->fdname, + i == s->fd_store ? "File Descriptor Store Entry:" : " ", + f, + prefix); +} - if (fstat(i->fd, &st) < 0) { - log_debug_errno(errno, "Failed to stat fdstore entry: %m"); - continue; - } +static void service_dump_extra_fds(Service *s, FILE *f, const char *prefix) { + assert(s); + assert(f); + assert(prefix); - flags = fcntl(i->fd, F_GETFL); - if (flags < 0) { - log_debug_errno(errno, "Failed to get fdstore entry flags: %m"); - continue; - } - - (void) fd_get_path(i->fd, &path); - - fprintf(f, - "%s%s '%s' (type=%s; dev=" DEVNUM_FORMAT_STR "; inode=%" PRIu64 "; rdev=" DEVNUM_FORMAT_STR "; path=%s; access=%s)\n", - prefix, i == s->fd_store ? "File Descriptor Store Entry:" : " ", - i->fdname, - strna(inode_type_to_string(st.st_mode)), - DEVNUM_FORMAT_VAL(st.st_dev), - (uint64_t) st.st_ino, - DEVNUM_FORMAT_VAL(st.st_rdev), - strna(path), - strna(accmode_to_string(flags))); - } + LIST_FOREACH(extra_fd, i, s->extra_fds) + (void) service_dump_fd(i->fd, + i->fdname, + i == s->extra_fds ? "Extra File Descriptor Entry:" : " ", + f, + prefix); } static void service_dump(Unit *u, FILE *f, const char *prefix) { @@ -1093,6 +1128,8 @@ static void service_dump(Unit *u, FILE *f, const char *prefix) { fprintf(f, "%sOpen File: %s\n", prefix, ofs); } + service_dump_extra_fds(s, f, prefix); + cgroup_context_dump(UNIT(s), f, prefix); } @@ -1423,11 +1460,12 @@ static int service_collect_fds( int **fds, char ***fd_names, size_t *n_socket_fds, - size_t *n_storage_fds) { + size_t *n_storage_fds, + size_t *n_extra_fds) { _cleanup_strv_free_ char **rfd_names = NULL; _cleanup_free_ int *rfds = NULL; - size_t rn_socket_fds = 0, rn_storage_fds = 0; + size_t rn_socket_fds = 0, rn_storage_fds = 0, rn_extra_fds = 0; int r; assert(s); @@ -1435,6 +1473,7 @@ static int service_collect_fds( assert(fd_names); assert(n_socket_fds); assert(n_storage_fds); + assert(n_extra_fds); if (s->socket_fd >= 0) { Socket *sock = ASSERT_PTR(SOCKET(UNIT_DEREF(s->accept_socket))); @@ -1512,10 +1551,44 @@ static int service_collect_fds( rfd_names[n_fds] = NULL; } + LIST_FOREACH(extra_fd, extra_fd, s->extra_fds) + rn_extra_fds++; + + if (rn_extra_fds > 0) { + size_t n_fds; + char **nl; + int *t; + + t = reallocarray(rfds, rn_socket_fds + rn_storage_fds + rn_extra_fds, sizeof(int)); + if (!t) + return -ENOMEM; + + rfds = t; + + nl = reallocarray(rfd_names, rn_socket_fds + rn_storage_fds + rn_extra_fds + 1, sizeof(char *)); + if (!nl) + return -ENOMEM; + + rfd_names = nl; + n_fds = rn_socket_fds + rn_storage_fds; + + LIST_FOREACH(extra_fd, extra_fd, s->extra_fds) { + rfds[n_fds] = extra_fd->fd; + rfd_names[n_fds] = strdup(extra_fd->fdname); + if (!rfd_names[n_fds]) + return -ENOMEM; + + n_fds++; + } + + rfd_names[n_fds] = NULL; + } + *fds = TAKE_PTR(rfds); *fd_names = TAKE_PTR(rfd_names); *n_socket_fds = rn_socket_fds; *n_storage_fds = rn_storage_fds; + *n_extra_fds = rn_extra_fds; return 0; } @@ -1714,7 +1787,8 @@ static int service_spawn_internal( &exec_params.fds, &exec_params.fd_names, &exec_params.n_socket_fds, - &exec_params.n_storage_fds); + &exec_params.n_storage_fds, + &exec_params.n_extra_fds); if (r < 0) return r; @@ -1722,7 +1796,7 @@ static int service_spawn_internal( exec_params.flags |= EXEC_PASS_FDS; - log_unit_debug(UNIT(s), "Passing %zu fds to service", exec_params.n_socket_fds + exec_params.n_storage_fds); + log_unit_debug(UNIT(s), "Passing %zu fds to service", exec_params.n_socket_fds + exec_params.n_storage_fds + exec_params.n_extra_fds); } if (!FLAGS_SET(exec_params.flags, EXEC_IS_CONTROL) && s->type == SERVICE_EXEC) { @@ -5288,6 +5362,7 @@ static void service_release_resources(Unit *u) { service_release_socket_fd(s); service_release_stdio_fd(s); + service_release_extra_fds(s); if (s->fd_store_preserve_mode != EXEC_PRESERVE_YES) service_release_fd_store(s); diff --git a/src/core/service.h b/src/core/service.h index 6a0c492992..97c217209e 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -3,6 +3,7 @@ typedef struct Service Service; typedef struct ServiceFDStore ServiceFDStore; +typedef struct ServiceExtraFD ServiceExtraFD; #include "exit-status.h" #include "kill.h" @@ -111,6 +112,13 @@ struct ServiceFDStore { LIST_FIELDS(ServiceFDStore, fd_store); }; +struct ServiceExtraFD { + int fd; + char *fdname; + + LIST_FIELDS(ServiceExtraFD, extra_fd); +}; + struct Service { Unit meta; @@ -231,6 +239,9 @@ struct Service { LIST_HEAD(OpenFile, open_files); + /* If service spawned from transient unit, extra file descriptors can be passed via dbus API */ + LIST_HEAD(ServiceExtraFD, extra_fds); + int reload_signal; usec_t reload_begin_usec; @@ -295,3 +306,6 @@ DEFINE_CAST(SERVICE, Service); /* Only exported for unit tests */ int service_deserialize_exec_command(Unit *u, const char *key, const char *value); + +ServiceExtraFD* service_extra_fd_free(ServiceExtraFD *efd); +DEFINE_TRIVIAL_CLEANUP_FUNC(ServiceExtraFD*, service_extra_fd_free); diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index 0f954b89b3..6f3fd76622 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -2017,6 +2017,21 @@ 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 (streq(name, "ExtraFileDescriptorNames")) { + _cleanup_strv_free_ char **extra_fd_names = NULL; + _cleanup_free_ char *joined = NULL; + + r = sd_bus_message_read_strv(m, &extra_fd_names); + if (r < 0) + return bus_log_parse_error(r); + + joined = strv_join(extra_fd_names, " "); + if (!joined) + return log_oom(); + + bus_print_property_value(name, expected_value, flags, joined); + return 1; } diff --git a/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh b/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh new file mode 100755 index 0000000000..cf5c240908 --- /dev/null +++ b/test/units/TEST-23-UNIT-FILE.ExtraFileDescriptors.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +at_exit() { + set +e + + rm -rf /tmp/test-extra-fd/ +} + +trap at_exit EXIT + +mkdir /tmp/test-extra-fd +echo "Hello" > /tmp/test-extra-fd/1.txt +echo "Extra" > /tmp/test-extra-fd/2.txt + +systemd-analyze log-level debug + +# Open files and assign FD to variables +exec {TEST_FD1}