Skip to content

Commit 5a16b4c

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, so global variables are used right now. 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 7b4770e commit 5a16b4c

File tree

6 files changed

+148
-9
lines changed

6 files changed

+148
-9
lines changed

libqrexec/exec.c

+19-1
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,15 @@ static int find_file(
311311
return rc;
312312
}
313313

314+
bool exit_on_stdout_eof = false;
315+
bool exit_on_stdin_eof = false;
316+
314317
static int load_service_config_raw(struct qrexec_parsed_command *cmd,
315318
char **user)
316319
{
320+
static bool called_yet = false;
321+
assert(!called_yet);
322+
called_yet = true;
317323
const char *config_path = getenv("QUBES_RPC_CONFIG_PATH");
318324
if (!config_path)
319325
config_path = QUBES_RPC_CONFIG_PATH;
@@ -328,7 +334,9 @@ static int load_service_config_raw(struct qrexec_parsed_command *cmd,
328334
if (ret == -1)
329335
return 0;
330336
return qubes_toml_config_parse(config_full_path, &cmd->wait_for_session, user,
331-
&cmd->send_service_descriptor);
337+
&cmd->send_service_descriptor,
338+
&exit_on_stdout_eof,
339+
&exit_on_stdin_eof);
332340
}
333341

334342
int load_service_config_v2(struct qrexec_parsed_command *cmd) {
@@ -731,6 +739,16 @@ int find_qrexec_service(
731739
path_buffer.data);
732740
return -2;
733741
}
742+
if (exit_on_stdout_eof) {
743+
LOG(ERROR, "Refusing to execute executable service %s with exit_on_stdout_eof=true",
744+
path_buffer.data);
745+
return -2;
746+
}
747+
if (exit_on_stdin_eof) {
748+
LOG(ERROR, "Refusing to execute executable service %s with exit_on_stdin_eof=true",
749+
path_buffer.data);
750+
return -2;
751+
}
734752
return 0;
735753
}
736754

libqrexec/private.h

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
#include <stdbool.h>
2-
int qubes_toml_config_parse(const char *config_full_path, bool *wait_for_session, char **user, bool *skip_service_descriptor);
2+
extern bool exit_on_stdout_eof, exit_on_stdin_eof;
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

+31-6
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
{
@@ -91,6 +92,8 @@ int process_io(const struct process_io_request *old_req) {
9192
.is_service = old_req->is_service,
9293
.replace_chars_stdout = old_req->replace_chars_stdout,
9394
.replace_chars_stderr = old_req->replace_chars_stderr,
95+
.exit_on_stdout_eof = exit_on_stdout_eof,
96+
.exit_on_stdin_eof = exit_on_stdin_eof,
9497
.data_protocol_version = (uint16_t)old_req->data_protocol_version,
9598
.size = sizeof(new_req),
9699
.null_fd = -1,
@@ -144,19 +147,41 @@ int qubes_qrexec_process_io(const struct qrexec_process_io_request *req) {
144147
set_nonblock(stdin_fd);
145148
if (stdout_fd != stdin_fd)
146149
set_nonblock(stdout_fd);
150+
if (is_service && local_pid == 0) {
151+
assert(stdin_fd == stdout_fd);
152+
assert(stderr_fd == -1);
153+
}
147154
if (stderr_fd >= 0) {
148155
assert(is_service); // if this is a client, stderr_fd is *always* -1
149156
set_nonblock(stderr_fd);
150157
}
158+
if (req->exit_on_stdout_eof || req->exit_on_stdin_eof) {
159+
assert(is_service); // only valid for socket services
160+
assert(local_pid == 0); // ditto
161+
}
151162

152163
/* 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; \
164+
#define close_stdin() do { \
165+
if (req->exit_on_stdin_eof) { \
166+
/* Set stdin_fd and stdout_fd to -1. \
167+
* No need to close them as the process \
168+
* will soon exit. */ \
169+
stdin_fd = stdout_fd = -1; \
170+
} else { \
171+
close_stdio(stdin_fd, stdout_fd, SHUT_WR); \
172+
stdin_fd = -1; \
173+
} \
156174
} while (0)
157-
#define close_stdout() do { \
158-
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
159-
stdout_fd = -1; \
175+
#define close_stdout() do { \
176+
if (req->exit_on_stdout_eof) { \
177+
/* Set stdin_fd and stdout_fd to -1. \
178+
* No need to close them as the process \
179+
* will soon exit. */ \
180+
stdin_fd = stdout_fd = -1; \
181+
} else { \
182+
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
183+
stdout_fd = -1; \
184+
} \
160185
} while (0)
161186
#pragma GCC poison close_stdio
162187

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_stdout_eof, bool *exit_on_stdin_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_stdin_eof = false;
192+
bool seen_exit_on_stdout_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-stdin-eof") == 0) {
297+
CHECK_DUP_KEY(seen_exit_on_stdin_eof);
298+
CHECK_TYPE(TOML_TYPE_BOOL, "exit-on-stdin-eof");
299+
*exit_on_stdin_eof = value.boolean;
300+
} else if (strcmp(current_line, "exit-on-stdout-eof") == 0) {
301+
CHECK_DUP_KEY(seen_exit_on_stdout_eof);
302+
CHECK_TYPE(TOML_TYPE_BOOL, "exit-on-stdout-eof");
303+
*exit_on_stdout_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");

qrexec/tests/socket/agent.py

+78
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,14 @@ def test_exec_service_with_invalid_config_7(self):
584584
# skip-service-descriptor not allowed with executable service
585585
self.exec_service_with_invalid_config("skip-service-descriptor = true\n")
586586

587+
def test_exec_service_with_invalid_config_8(self):
588+
# exit-on-stdout-eof not allowed with executable service
589+
self.exec_service_with_invalid_config("exit-on-stdout-eof = true\n")
590+
591+
def test_exec_service_with_invalid_config_9(self):
592+
# exit-on-stdin-eof not allowed with executable service
593+
self.exec_service_with_invalid_config("exit-on-stdin-eof = true\n")
594+
587595
def test_exec_service_with_arg(self):
588596
self.make_executable_service(
589597
"local-rpc",
@@ -714,6 +722,76 @@ def test_connect_socket_no_metadata_user(self):
714722
)
715723
self.check_dom0(dom0)
716724

725+
def test_connect_socket_exit_on_stdin_eof(self):
726+
socket_path = os.path.join(
727+
self.tempdir, "rpc", "qubes.SocketService+arg2"
728+
)
729+
with open(
730+
os.path.join(self.tempdir, "rpc-config", "qubes.SocketService+arg2"), "w"
731+
) as f:
732+
f.write("""\
733+
skip-service-descriptor = true
734+
exit-on-stdin-eof = true
735+
""")
736+
server = qrexec.socket_server(socket_path)
737+
self.addCleanup(server.close)
738+
739+
target, dom0 = self.execute_qubesrpc("qubes.SocketService+arg2", "domX")
740+
741+
server.accept()
742+
743+
message = b"stdin data"
744+
target.send_message(qrexec.MSG_DATA_STDIN, message)
745+
target.send_message(qrexec.MSG_DATA_STDIN, b"")
746+
# Check for EOF on stdin
747+
self.assertEqual(server.recvall(len(message) + 1), message)
748+
messages = target.recv_all_messages()
749+
# No stderr
750+
self.assertListEqual(
751+
util.sort_messages(messages),
752+
[
753+
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
754+
],
755+
)
756+
self.check_dom0(dom0)
757+
server.close()
758+
759+
def test_connect_socket_exit_on_stdout_eof(self):
760+
socket_path = os.path.join(
761+
self.tempdir, "rpc", "qubes.SocketService+arg2"
762+
)
763+
with open(
764+
os.path.join(self.tempdir, "rpc-config", "qubes.SocketService+arg2"), "w"
765+
) as f:
766+
f.write("""\
767+
skip-service-descriptor = true
768+
exit-on-stdout-eof = true
769+
""")
770+
server = qrexec.socket_server(socket_path)
771+
self.addCleanup(server.close)
772+
773+
target, dom0 = self.execute_qubesrpc("qubes.SocketService+arg2", "domX")
774+
775+
server.accept()
776+
777+
message = b"stdin data"
778+
target.send_message(qrexec.MSG_DATA_STDIN, message)
779+
self.assertEqual(server.recvall(len(message)), message)
780+
# Trigger EOF on stdout
781+
server.shutdown(socket.SHUT_WR)
782+
# Server should exit
783+
messages = target.recv_all_messages()
784+
# No stderr
785+
self.assertListEqual(
786+
util.sort_messages(messages),
787+
[
788+
(qrexec.MSG_DATA_STDOUT, b""),
789+
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
790+
],
791+
)
792+
self.check_dom0(dom0)
793+
server.close()
794+
717795
def test_connect_socket_no_metadata(self):
718796
socket_path = os.path.join(
719797
self.tempdir, "rpc", "qubes.SocketService+arg2"

qrexec/tests/socket/qrexec.py

+2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ def accept(self):
117117
self.server_conn.close()
118118
self.server_conn = None
119119

120+
def shutdown(self, arg):
121+
self.conn.shutdown(arg)
120122

121123
def vchan_client(socket_dir, domain, remote_domain, port):
122124
vchan_socket_path = os.path.join(

0 commit comments

Comments
 (0)