Skip to content

Commit 15f4313

Browse files
committed
Avoid qrexec-client for VM -> VM calls
This saves an execve(). It also allows qrexec-daemon to change how it performs a VM -> VM service call without having to change qrexec-client. During an upgrade, it is possible that qrexec-daemon is older than qrexec-client, causing the old qrexec-daemon to try to use a calling convention that the new qrexec-client doesn't support. Doing VM -> VM calls without calling execve() means this cannot happen. VM -> dom0 and dom0 -> VM calls still use qrexec-client, but VM -> dom0 calls are safe from domain name reuse races as of [1], and the latter do not involve qrexec-daemon at all. qrexec-daemon uses atexit() hooks to clean up its listening sockets, so it is critical that these hooks do _not_ run in the child process. Therefore, change the functions that used exit(), err(), or errx() to return normally or call abort(). The return value is checked by the caller and the functions are marked __attribute__((warn_unused_result)) to ensure this. This also fixes some (but not all) cases where a disposable VM would not be cleaned up by qrexec-client -k. To ensure proper behavior if there are any remaining functions calls to exit(), add an atexit() hook in the child process that calls __gcov_dump() (if coverage is enabled) followed by _exit(126). All of these calls will be in error paths (if not, there is a bug somewhere), so the fixed exit code is okay. Since atexit() hooks are run in reverse order of registration, the call to _exit() will prevent other hooks (such as the one that cleans up the listening sockets) from running. To ensure that code running in a child process gets coverage measured in CI, it is necessary to add calls to __gcov_dump(). Add these calls by means of a wrapper function around _exit(), which is much less error-prone than calling __gcov_dump() and _exit() directly. Part of QubesOS/qubes-issues#9066 [1]: 100fbb9 ("qrexec-client: Use XID to connect to qrexec daemon when possible")
1 parent 7c63e64 commit 15f4313

6 files changed

+332
-218
lines changed

daemon/Makefile

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ override QUBES_CFLAGS:=-I../libqrexec -g -O2 -Wall -Wextra -Werror -fPIC \
44
$(shell pkg-config --cflags $(VCHAN_PKG)) -fstack-protector \
55
-D_FORTIFY_SOURCE=2 -fstack-protector-strong -std=gnu11 -D_POSIX_C_SOURCE=200809L \
66
-D_GNU_SOURCE $(CFLAGS) \
7-
-Wstrict-prototypes -Wold-style-definition -Wmissing-declarations
7+
-Wstrict-prototypes -Wold-style-definition -Wmissing-declarations \
8+
-fvisibility=hidden
89
override LDFLAGS += -pie -Wl,-z,relro,-z,now -L../libqrexec
910
override LDLIBS += $(shell pkg-config --libs $(VCHAN_PKG)) -lqrexec-utils
1011

@@ -22,8 +23,8 @@ install: all
2223
ln -sf ../../bin/qrexec-client $(DESTDIR)/usr/lib/qubes/qrexec-client
2324
.PHONY: all clean install
2425

25-
qrexec-daemon qrexec-client: %: %.o
26-
$(CC) $(LDFLAGS) -pie -g -o $@ $< $(LDLIBS)
26+
qrexec-daemon qrexec-client: %: %.o qrexec-daemon-common.o
27+
$(CC) $(LDFLAGS) -pie -g -o $@ $^ $(LDLIBS)
2728

2829
%.o: %.c
2930
$(CC) $< -c -o $@ $(QUBES_CFLAGS) -MD -MP -MF $@.dep

daemon/qrexec-client.c

+33-154
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include <time.h>
4040

4141
#include "libqrexec-utils.h"
42+
#include "qrexec-daemon-common.h"
4243

4344
// whether qrexec-client should replace problematic bytes with _ before printing the output
4445
static bool replace_chars_stdout = false;
@@ -58,8 +59,6 @@ static bool is_service = false;
5859

5960
static volatile sig_atomic_t sigchld = 0;
6061

61-
static const char *socket_dir = QREXEC_DAEMON_SOCKET_DIR;
62-
6362
extern char **environ;
6463

6564
static char *xstrdup(const char *arg) {
@@ -128,75 +127,6 @@ static int handle_agent_handshake(libvchan_t *vchan, bool remote_send_first)
128127
return data_protocol_version;
129128
}
130129

131-
static int handle_daemon_handshake(int fd)
132-
{
133-
struct msg_header hdr;
134-
struct peer_info info;
135-
136-
/* daemon send MSG_HELLO first */
137-
if (!read_all(fd, &hdr, sizeof(hdr))) {
138-
PERROR("daemon handshake");
139-
return -1;
140-
}
141-
if (hdr.type != MSG_HELLO || hdr.len != sizeof(info)) {
142-
LOG(ERROR, "Invalid daemon MSG_HELLO");
143-
return -1;
144-
}
145-
if (!read_all(fd, &info, sizeof(info))) {
146-
PERROR("daemon handshake");
147-
return -1;
148-
}
149-
150-
if (info.version != QREXEC_PROTOCOL_VERSION) {
151-
LOG(ERROR, "Incompatible daemon protocol version "
152-
"(daemon %d, client %d)",
153-
info.version, QREXEC_PROTOCOL_VERSION);
154-
return -1;
155-
}
156-
157-
hdr.type = MSG_HELLO;
158-
hdr.len = sizeof(info);
159-
info.version = QREXEC_PROTOCOL_VERSION;
160-
161-
if (!write_all(fd, &hdr, sizeof(hdr))) {
162-
LOG(ERROR, "Failed to send MSG_HELLO hdr to daemon");
163-
return -1;
164-
}
165-
if (!write_all(fd, &info, sizeof(info))) {
166-
LOG(ERROR, "Failed to send MSG_HELLO to daemon");
167-
return -1;
168-
}
169-
return 0;
170-
}
171-
172-
static int connect_unix_socket(const char *domname)
173-
{
174-
int s, len, res;
175-
struct sockaddr_un remote;
176-
177-
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
178-
PERROR("socket");
179-
return -1;
180-
}
181-
182-
remote.sun_family = AF_UNIX;
183-
res = snprintf(remote.sun_path, sizeof remote.sun_path,
184-
"%s/qrexec.%s", socket_dir, domname);
185-
if (res < 0)
186-
err(1, "snprintf");
187-
if (res >= (int)sizeof(remote.sun_path))
188-
errx(1, "%s/qrexec.%s is too long for AF_UNIX socket path",
189-
socket_dir, domname);
190-
len = (size_t)res + 1 + offsetof(struct sockaddr_un, sun_path);
191-
if (connect(s, (struct sockaddr *) &remote, len) == -1) {
192-
PERROR("connect");
193-
exit(1);
194-
}
195-
if (handle_daemon_handshake(s) < 0)
196-
exit(1);
197-
return s;
198-
}
199-
200130
static void sigchld_handler(int x __attribute__((__unused__)))
201131
{
202132
sigchld = 1;
@@ -312,66 +242,6 @@ static _Noreturn void handle_failed_exec(libvchan_t *data_vchan)
312242
}
313243
exit(exit_code);
314244
}
315-
316-
/* ask the daemon to allocate vchan port */
317-
static void negotiate_connection_params(int s, int other_domid, unsigned type,
318-
void *cmdline_param, int cmdline_size,
319-
int *data_domain, int *data_port)
320-
{
321-
struct msg_header hdr;
322-
struct exec_params params;
323-
hdr.type = type;
324-
hdr.len = sizeof(params) + cmdline_size;
325-
params.connect_domain = other_domid;
326-
params.connect_port = 0;
327-
if (!write_all(s, &hdr, sizeof(hdr))
328-
|| !write_all(s, &params, sizeof(params))
329-
|| !write_all(s, cmdline_param, cmdline_size)) {
330-
PERROR("write daemon");
331-
exit(1);
332-
}
333-
/* the daemon will respond with the same message with connect_port filled
334-
* and empty cmdline */
335-
if (!read_all(s, &hdr, sizeof(hdr))) {
336-
PERROR("read daemon");
337-
exit(1);
338-
}
339-
assert(hdr.type == type);
340-
if (hdr.len != sizeof(params)) {
341-
LOG(ERROR, "Invalid response for 0x%x", type);
342-
exit(1);
343-
}
344-
if (!read_all(s, &params, sizeof(params))) {
345-
PERROR("read daemon");
346-
exit(1);
347-
}
348-
*data_port = params.connect_port;
349-
*data_domain = params.connect_domain;
350-
}
351-
352-
static void send_service_connect(int s, char *conn_ident,
353-
int connect_domain, int connect_port)
354-
{
355-
struct msg_header hdr;
356-
struct exec_params exec_params;
357-
struct service_params srv_params;
358-
359-
hdr.type = MSG_SERVICE_CONNECT;
360-
hdr.len = sizeof(exec_params) + sizeof(srv_params);
361-
362-
exec_params.connect_domain = connect_domain;
363-
exec_params.connect_port = connect_port;
364-
strncpy(srv_params.ident, conn_ident, sizeof(srv_params.ident) - 1);
365-
srv_params.ident[sizeof(srv_params.ident) - 1] = '\0';
366-
367-
if (!write_all(s, &hdr, sizeof(hdr))
368-
|| !write_all(s, &exec_params, sizeof(exec_params))
369-
|| !write_all(s, &srv_params, sizeof(srv_params))) {
370-
PERROR("write daemon");
371-
exit(1);
372-
}
373-
}
374-
375245
static void select_loop(libvchan_t *vchan, int data_protocol_version, struct buffer *stdin_buf)
376246
{
377247
struct process_io_request req = { 0 };
@@ -560,7 +430,7 @@ int main(int argc, char **argv)
560430
int data_domain;
561431
int s;
562432
bool just_exec = false;
563-
int wait_connection_end = 0;
433+
int wait_connection_end = -1;
564434
char *local_cmdline = NULL;
565435
char *remote_cmdline = NULL;
566436
char *request_id = NULL;
@@ -570,6 +440,7 @@ int main(int argc, char **argv)
570440
struct service_params svc_params;
571441
int prepare_ret;
572442
bool kill = false;
443+
int rc = 126;
573444

574445
if (argc < 3) {
575446
// certainly too few arguments
@@ -636,13 +507,6 @@ int main(int argc, char **argv)
636507
usage(argv[0]);
637508
}
638509

639-
char src_domain_id_str[11];
640-
{
641-
int snprintf_res = snprintf(src_domain_id_str, sizeof(src_domain_id_str), "%d", src_domain_id);
642-
if (snprintf_res < 0 || snprintf_res >= (int)sizeof(src_domain_id_str))
643-
err(1, "snprintf()");
644-
}
645-
646510
if (strcmp(domname, "dom0") == 0 || strcmp(domname, "@adminvm") == 0) {
647511
if (request_id == NULL) {
648512
fprintf(stderr, "ERROR: when target domain is 'dom0', -c must be specified\n");
@@ -655,14 +519,19 @@ int main(int argc, char **argv)
655519
abort();
656520
}
657521
set_remote_domain(src_domain_name);
658-
s = connect_unix_socket(src_domain_id_str);
659-
negotiate_connection_params(s,
522+
s = connect_unix_socket_by_id(src_domain_id);
523+
if (s < 0) {
524+
goto cleanup;
525+
}
526+
if (!negotiate_connection_params(s,
660527
0, /* dom0 */
661528
MSG_SERVICE_CONNECT,
662529
(void*)&svc_params,
663530
sizeof(svc_params),
664531
&data_domain,
665-
&data_port);
532+
&data_port)) {
533+
goto cleanup;
534+
}
666535

667536
struct buffer stdin_buffer;
668537
buffer_init(&stdin_buffer);
@@ -684,30 +553,39 @@ int main(int argc, char **argv)
684553
handshake_and_go(data_vchan, &stdin_buffer, true, prepare_ret);
685554
} else {
686555
s = connect_unix_socket(domname);
687-
negotiate_connection_params(s,
556+
if (!negotiate_connection_params(s,
688557
src_domain_id,
689558
just_exec ? MSG_JUST_EXEC : MSG_EXEC_CMDLINE,
690559
remote_cmdline,
691560
compute_service_length(remote_cmdline, argv[0]),
692561
&data_domain,
693-
&data_port);
694-
if (wait_connection_end && request_id)
695-
/* save socket fd, 's' will be reused for the other qrexec-daemon
696-
* connection */
697-
wait_connection_end = s;
698-
else
699-
close(s);
562+
&data_port)) {
563+
goto cleanup;
564+
}
700565
if (request_id) {
701-
s = connect_unix_socket(src_domain_id_str);
702-
send_service_connect(s, request_id, data_domain, data_port);
566+
if (wait_connection_end != -1) {
567+
/* save socket fd, 's' will be reused for the other qrexec-daemon
568+
* connection */
569+
wait_connection_end = s;
570+
} else {
571+
close(s);
572+
}
573+
s = connect_unix_socket_by_id(src_domain_id);
574+
if (s == -1) {
575+
goto cleanup;
576+
}
577+
if (!send_service_connect(s, request_id, data_domain, data_port)) {
578+
goto cleanup;
579+
}
703580
close(s);
704-
if (wait_connection_end) {
581+
if (wait_connection_end != -1) {
705582
/* wait for EOF */
706583
struct pollfd fds[1] = {
707584
{ .fd = wait_connection_end, .events = POLLIN | POLLHUP, .revents = 0 },
708585
};
709586
poll(fds, 1, -1);
710587
}
588+
rc = 0;
711589
} else {
712590
set_remote_domain(domname);
713591
struct buffer stdin_buffer;
@@ -724,12 +602,13 @@ int main(int argc, char **argv)
724602
}
725603
}
726604

605+
cleanup:
727606
if (kill && domname) {
728607
size_t l;
729608
qubesd_call(domname, "admin.vm.Kill", "", &l);
730609
}
731610

732-
return 0;
611+
return rc;
733612
}
734613

735614
// vim:ts=4:sw=4:et:

0 commit comments

Comments
 (0)