varlinkctl: add support for --exec with --upgrade

Having support for `--exec` when using `--upgrade` is nice so this
commit adds it. It does it by extracting a shared helper called
`exec_with_listen_fds()` and then use that in the `verb_call()`
and `varlink_call_and_upgrade()` calls.
This commit is contained in:
Michael Vogt
2026-03-31 18:25:09 +02:00
parent 0fc3f85d35
commit 042f633b12
3 changed files with 139 additions and 73 deletions
+12 -5
View File
@@ -275,13 +275,20 @@
<listitem><para>When used with <command>call</command>: request a protocol upgrade. The method call
is sent with the <constant>upgrade</constant> flag set. The service is expected to send a single
reply confirming the upgrade. After the reply, the Varlink protocol is no longer in effect on
the connection. Instead, <command>varlinkctl</command> acts as a bidirectional proxy: data read
from standard input is forwarded to the upgraded connection, and data received from the connection
is written to standard output.</para>
the connection.</para>
<para>If <option>--exec</option> is not specified, <command>varlinkctl</command> acts as a
bidirectional proxy: data read from standard input is forwarded to the upgraded connection, and data
received from the connection is written to standard output.</para>
<para>If <option>--exec</option> is specified, the upgraded connection socket is placed on both
standard input and standard output of the invoked process. This is similar to the regular
<option>--exec</option> behavior (without <option>--upgrade</option>), which places the method call
reply on standard input. The invoked process can thus simply read from and write to
stdin/stdout to communicate over the upgraded protocol.</para>
<para>This option may not be combined with <option>--more</option>, <option>--oneway</option>,
<option>--collect</option>, <option>--exec</option>, <option>--graceful=</option>, or
<option>--push-fd=</option>.</para>
<option>--collect</option>, <option>--graceful=</option>, or <option>--push-fd=</option>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
+106 -65
View File
@@ -650,7 +650,72 @@ static int upgrade_forward_done(SocketForward *sf, int error, void *userdata) {
return sd_event_exit(event, error < 0 ? error : 0);
}
static int varlink_call_and_upgrade(const char *url, const char *method, sd_json_variant *parameters) {
_noreturn_ static void exec_with_listen_fds(char **exec_cmdline, int *fds, size_t n_fds) {
int r;
_cleanup_free_ char *j = strv_join(exec_cmdline, " ");
if (!j) {
log_oom();
_exit(EXIT_FAILURE);
}
log_close();
log_set_open_when_needed(true);
r = close_all_fds(fds, n_fds);
if (r < 0) {
log_error_errno(r, "Failed to close all remaining file descriptors: %m");
_exit(EXIT_FAILURE);
}
r = pack_fds(fds, n_fds);
if (r < 0) {
log_error_errno(r, "Failed to rearrange file descriptors: %m");
_exit(EXIT_FAILURE);
}
r = fd_cloexec_many(fds, n_fds, false);
if (r < 0) {
log_error_errno(r, "Failed to disable O_CLOEXEC for file descriptors: %m");
_exit(EXIT_FAILURE);
}
if (n_fds > 0) {
r = setenvf("LISTEN_FDS", /* overwrite= */ true, "%zu", n_fds);
if (r < 0) {
log_error_errno(r, "Failed to set $LISTEN_FDS environment variable: %m");
_exit(EXIT_FAILURE);
}
r = setenvf("LISTEN_PID", /* overwrite= */ true, PID_FMT, getpid_cached());
if (r < 0) {
log_error_errno(r, "Failed to set $LISTEN_PID environment variable: %m");
_exit(EXIT_FAILURE);
}
uint64_t pidfdid;
if (pidfd_get_inode_id_self_cached(&pidfdid) >= 0) {
r = setenvf("LISTEN_PIDFDID", /* overwrite= */ true, "%" PRIu64, pidfdid);
if (r < 0) {
log_error_errno(r, "Failed to set $LISTEN_PIDFDID environment variable: %m");
_exit(EXIT_FAILURE);
}
}
} else {
(void) unsetenv("LISTEN_FDS");
(void) unsetenv("LISTEN_PID");
(void) unsetenv("LISTEN_PIDFDID");
}
(void) unsetenv("LISTEN_FDNAMES");
log_debug("Executing: %s", j);
execvp(exec_cmdline[0], exec_cmdline);
log_error_errno(errno, "Failed to execute '%s': %m", j);
_exit(EXIT_FAILURE);
}
static int varlink_call_and_upgrade(const char *url, const char *method, sd_json_variant *parameters, char **exec_cmdline) {
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
_cleanup_(socket_forward_freep) SocketForward *sf = NULL;
@@ -684,6 +749,40 @@ static int varlink_call_and_upgrade(const char *url, const char *method, sd_json
return log_error_errno(errno, "Failed to dup upgraded connection fd: %m");
}
if (!strv_isempty(exec_cmdline)) {
/* --exec mode: place the upgraded connection on stdin/stdout so that the child
* process can just read/write naturally. */
(void) sd_notify(/* unset_environment= */ false, "READY=1");
log_close();
log_set_open_when_needed(true);
int keep_fds[] = { TAKE_FD(input_fd), TAKE_FD(output_fd) };
r = close_all_fds(keep_fds, 2);
if (r < 0) {
log_error_errno(r, "Failed to close remaining file descriptors: %m");
_exit(EXIT_FAILURE);
}
r = move_fd(keep_fds[0], STDIN_FILENO, /* cloexec= */ false);
if (r < 0) {
log_error_errno(r, "Failed to move upgraded connection to stdin: %m");
_exit(EXIT_FAILURE);
}
r = move_fd(keep_fds[1], STDOUT_FILENO, /* cloexec= */ false);
if (r < 0) {
log_error_errno(r, "Failed to move upgraded connection to stdout: %m");
_exit(EXIT_FAILURE);
}
/* We exec and never return here */
exec_with_listen_fds(exec_cmdline, /* fds= */ NULL, /* n_fds= */ 0);
}
/* No --exec: bidirectional proxy between stdin/stdout and the upgraded socket */
/* Use a clean event loop just for the forwarding to avoid any interference. */
r = sd_event_new(&event);
if (r < 0)
@@ -717,7 +816,7 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *jp = NULL;
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
const char *url, *method, *parameter, *source;
char **cmdline;
char **exec_cmdline;
int r;
assert(argc >= 3);
@@ -731,9 +830,8 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--exec and --collect/--more/--oneway may not be combined.");
if (arg_upgrade) {
/* TODO: support --exec with --upgrade, passing the upgraded socket fd to a child process */
if (arg_exec || arg_collect || arg_method_flags != 0 || arg_push_fds.n_fds > 0 || !strv_isempty(arg_graceful))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--upgrade may not be combined with --exec/--collect/--more/--oneway/--push-fd=/--graceful.");
if (arg_collect || arg_method_flags != 0 || arg_push_fds.n_fds > 0 || !strv_isempty(arg_graceful))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--upgrade may not be combined with --collect/--more/--oneway/--push-fd=/--graceful.");
}
(void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
@@ -741,7 +839,7 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) {
url = argv[1];
method = argv[2];
parameter = argc > 3 && !streq(argv[3], "-") ? argv[3] : NULL;
cmdline = strv_skip(argv, 4);
exec_cmdline = strv_skip(argv, 4);
if (!varlink_idl_qualified_symbol_name_is_valid(method))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid qualified method name: '%s' (Expected valid Varlink interface name, followed by a dot, followed by a valid Varlink symbol name.)", method);
@@ -754,7 +852,7 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) {
return log_error_errno(r, "Failed to parse parameters: %m");
}
return varlink_call_and_upgrade(url, method, jp);
return varlink_call_and_upgrade(url, method, jp, exec_cmdline);
}
/* No JSON mode explicitly configured? Then default to the same as -j (except if --exec is used, in
@@ -938,10 +1036,6 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) {
if (mfd < 0)
return log_error_errno(mfd, "Failed to allocate memfd for reply: %m");
_cleanup_free_ char *j = strv_join(cmdline, " ");
if (!j)
return log_oom();
int *fd_array = NULL, n = 0;
size_t m = 0;
CLEANUP_ARRAY(fd_array, m, close_many_and_free);
@@ -968,66 +1062,13 @@ static int verb_call(int argc, char *argv[], uintptr_t _data, void *userdata) {
* lives in our process their fds. Hence we will now no longer bubble up any
* errors. */
log_close();
log_set_open_when_needed(true);
r = move_fd(mfd, STDIN_FILENO, /* cloexec= */ false);
if (r < 0) {
log_error_errno(r, "Failed to move reply to STDIN_FILENO: %m");
_exit(EXIT_FAILURE);
}
r = close_all_fds(fd_array, m);
if (r < 0) {
log_error_errno(r, "Failed to close all remaining file descriptors: %m");
_exit(EXIT_FAILURE);
}
r = pack_fds(fd_array, m);
if (r < 0) {
log_error_errno(r, "Failed to rearrange file descriptors: %m");
_exit(EXIT_FAILURE);
}
r = fd_cloexec_many(fd_array, m, false);
if (r < 0) {
log_error_errno(r, "Failed to disable O_CLOEXEC for file descriptors: %m");
_exit(EXIT_FAILURE);
}
if (m > 0) {
r = setenvf("LISTEN_FDS", /* overwrite= */ true, "%zu", m);
if (r < 0) {
log_error_errno(r, "Failed to set $LISTEN_FDS environment variable: %m");
_exit(EXIT_FAILURE);
}
r = setenvf("LISTEN_PID", /* overwrite= */ true, PID_FMT, getpid_cached());
if (r < 0) {
log_error_errno(r, "Failed to set $LISTEN_PID environment variable: %m");
_exit(EXIT_FAILURE);
}
uint64_t pidfdid;
if (pidfd_get_inode_id_self_cached(&pidfdid) >= 0) {
r = setenvf("LISTEN_PIDFDID", /* overwrite= */ true, "%" PRIu64, pidfdid);
if (r < 0) {
log_error_errno(r, "Failed to set $LISTEN_PIDFDID environment variable: %m");
_exit(EXIT_FAILURE);
}
}
} else {
(void) unsetenv("LISTEN_FDS");
(void) unsetenv("LISTEN_PID");
(void) unsetenv("LISTEN_PIDFDID");
}
(void) unsetenv("LISTEN_FDNAMES");
log_debug("Executing: %s", j);
execvp(cmdline[0], cmdline);
log_error_errno(errno, "Failed to execute '%s': %m", j);
_exit(EXIT_FAILURE);
exec_with_listen_fds(exec_cmdline, fd_array, m);
}
if (arg_quiet)
+21 -3
View File
@@ -297,9 +297,10 @@ out.flush()
out.write(b"<<< UPGRADED >>>\n")
out.write((json.dumps(received_parameters) + "\n").encode())
out.flush()
data = inp.read().decode().rstrip("\n")
out.write((data[::-1] + "\n").encode())
out.flush()
for line in inp:
text = line.decode().rstrip("\n")
out.write((text[::-1] + "\n").encode())
out.flush()
if conn:
conn.close()
@@ -352,5 +353,22 @@ echo "$result" | grep "<<< UPGRADED >>>" >/dev/null
echo "$result" | grep '"foo": "ssh"' >/dev/null
echo "$result" | grep "tset epip hss" >/dev/null
# Start another server for --exec test
rm -f "$UPGRADE_SOCKET"
python3 "$UPGRADE_SERVER" "$UPGRADE_SOCKET" &
SERVER_PID=$!
timeout 5 bash -c "while [ ! -S '$UPGRADE_SOCKET' ]; do sleep 0.1; done"
# Test --exec mode: the upgraded socket becomes stdin/stdout of the child.
# Since stdout goes to the socket (not the terminal), write results to a file for verification.
EXEC_RESULT="$(mktemp)"
varlinkctl call --upgrade --exec "unix:$UPGRADE_SOCKET" io.systemd.test.Reverse '{"foo":"bar"}' -- \
bash -c "head -2 > '$EXEC_RESULT'; echo 'hello world'; head -1 >> '$EXEC_RESULT'"
grep "<<< UPGRADED >>>" "$EXEC_RESULT" >/dev/null
grep '"foo": "bar"' "$EXEC_RESULT" >/dev/null
grep "dlrow olleh" "$EXEC_RESULT" >/dev/null
rm -f "$EXEC_RESULT"
wait "$SERVER_PID" || :
rm -f "$UPGRADE_SOCKET" "$UPGRADE_SOCKET2" "$UPGRADE_SERVER" /tmp/test-upgrade-input
rm -rf "$(dirname "$UPGRADE_SOCKET")" "$(dirname "$UPGRADE_SOCKET2")"