From 29911b926dfa273f2580a06b94590aab5c2624f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 23 Apr 2026 00:49:30 +0200 Subject: [PATCH] Support --prefix-data in qrexec-client too It's an option for qrexec-client-vm already and it's very useful for example for qubes.VMShell service. Add it to qrexec-client too. It will help getting rid of the copy loop in qvm-run. QubesOS/qubes-issues#10855 --- daemon/qrexec-client.c | 14 ++++++++++++-- daemon/qrexec-daemon-common.c | 13 ++++++++++--- daemon/qrexec-daemon-common.h | 5 ++++- daemon/qrexec-daemon.c | 3 ++- qrexec/tests/socket/daemon.py | 23 ++++++++++++++++++++--- 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/daemon/qrexec-client.c b/daemon/qrexec-client.c index 18fe857a..1d041bb7 100644 --- a/daemon/qrexec-client.c +++ b/daemon/qrexec-client.c @@ -86,6 +86,7 @@ static struct option longopts[] = { { "help", no_argument, 0, 'h' }, { "socket-dir", required_argument, 0, opt_socket_dir }, { "no-exit-code", no_argument, 0, 'E' }, + { "prefix-data", required_argument, 0, 'p' }, { "use-stdin-socket", no_argument, 0, opt_use_stdin_socket }, { NULL, 0, 0, 0 }, }; @@ -108,6 +109,7 @@ _Noreturn static void usage(const char *const name, int status) " -w timeout - override default connection timeout of 5s (set 0 for no timeout)\n" " -k - kill the domain right before exiting\n" " --socket-dir=PATH - directory for qrexec socket, default: %s\n" + " -p PREFIX-DATA, --prefix-data=PREFIX-DATA - send the given data before the provided stdin (can only be used once)\n" " --use-stdin-socket - use fd 0 (which must be socket) for both stdin and stdout\n", name ? name : "qrexec-client", QREXEC_DAEMON_SOCKET_DIR); exit(status); @@ -177,11 +179,12 @@ int main(int argc, char **argv) bool replace_chars_stderr = false; bool wait_connection_end = false; bool exit_with_code = true; + const char *prefix_data = NULL; int rc = QREXEC_EXIT_PROBLEM; setup_logging("qrexec-client"); - while ((opt = getopt_long(argc, argv, "hd:l:eEc:tTw:Wk", longopts, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "hd:l:eEc:p:tTw:Wk", longopts, NULL)) != -1) { switch (opt) { case 'd': domname = xstrdup(optarg); @@ -206,6 +209,11 @@ int main(int argc, char **argv) usage(argv[0], 1); } break; + case 'p': + if (prefix_data) + usage(argv[0], 2); + prefix_data = optarg; + break; case 't': replace_chars_stdout = true; break; @@ -281,7 +289,8 @@ int main(int argc, char **argv) src_domain_name, remote_cmdline, connection_timeout, - exit_with_code); + exit_with_code, + prefix_data); } else { /* dom0 -> dom0 fake service call */ assert(src_domain_id == 0); @@ -398,6 +407,7 @@ int main(int argc, char **argv) .exit_with_code = exit_with_code, .replace_chars_stdout = replace_chars_stdout, .replace_chars_stderr = replace_chars_stderr, + .prefix_data = prefix_data, }; rc = handshake_and_go(¶ms, NULL); cleanup: diff --git a/daemon/qrexec-daemon-common.c b/daemon/qrexec-daemon-common.c index b2f3d29f..20529a7f 100644 --- a/daemon/qrexec-daemon-common.c +++ b/daemon/qrexec-daemon-common.c @@ -478,8 +478,13 @@ static int select_loop(const struct handshake_params *params, req.data_protocol_version = params->data_protocol_version; req.sigchld = &sigchld; req.sigusr1 = NULL; - req.prefix_data.data = NULL; - req.prefix_data.len = 0; + if (params->prefix_data) { + req.prefix_data.data = params->prefix_data; + req.prefix_data.len = strlen(params->prefix_data); + } else { + req.prefix_data.data = NULL; + req.prefix_data.len = 0; + } exit_code = qrexec_process_io(&req, cmd); return (params->exit_with_code ? exit_code : 0); @@ -490,7 +495,8 @@ int run_qrexec_to_dom0(const struct service_params *svc_params, const char *src_domain_name, char *remote_cmdline, int connection_timeout, - bool exit_with_code) + bool exit_with_code, + const char *prefix_data) { int data_domain; int data_port; @@ -544,6 +550,7 @@ int run_qrexec_to_dom0(const struct service_params *svc_params, .exit_with_code = exit_with_code, .replace_chars_stdout = false, // stdout is _from_ dom0 .replace_chars_stderr = false, // stderr is _from_ dom0 + .prefix_data = prefix_data, }; return handshake_and_go(¶ms, command); } diff --git a/daemon/qrexec-daemon-common.h b/daemon/qrexec-daemon-common.h index 335888d2..4d1f5399 100644 --- a/daemon/qrexec-daemon-common.h +++ b/daemon/qrexec-daemon-common.h @@ -72,7 +72,8 @@ int run_qrexec_to_dom0(const struct service_params *svc_params, const char *src_domain_name, char *remote_cmdline, int connection_timeout, - bool exit_with_code); + bool exit_with_code, + const char *prefix_data); /** Parameters for handshake_and_go(), organized as a struct * for convenience. */ struct handshake_params { @@ -97,6 +98,8 @@ struct handshake_params { bool replace_chars_stdout; /// Whether to replace problematic bytes with _ before writing to stderr. bool replace_chars_stderr; + /// Data to send after connecting + const char *prefix_data; }; /** * Process IO call with the parameters specified by the parameters. diff --git a/daemon/qrexec-daemon.c b/daemon/qrexec-daemon.c index decb8336..f8d48129 100644 --- a/daemon/qrexec-daemon.c +++ b/daemon/qrexec-daemon.c @@ -1256,7 +1256,8 @@ _Noreturn static void handle_execute_service_child( source_domain, cmd, 5 /* 5 second timeout */, - false /* return 0 not remote status code */)); + false /* return 0 not remote status code */, + NULL /* prefix data */)); } else { bool const use_uuid = target_uuid != NULL; const char *const selected_target = use_uuid ? target_uuid : target; diff --git a/qrexec/tests/socket/daemon.py b/qrexec/tests/socket/daemon.py index f020ce70..9d667ed8 100644 --- a/qrexec/tests/socket/daemon.py +++ b/qrexec/tests/socket/daemon.py @@ -733,7 +733,9 @@ def test_run_vm_command_from_dom0(self): target_daemon = self.connect_daemon( target_domain, target_domain_name, target_domain_uuid ) - self.start_client(["-d", target_domain_name, cmd]) + self.start_client( + ["-d", target_domain_name, "-p", "prefix data\n", cmd] + ) target_daemon.accept() target_daemon.handshake(QREXEC_PROTOCOL_V3) @@ -754,6 +756,9 @@ def test_run_vm_command_from_dom0(self): target.handshake() # select_loop + self.assertEqual( + target.recv_message(), (qrexec.MSG_DATA_STDIN, b"prefix data\n") + ) target.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n") target.send_message(qrexec.MSG_DATA_STDOUT, b"") self.assertEqual(self.client.stdout.read(), b"stdout data\n") @@ -822,7 +827,7 @@ def test_run_vm_command_from_dom0_reject_stdin(self): def test_run_vm_command_from_dom0_with_local_command(self): cmd = "user:command" - local_cmd = "while read x; do echo input: $x; done; exit 44" + local_cmd = 'while read x; do echo input: "$x"; done; exit 44' target_domain_name = "target_domain" target_domain_uuid = "d95e1147-2d82-4595-90bb-5a7500cc3196" target_domain = 42 @@ -832,7 +837,15 @@ def test_run_vm_command_from_dom0_with_local_command(self): target_domain, target_domain_name, target_domain_uuid ) self.start_client( - ["-d", "uuid:" + target_domain_uuid, "-l", local_cmd, cmd] + [ + "-d", + "uuid:" + target_domain_uuid, + "-l", + local_cmd, + "-p", + "prefix data\n", + cmd, + ] ) target_daemon.accept() target_daemon.handshake(QREXEC_PROTOCOL_V3) @@ -854,6 +867,10 @@ def test_run_vm_command_from_dom0_with_local_command(self): target.handshake() # select_loop + self.assertEqual( + target.recv_message(), + (qrexec.MSG_DATA_STDIN, b"prefix data\n"), + ) target.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n") self.assertEqual( target.recv_message(),