Skip to content

Commit a60c40c

Browse files
committed
Add support for exiting on client or service EOF
This adds two new boolean service configuration options: - exit-on-service-eof: exit when the socket service shuts down its output stream for writing. - exit-on-client-eof: exit when the client shuts down its output stream for writing. The information is passed through an extended qrexec_parsed_command struct. New functions are added that avoid the executables having to access members that are private to libqrexec. The main use of these features is to emulate the old qubes.ConnectTCP and qubes.UpdatesProxy services, which already had this behavior due to the use of socat. These features are only supported for socket-based services, as executable services are more complicated and do not have a use case right now. Currently, if a service exits due to exit-on-stdin-eof, the empty MSG_DATA_STDOUT that indicates EOF is not sent. This is not a problem because qrexec-client-vm interprets MSG_DATA_EXIT_CODE as also indicating EOF on stdout and stderr. Fixes: QubesOS/qubes-issues#9176
1 parent 129b5dd commit a60c40c

13 files changed

+229
-27
lines changed

agent/qrexec-agent-data.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ static int handle_new_process_common(
281281
req.prefix_data.data = NULL;
282282
req.prefix_data.len = 0;
283283

284-
exit_code = process_io(&req);
284+
exit_code = qrexec_process_io(&req, cmd);
285285

286286
if (type == MSG_EXEC_CMDLINE) {
287287
if (pid > 0)
@@ -382,7 +382,7 @@ int handle_data_client(
382382
req.prefix_data.len = 0;
383383
}
384384

385-
exit_code = process_io(&req);
385+
exit_code = qrexec_process_io(&req, NULL);
386386
libvchan_close(data_vchan);
387387
return exit_code;
388388
}

agent/qrexec-agent.c

+2-3
Original file line numberDiff line numberDiff line change
@@ -648,9 +648,8 @@ static void handle_server_exec_request_do(int type,
648648
return;
649649
}
650650

651-
/* Fork server does not load configuration, so if sending a service
652-
* descriptor is not enabled, do not use it. */
653-
if (cmd != NULL && !cmd->nogui && cmd->send_service_descriptor) {
651+
/* Ask libqrexec-utils if the fork server is safe to use */
652+
if (qrexec_cmd_use_fork_server(cmd)) {
654653
/* try fork server */
655654
int child_socket = try_fork_server(type,
656655
params->connect_domain, params->connect_port,

agent/qrexec-client-vm.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ int main(int argc, char **argv)
149149

150150
setup_logging("qrexec-client-vm");
151151

152-
// TODO: this should be in process_io
152+
// TODO: this should be in qrexec_process_io
153153
signal(SIGPIPE, SIG_IGN);
154154

155155
while (1) {

daemon/qrexec-client.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ int main(int argc, char **argv)
303303
prepare_ret = QREXEC_EXIT_PROBLEM;
304304
else {
305305
prepare_ret = prepare_local_fds(command, &stdin_buffer);
306+
/* Don't pass this to handshake_and_go() as this is not
307+
* a service call to dom0. */
306308
destroy_qrexec_parsed_command(command);
307309
}
308310
} else {
@@ -333,7 +335,7 @@ int main(int argc, char **argv)
333335
.replace_chars_stdout = replace_chars_stdout,
334336
.replace_chars_stderr = replace_chars_stderr,
335337
};
336-
rc = handshake_and_go(&params);
338+
rc = handshake_and_go(&params, NULL);
337339
cleanup:
338340
if (kill && domname) {
339341
size_t l;

daemon/qrexec-daemon-common.c

+9-6
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ static void handle_failed_exec(libvchan_t *data_vchan, bool is_service, int exit
409409
}
410410
}
411411

412-
static int select_loop(struct handshake_params *params)
412+
static int select_loop(const struct handshake_params *params,
413+
const struct qrexec_parsed_command *cmd)
413414
{
414415
struct process_io_request req = { 0 };
415416
int exit_code;
@@ -429,7 +430,7 @@ static int select_loop(struct handshake_params *params)
429430
req.prefix_data.data = NULL;
430431
req.prefix_data.len = 0;
431432

432-
exit_code = process_io(&req);
433+
exit_code = qrexec_process_io(&req, cmd);
433434
return (params->exit_with_code ? exit_code : 0);
434435
}
435436

@@ -493,10 +494,11 @@ int run_qrexec_to_dom0(const struct service_params *svc_params,
493494
.replace_chars_stdout = false, // stdout is _from_ dom0
494495
.replace_chars_stderr = false, // stderr is _from_ dom0
495496
};
496-
return handshake_and_go(&params);
497+
return handshake_and_go(&params, command);
497498
}
498499

499-
int handshake_and_go(struct handshake_params *params)
500+
int handshake_and_go(struct handshake_params *params,
501+
const struct qrexec_parsed_command *cmd)
500502
{
501503
int rc = QREXEC_EXIT_PROBLEM;
502504
if (params->data_vchan == NULL || !libvchan_is_open(params->data_vchan)) {
@@ -507,11 +509,12 @@ int handshake_and_go(struct handshake_params *params)
507509
params->remote_send_first);
508510
if (data_protocol_version >= 0) {
509511
if (params->prepare_ret != 0) {
510-
rc = params->prepare_ret == -1 ? QREXEC_EXIT_SERVICE_NOT_FOUND : QREXEC_EXIT_PROBLEM;
512+
rc = params->prepare_ret == -1 ? QREXEC_EXIT_SERVICE_NOT_FOUND : rc;
511513
handle_failed_exec(params->data_vchan, params->remote_send_first, rc);
512514
} else {
515+
assert((cmd != NULL) == params->remote_send_first);
513516
params->data_protocol_version = data_protocol_version;
514-
rc = select_loop(params);
517+
rc = select_loop(params, cmd);
515518
}
516519
}
517520
cleanup:

daemon/qrexec-daemon-common.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ struct handshake_params {
3333
bool replace_chars_stderr;
3434
};
3535
__attribute__((warn_unused_result))
36-
int handshake_and_go(struct handshake_params *params);
36+
int handshake_and_go(struct handshake_params *params,
37+
const struct qrexec_parsed_command *cmd);
3738
__attribute__((warn_unused_result))
3839
int handle_agent_handshake(libvchan_t *vchan, bool remote_send_first);
3940
__attribute__((warn_unused_result))

libqrexec/exec.c

+32-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,16 @@ static int load_service_config_raw(struct qrexec_parsed_command *cmd,
328328
if (ret == -1)
329329
return 0;
330330
return qubes_toml_config_parse(config_full_path, &cmd->wait_for_session, user,
331-
&cmd->send_service_descriptor);
331+
&cmd->send_service_descriptor,
332+
&cmd->exit_on_stdout_eof,
333+
&cmd->exit_on_stdin_eof);
334+
}
335+
336+
bool qrexec_cmd_use_fork_server(const struct qrexec_parsed_command *cmd) {
337+
if (cmd == NULL)
338+
return false;
339+
return !cmd->nogui && cmd->send_service_descriptor && !cmd->exit_on_stdin_eof &&
340+
!cmd->exit_on_stdout_eof;
332341
}
333342

334343
int load_service_config_v2(struct qrexec_parsed_command *cmd) {
@@ -341,6 +350,16 @@ int load_service_config_v2(struct qrexec_parsed_command *cmd) {
341350
"skip-service-descriptor=true", cmd->service_descriptor);
342351
return -1;
343352
}
353+
if (cmd->exit_on_stdin_eof) {
354+
LOG(ERROR, "service %s: Cannot set explicit username if "
355+
"exit-on-client-eof=true", cmd->service_descriptor);
356+
return -1;
357+
}
358+
if (cmd->exit_on_stdout_eof) {
359+
LOG(ERROR, "service %s: Cannot set explicit username if "
360+
"exit-on-service-eof=true", cmd->service_descriptor);
361+
return -1;
362+
}
344363
free(cmd->username);
345364
cmd->username = tmp_user;
346365
}
@@ -731,6 +750,18 @@ int find_qrexec_service(
731750
path_buffer.data);
732751
return -2;
733752
}
753+
if (cmd->exit_on_stdout_eof) {
754+
LOG(ERROR, "Refusing to execute executable service %s with "
755+
"exit-on-service-eof=true",
756+
path_buffer.data);
757+
return -2;
758+
}
759+
if (cmd->exit_on_stdin_eof) {
760+
LOG(ERROR, "Refusing to execute executable service %s with "
761+
"exit-on-client-eof=true",
762+
path_buffer.data);
763+
return -2;
764+
}
734765
return 0;
735766
}
736767

libqrexec/libqrexec-utils.h

+34-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ struct qrexec_parsed_command {
8989
/* Remaining fields are private to libqrexec-utils. Do not access them
9090
* directly - they may change in any update. */
9191

92+
/* For socket-based services: Should the event loop exit on EOF from
93+
* the client? */
94+
bool exit_on_stdin_eof;
95+
96+
/* For socket-based services: Should the event loop exit on EOF from
97+
* the service? */
98+
bool exit_on_stdout_eof;
99+
92100
/* Pointer to the argument, or NULL if there is no argument.
93101
* Same buffer as "service_descriptor". */
94102
char *arg;
@@ -307,10 +315,24 @@ struct process_io_request {
307315
* process_io_request.
308316
*
309317
* Returns intended exit code (local or remote), but calls exit() on errors.
318+
*
319+
* Deprecated, use qrexec_process_io() instead.
310320
*/
311-
__attribute__((visibility("default")))
321+
__attribute__((visibility("default"), warn_unused_result))
312322
int process_io(const struct process_io_request *req);
313323

324+
/*
325+
* Pass IO between vchan and local FDs. See the comments for
326+
* process_io_request.
327+
*
328+
* Returns intended exit code (local or remote), but calls exit() on errors.
329+
*
330+
* cmd may be NULL to use the default behavior.
331+
*/
332+
__attribute__((visibility("default"), warn_unused_result))
333+
int qrexec_process_io(const struct process_io_request *req,
334+
const struct qrexec_parsed_command *cmd);
335+
314336
// Logging
315337

316338
#define DEBUG 1
@@ -388,4 +410,15 @@ bool qubes_sendmsg_all(struct msghdr *msg, int sock);
388410
__attribute__((visibility("default")))
389411
int qubes_wait_for_vchan_connection_with_timeout(
390412
libvchan_t *conn, int wait_fd, bool is_server, time_t timeout);
413+
414+
/**
415+
* Determine if the fork server should be used, even though the fork server
416+
* does not load service configuration.
417+
*
418+
* \param cmd The command to check.
419+
* \return true if the command should be executed using the fork server,
420+
* false otherwise.
421+
*/
422+
__attribute__((visibility("default")))
423+
bool qrexec_cmd_use_fork_server(const struct qrexec_parsed_command *cmd);
391424
#endif /* LIBQREXEC_UTILS_H */

libqrexec/private.h

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1+
#pragma once
12
#include <stdbool.h>
2-
int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session, char **user, bool *skip_service_descriptor);
3+
int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session,
4+
char **user,
5+
bool *send_service_descriptor,
6+
bool *exit_on_stdout_eof,
7+
bool *exit_on_stdin_eof);

libqrexec/process_io.c

+38-7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include "libqrexec-utils.h"
3434
#include "remote.h"
35+
#include "private.h"
3536

3637
static _Noreturn void handle_vchan_error(const char *op)
3738
{
@@ -79,15 +80,23 @@ enum {
7980
};
8081

8182
int process_io(const struct process_io_request *req) {
83+
return qrexec_process_io(req, NULL);
84+
}
85+
86+
int qrexec_process_io(const struct process_io_request *req,
87+
const struct qrexec_parsed_command *cmd) {
8288
libvchan_t *vchan = req->vchan;
8389
int stdin_fd = req->stdin_fd;
8490
int stdout_fd = req->stdout_fd;
8591
int stderr_fd = req->stderr_fd;
8692
struct buffer *stdin_buf = req->stdin_buf;
8793

88-
bool is_service = req->is_service;
94+
bool const is_service = req->is_service;
95+
assert(is_service == (cmd != NULL));
8996
bool replace_chars_stdout = req->replace_chars_stdout;
9097
bool replace_chars_stderr = req->replace_chars_stderr;
98+
bool const exit_on_stdin_eof = cmd != NULL && cmd->exit_on_stdin_eof;
99+
bool const exit_on_stdout_eof = cmd != NULL && cmd->exit_on_stdout_eof;
91100
const int data_protocol_version = req->data_protocol_version;
92101
const size_t max_chunk_size = max_data_chunk_size(data_protocol_version);
93102
pid_t local_pid = req->local_pid;
@@ -119,19 +128,41 @@ int process_io(const struct process_io_request *req) {
119128
set_nonblock(stdin_fd);
120129
if (stdout_fd != stdin_fd)
121130
set_nonblock(stdout_fd);
131+
if (is_service && local_pid == 0) {
132+
assert(stdin_fd == stdout_fd);
133+
assert(stderr_fd == -1);
134+
}
122135
if (stderr_fd >= 0) {
123136
assert(is_service); // if this is a client, stderr_fd is *always* -1
124137
set_nonblock(stderr_fd);
125138
}
139+
if (exit_on_stdin_eof || exit_on_stdout_eof) {
140+
assert(is_service); // only valid for socket services
141+
assert(local_pid == 0); // ditto
142+
}
126143

127144
/* Convenience macros that eliminate a ton of error-prone boilerplate */
128-
#define close_stdin() do { \
129-
close_stdio(stdin_fd, stdout_fd, SHUT_WR); \
130-
stdin_fd = -1; \
145+
#define close_stdin() do { \
146+
if (exit_on_stdin_eof) { \
147+
/* Set stdin_fd and stdout_fd to -1. \
148+
* No need to close them as the process \
149+
* will soon exit. */ \
150+
stdin_fd = stdout_fd = -1; \
151+
} else { \
152+
close_stdio(stdin_fd, stdout_fd, SHUT_WR); \
153+
stdin_fd = -1; \
154+
} \
131155
} while (0)
132-
#define close_stdout() do { \
133-
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
134-
stdout_fd = -1; \
156+
#define close_stdout() do { \
157+
if (exit_on_stdout_eof) { \
158+
/* Set stdin_fd and stdout_fd to -1. \
159+
* No need to close them as the process \
160+
* will soon exit. */ \
161+
stdin_fd = stdout_fd = -1; \
162+
} else { \
163+
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
164+
stdout_fd = -1; \
165+
} \
135166
} while (0)
136167
#pragma GCC poison close_stdio
137168

libqrexec/toml.c

+12-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ static void toml_value_free(union toml_data *value, enum toml_type ty) {
171171
}
172172
}
173173

174-
int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session, char **user, bool *send_service_descriptor)
174+
int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session, char **user, bool *send_service_descriptor,
175+
bool *exit_on_service_eof, bool *exit_on_client_eof)
175176
{
176177
int result = -1; /* assume problem */
177178
FILE *config_file = fopen(config_full_path, "re");
@@ -187,6 +188,8 @@ int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session
187188
bool seen_wait_for_session = false;
188189
bool seen_user = false;
189190
bool seen_skip_service_descriptor = false;
191+
bool seen_exit_on_client_eof = false;
192+
bool seen_exit_on_service_eof = false;
190193
*wait_for_session = 0;
191194
*send_service_descriptor = true;
192195
#define CHECK_DUP_KEY(v) do { \
@@ -290,6 +293,14 @@ int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session
290293
CHECK_TYPE(TOML_TYPE_BOOL, "wait-for-session");
291294
*wait_for_session = value.boolean;
292295
}
296+
} else if (strcmp(current_line, "exit-on-client-eof") == 0) {
297+
CHECK_DUP_KEY(seen_exit_on_client_eof);
298+
CHECK_TYPE(TOML_TYPE_BOOL, "exit-on-client-eof");
299+
*exit_on_client_eof = value.boolean;
300+
} else if (strcmp(current_line, "exit-on-service-eof") == 0) {
301+
CHECK_DUP_KEY(seen_exit_on_service_eof);
302+
CHECK_TYPE(TOML_TYPE_BOOL, "exit-on-service-eof");
303+
*exit_on_service_eof = value.boolean;
293304
} else if (strcmp(current_line, "skip-service-descriptor") == 0) {
294305
CHECK_DUP_KEY(seen_skip_service_descriptor);
295306
CHECK_TYPE(TOML_TYPE_BOOL, "skip-service-descriptor");

0 commit comments

Comments
 (0)