diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index c95dc8c452..cd6df5a5b8 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -3,12 +3,14 @@ #include "sd-bus.h" #include "sd-json.h" +#include "async.h" #include "bitfield.h" #include "bus-polkit.h" #include "cgroup.h" #include "condition.h" #include "dbus-job.h" #include "execute.h" +#include "fd-util.h" #include "format-util.h" #include "install.h" #include "iovec-util.h" @@ -739,6 +741,14 @@ typedef struct TransientServiceParameters { TransientExecCommandItem *exec_start; size_t n_exec_start; int remain_after_exit; + /* Indices of stdio fds attached to the method call (SCM_RIGHTS), UINT_MAX if unset. Resolved into + * the owned fds below by transient_service_resolve_stdio_fds(). */ + unsigned stdin_fd_index; + unsigned stdout_fd_index; + unsigned stderr_fd_index; + int stdin_fd; + int stdout_fd; + int stderr_fd; } TransientServiceParameters; static void transient_service_parameters_done(TransientServiceParameters *p) { @@ -746,6 +756,9 @@ static void transient_service_parameters_done(TransientServiceParameters *p) { FOREACH_ARRAY(i, p->exec_start, p->n_exec_start) transient_exec_command_item_done(i); free(p->exec_start); + safe_close(p->stdin_fd); + safe_close(p->stdout_fd); + safe_close(p->stderr_fd); } static void transient_service_parameters_init(TransientServiceParameters *p) { @@ -753,6 +766,12 @@ static void transient_service_parameters_init(TransientServiceParameters *p) { *p = (TransientServiceParameters) { .type = _SERVICE_TYPE_INVALID, .remain_after_exit = -1, + .stdin_fd_index = UINT_MAX, + .stdout_fd_index = UINT_MAX, + .stderr_fd_index = UINT_MAX, + .stdin_fd = -EBADF, + .stdout_fd = -EBADF, + .stderr_fd = -EBADF, }; } @@ -938,9 +957,12 @@ static int dispatch_transient_exec_context(const char *name, sd_json_variant *va static int dispatch_transient_service(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { static const sd_json_dispatch_field service_dispatch[] = { - { "Type", SD_JSON_VARIANT_STRING, dispatch_service_type, offsetof(TransientServiceParameters, type), 0 }, - { "ExecStart", SD_JSON_VARIANT_ARRAY, dispatch_transient_exec_command, 0, 0 }, - { "RemainAfterExit", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(TransientServiceParameters, remain_after_exit), 0 }, + { "Type", SD_JSON_VARIANT_STRING, dispatch_service_type, offsetof(TransientServiceParameters, type), 0 }, + { "ExecStart", SD_JSON_VARIANT_ARRAY, dispatch_transient_exec_command, 0, 0 }, + { "RemainAfterExit", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(TransientServiceParameters, remain_after_exit), 0 }, + { "StandardInputFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(TransientServiceParameters, stdin_fd_index), 0 }, + { "StandardOutputFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(TransientServiceParameters, stdout_fd_index), 0 }, + { "StandardErrorFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(TransientServiceParameters, stderr_fd_index), 0 }, {} }; @@ -1435,6 +1457,66 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran return 0; } +static int transient_service_resolve_one_stdio_fd( + sd_varlink *link, + unsigned idx, + int accmode, + int *ret_fd) { + + int r; + + assert(link); + assert(ret_fd); + + if (idx == UINT_MAX) /* not specified */ + return 0; + + /* Dup non-destructively off the connection, so that a re-dispatch of the method (e.g. after a + * deferred polkit authorization) can resolve the same index again. */ + _cleanup_close_ int fd = sd_varlink_peek_dup_fd(link, (size_t) idx); + if (fd < 0) + return fd; + + r = fd_vet_accmode(fd, accmode); + if (r < 0) + return r; + + *ret_fd = TAKE_FD(fd); + return 1; +} + +/* Resolve stdio fd indices to fds attached to the method call. Ownership of the resolved fds lands in + * the parameters and is released by transient_service_parameters_done(). */ +static int transient_service_resolve_stdio_fds(sd_varlink *link, TransientServiceParameters *sp, const char **reterr_field) { + int r; + + assert(link); + assert(sp); + + r = transient_service_resolve_one_stdio_fd(link, sp->stdin_fd_index, O_RDONLY, &sp->stdin_fd); + if (r < 0) { + if (reterr_field) + *reterr_field = "Service.StandardInputFileDescriptor"; + return r; + } + + r = transient_service_resolve_one_stdio_fd(link, sp->stdout_fd_index, O_WRONLY, &sp->stdout_fd); + if (r < 0) { + if (reterr_field) + *reterr_field = "Service.StandardOutputFileDescriptor"; + return r; + } + + r = transient_service_resolve_one_stdio_fd(link, sp->stderr_fd_index, O_WRONLY, &sp->stderr_fd); + if (r < 0) { + if (reterr_field) + *reterr_field = "Service.StandardErrorFileDescriptor"; + return r; + } + + return 0; +} + static int transient_service_apply_properties(Service *s, TransientServiceParameters *sp, const char **reterr_field) { Unit *u = UNIT(ASSERT_PTR(s)); int r; @@ -1446,6 +1528,25 @@ static int transient_service_apply_properties(Service *s, TransientServiceParame unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "Type", "Type=%s", service_type_to_string(sp->type)); } + /* Passed stdio fds, resolved earlier by transient_service_resolve_stdio_fds(): store them on the + * Service and flag the exec context. Not written as a text setting; fd serialization across + * reexec is handled by service_serialize(). */ + if (sp->stdin_fd >= 0) { + asynchronous_close(s->stdin_fd); + s->stdin_fd = TAKE_FD(sp->stdin_fd); + s->exec_context.stdio_as_fds = true; + } + if (sp->stdout_fd >= 0) { + asynchronous_close(s->stdout_fd); + s->stdout_fd = TAKE_FD(sp->stdout_fd); + s->exec_context.stdio_as_fds = true; + } + if (sp->stderr_fd >= 0) { + asynchronous_close(s->stderr_fd); + s->stderr_fd = TAKE_FD(sp->stderr_fd); + s->exec_context.stdio_as_fds = true; + } + if (sp->remain_after_exit >= 0) { s->remain_after_exit = sp->remain_after_exit; unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "RemainAfterExit", "RemainAfterExit=%s", yes_no(sp->remain_after_exit)); @@ -1578,6 +1679,15 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters if (r <= 0) return r; + /* Resolve stdio fd indices against the fds attached to this method call before the unit exists, + * so a failure (bad index, wrong access mode) doesn't leave a half-configured transient unit. */ + bad_field = NULL; + r = transient_service_resolve_stdio_fds(link, &p.context.service, &bad_field); + if (IN_SET(r, -ENXIO, -EPROTOTYPE, -EBADFD, -EISDIR)) /* bad index or unsuitable fd */ + return sd_varlink_error_invalid_parameter_name(link, bad_field ?: "Service"); + if (r < 0) + return sd_varlink_error_errno(link, r); + r = manager_setup_transient_unit(manager, p.context.id, &u, &bus_error); if (r < 0) return varlink_reply_bus_error(link, r, &bus_error); diff --git a/src/core/varlink.c b/src/core/varlink.c index d30cb7cab3..fa6a5e1565 100644 --- a/src/core/varlink.c +++ b/src/core/varlink.c @@ -401,7 +401,9 @@ int manager_setup_varlink_server(Manager *m) { if (m->varlink_server) return 0; - sd_varlink_server_flags_t flags = SD_VARLINK_SERVER_INHERIT_USERDATA; + /* ALLOW_FD_PASSING_INPUT: allow clients to attach fds to method calls, used by + * io.systemd.Unit.StartTransient() to accept stdio fds. */ + sd_varlink_server_flags_t flags = SD_VARLINK_SERVER_INHERIT_USERDATA|SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT; if (MANAGER_IS_SYSTEM(m)) flags |= SD_VARLINK_SERVER_ACCOUNT_UID; diff --git a/src/shared/varlink-io.systemd.Unit.c b/src/shared/varlink-io.systemd.Unit.c index 5a8e7c9833..202b529e87 100644 --- a/src/shared/varlink-io.systemd.Unit.c +++ b/src/shared/varlink-io.systemd.Unit.c @@ -1417,6 +1417,12 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( ServiceContext, SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man/"PROJECT_VERSION_STR"/systemd.service.html#Type="), SD_VARLINK_DEFINE_FIELD_BY_TYPE(Type, ServiceType, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index of a file descriptor passed along with the method call to connect to the service's standard input. Only settable at unit creation time via StartTransient(); never set in unit descriptions returned by List()."), + SD_VARLINK_DEFINE_FIELD(StandardInputFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index of a file descriptor passed along with the method call to connect to the service's standard output. Only settable at unit creation time via StartTransient()."), + SD_VARLINK_DEFINE_FIELD(StandardOutputFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Index of a file descriptor passed along with the method call to connect to the service's standard error. Only settable at unit creation time via StartTransient()."), + SD_VARLINK_DEFINE_FIELD(StandardErrorFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man/"PROJECT_VERSION_STR"/systemd.service.html#ExitType="), SD_VARLINK_DEFINE_FIELD_BY_TYPE(ExitType, ServiceExitType, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man/"PROJECT_VERSION_STR"/systemd.service.html#Restart="), diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh index fc7d16b199..f20a7fd425 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh @@ -264,6 +264,25 @@ test -n "$fragment" grep '^RootHash=/etc/hostname$' "$fragment" >/dev/null grep '^RootHashSignature=/etc/machine-id$' "$fragment" >/dev/null +# Service.Standard{Output,Error}FileDescriptor: connect passed fds (by push order) +# to the unit's stdout/stderr; regular files, so output can be checked after exit. +transient_out=$(mktemp) +transient_err=$(mktemp) +exec {transient_out_fd}>"$transient_out" +exec {transient_err_fd}>"$transient_err" +defer_transient_cleanup varlink-transient-fdpass.service +varlinkctl --push-fd="$transient_out_fd" --push-fd="$transient_err_fd" \ + call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-fdpass.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/sh","arguments":["/bin/sh","-c","echo to-stdout; echo to-stderr >&2"]}],"StandardOutputFileDescriptor":0,"StandardErrorFileDescriptor":1}}}' >/dev/null +# Close our copies so the unit holds the only remaining write ends. +exec {transient_out_fd}>&- +exec {transient_err_fd}>&- +timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-fdpass.service | grep inactive >/dev/null; do sleep 0.5; done' +systemctl show -P Result varlink-transient-fdpass.service | grep success >/dev/null +grep -x to-stdout "$transient_out" >/dev/null +grep -x to-stderr "$transient_err" >/dev/null +rm -f "$transient_out" "$transient_err" + # Error cases: verify specific varlink error types set +o pipefail varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \