]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: support stdio fd passing in io.systemd.Unit.StartTransient
authorLars Sjöstrom <lars@radicore.se>
Thu, 2 Jul 2026 08:08:03 +0000 (10:08 +0200)
committerMichael Vogt <michael.vogt@gmail.com>
Wed, 8 Jul 2026 06:31:29 +0000 (08:31 +0200)
Add StandardInputFileDescriptor, StandardOutputFileDescriptor and
StandardErrorFileDescriptor to the Service context of the Varlink
StartTransient() method. Each carries the push-order index of a file descriptor
passed along with the method call (via SCM_RIGHTS), and connects it to the
transient service's standard input/output/error. This is the Varlink equivalent
of the StandardInputFileDescriptor= / StandardOutputFileDescriptor= /
StandardErrorFileDescriptor= D-Bus transient properties behind
"systemd-run --pipe", which had no Varlink counterpart.

The manager Varlink server now enables SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT
so clients may attach descriptors to their method calls, matching other
fd-accepting Varlink services (mountfsd, networkd, vmspawn, ...). The indices are
resolved with sd_varlink_peek_dup_fd() after the polkit authorization check and
stored on the Service exactly like the D-Bus path
(bus_set_transient_exec_context_fd()): the fds land in stdin_fd/stdout_fd/
stderr_fd and exec_context.stdio_as_fds is set, so the existing exec-invoke
plumbing wires them to the spawned process unchanged. Unsuitable fds (bad
index, wrong access mode) are rejected as InvalidParameter, other resolution
failures propagate as raw errnos.

Add coverage to TEST-74-AUX-UTILS.varlinkctl-unit.sh, passing regular files as
the stdout/stderr fds and asserting the unit's output lands on them.

src/core/varlink-unit.c
src/core/varlink.c
src/shared/varlink-io.systemd.Unit.c
test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh

index c95dc8c45214c17a488d7f4437f727f2e8ebef65..cd6df5a5b844eb11fb03a142b63b3076a422a8cc 100644 (file)
@@ -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);
index d30cb7cab3358353f0f881c7a304da10ec29a551..fa6a5e15656938b675289a3b634fa0143d56bc66 100644 (file)
@@ -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;
 
index 5a8e7c98331edfe95bd1b78fdce07c9bf45a53ea..202b529e879f701a8f34fdeb27f2e88b60ddad91 100644 (file)
@@ -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="),
index fc7d16b199c31c8e88b51acf24a8ccd83824aa3c..f20a7fd4257c5879f457b3e7edc47b9ffc12ce75 100755 (executable)
@@ -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 \