Skip to content

Commit 95bcdf7

Browse files
committed
Add support for exiting on stdin or stdout EOF
This adds two new boolean service configuration options: - exit-on-stdout-eof: exit when the socket service shuts down its output stream for writing. - exit-on-stdin-eof: exit when the client sends EOF. To avoid compatibility problems, global variables are used to pass this information from the configuration parser to the I/O code. In the future, there should be a better way to pass this information, but this is not possible without more extensive changes. Fortunately, the configuration parser only runs once in the life of any process right now, so this commit adds assertions to check this. Qubes OS ships with assertions enabled, so any violation of this rule will be detected. 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 4bd53d3 commit 95bcdf7

13 files changed

+203
-102
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 && pid > 0)
287287
LOG(INFO, "pid %d exited with %d", pid, exit_code);
@@ -378,7 +378,7 @@ int handle_data_client(
378378
req.prefix_data.len = 0;
379379
}
380380

381-
exit_code = process_io(&req);
381+
exit_code = qrexec_process_io(&req, NULL);
382382
libvchan_close(data_vchan);
383383
return exit_code;
384384
}

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

+22-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) {
@@ -731,6 +740,18 @@ int find_qrexec_service(
731740
path_buffer.data);
732741
return -2;
733742
}
743+
if (cmd->exit_on_stdout_eof) {
744+
LOG(ERROR, "Refusing to execute executable service %s with "
745+
"exit-on-service-eof=true",
746+
path_buffer.data);
747+
return -2;
748+
}
749+
if (cmd->exit_on_stdin_eof) {
750+
LOG(ERROR, "Refusing to execute executable service %s with "
751+
"exit-on-client-eof=true",
752+
path_buffer.data);
753+
return -2;
754+
}
734755
return 0;
735756
}
736757

libqrexec/libqrexec-utils.h

+27-54
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;
@@ -301,75 +309,29 @@ struct process_io_request {
301309
volatile sig_atomic_t *sigusr1;
302310
struct prefix_data prefix_data;
303311
};
304-
#pragma GCC diagnostic push
305-
#pragma GCC diagnostic error "-Wpadded"
306-
307-
/* Set of options for qubes_qrexec_process_io(). */
308-
struct qrexec_process_io_request {
309-
/* Note that stdin_fd, stdout_fd, and stderr_fd are named assuming a
310-
* *local* process. For a *remote* process, stdin_fd is the standard
311-
* *output*, stdout_fd is the standard *input*, and stderr_fd must be -1. */
312-
int stdin_fd, stdout_fd, stderr_fd;
313-
// 0 if no child process
314-
pid_t local_pid;
315-
/*
316-
is_service true (this is a service):
317-
- send local data as MSG_DATA_STDOUT
318-
- send local stderr as MSG_DATA_STDERR, unless in dom0
319-
- send exit code
320-
- wait just for local end
321-
- return local exit code
322-
323-
is_service false (this is a client):
324-
- send local data as MSG_DATA_STDIN
325-
- stderr_fd is always -1
326-
- don't send exit code
327-
- wait for local and remote end
328-
- return remote exit code
329-
*/
330-
uint32_t is_service: 1;
331-
uint32_t replace_chars_stdout: 1;
332-
uint32_t replace_chars_stderr: 1;
333-
uint32_t exit_on_stdout_eof: 1;
334-
uint32_t exit_on_stdin_eof: 1;
335-
uint32_t data_protocol_version: 16;
336-
// if the struct ever exceeds 2KiB there is a major problem
337-
uint32_t size: 11; // Size of the struct. Fill in with
338-
// sizeof(struct qrexec_process_io_request).
339-
// FD to /dev/null, might be -1.
340-
int null_fd;
341-
libvchan_t *vchan;
342-
struct buffer *stdin_buf;
343-
volatile sig_atomic_t *sigchld;
344-
// can be NULL
345-
volatile sig_atomic_t *sigusr1;
346-
struct prefix_data prefix_data;
347-
// New fields can be added at the end (only) and their use will be guarded
348-
// by checks on `size`.
349-
};
350-
#pragma GCC diagnostic pop
351-
_Static_assert(offsetof(struct qrexec_process_io_request, vchan) -
352-
offsetof(struct qrexec_process_io_request, local_pid) == 8 + sizeof(pid_t),
353-
"alignment bug?");
354-
355312

356313
/*
357314
* Pass IO between vchan and local FDs. See the comments for
358315
* process_io_request.
359316
*
360317
* Returns intended exit code (local or remote), but calls exit() on errors.
361318
*
362-
* Deprecated, use qubes_qrexec_process_io_v2() instead.
319+
* Deprecated, use qrexec_process_io() instead.
363320
*/
364-
__attribute__((visibility("default")))
321+
__attribute__((visibility("default"), warn_unused_result))
365322
int process_io(const struct process_io_request *req);
323+
366324
/*
367325
* Pass IO between vchan and local FDs. See the comments for
368326
* process_io_request.
369327
*
370328
* Returns intended exit code (local or remote), but calls exit() on errors.
329+
*
330+
* cmd may be NULL to use the default behavior.
371331
*/
372-
int qubes_qrexec_process_io(const struct qrexec_process_io_request *req);
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);
373335

374336
// Logging
375337

@@ -448,4 +410,15 @@ bool qubes_sendmsg_all(struct msghdr *msg, int sock);
448410
__attribute__((visibility("default")))
449411
int qubes_wait_for_vchan_connection_with_timeout(
450412
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);
451424
#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

+37-31
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
{
@@ -78,41 +79,24 @@ enum {
7879
FD_NUM
7980
};
8081

81-
int process_io(const struct process_io_request *old_req) {
82-
if (old_req->data_protocol_version < 0 || old_req->data_protocol_version > 65535) {
83-
LOG(ERROR, "Bad data protocol version %d", old_req->data_protocol_version);
84-
return 125;
85-
}
86-
struct qrexec_process_io_request new_req = {
87-
.stdin_fd = old_req->stdin_fd,
88-
.stdout_fd = old_req->stdout_fd,
89-
.stderr_fd = old_req->stderr_fd,
90-
.local_pid = old_req->local_pid,
91-
.is_service = old_req->is_service,
92-
.replace_chars_stdout = old_req->replace_chars_stdout,
93-
.replace_chars_stderr = old_req->replace_chars_stderr,
94-
.data_protocol_version = (uint16_t)old_req->data_protocol_version,
95-
.size = sizeof(new_req),
96-
.null_fd = -1,
97-
.vchan = old_req->vchan,
98-
.stdin_buf = old_req->stdin_buf,
99-
.sigchld = old_req->sigchld,
100-
.sigusr1 = old_req->sigusr1,
101-
.prefix_data = old_req->prefix_data,
102-
};
103-
return qubes_qrexec_process_io(&new_req);
82+
int process_io(const struct process_io_request *req) {
83+
return qrexec_process_io(req, NULL);
10484
}
10585

106-
int qubes_qrexec_process_io(const struct qrexec_process_io_request *req) {
86+
int qrexec_process_io(const struct process_io_request *req,
87+
const struct qrexec_parsed_command *cmd) {
10788
libvchan_t *vchan = req->vchan;
10889
int stdin_fd = req->stdin_fd;
10990
int stdout_fd = req->stdout_fd;
11091
int stderr_fd = req->stderr_fd;
11192
struct buffer *stdin_buf = req->stdin_buf;
11293

113-
bool is_service = req->is_service;
94+
bool const is_service = req->is_service;
95+
assert(is_service == (cmd != NULL));
11496
bool replace_chars_stdout = req->replace_chars_stdout;
11597
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;
116100
const int data_protocol_version = req->data_protocol_version;
117101
const size_t max_chunk_size = max_data_chunk_size(data_protocol_version);
118102
pid_t local_pid = req->local_pid;
@@ -144,19 +128,41 @@ int qubes_qrexec_process_io(const struct qrexec_process_io_request *req) {
144128
set_nonblock(stdin_fd);
145129
if (stdout_fd != stdin_fd)
146130
set_nonblock(stdout_fd);
131+
if (is_service && local_pid == 0) {
132+
assert(stdin_fd == stdout_fd);
133+
assert(stderr_fd == -1);
134+
}
147135
if (stderr_fd >= 0) {
148136
assert(is_service); // if this is a client, stderr_fd is *always* -1
149137
set_nonblock(stderr_fd);
150138
}
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+
}
151143

152144
/* Convenience macros that eliminate a ton of error-prone boilerplate */
153-
#define close_stdin() do { \
154-
close_stdio(stdin_fd, stdout_fd, SHUT_WR); \
155-
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+
} \
156155
} while (0)
157-
#define close_stdout() do { \
158-
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
159-
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+
} \
160166
} while (0)
161167
#pragma GCC poison close_stdio
162168

0 commit comments

Comments
 (0)