Skip to content

Commit 2a302d8

Browse files
committed
Search for qubes.Service+ if call for qubes.Service is made
Previously, qubes.Service+ was searched for if the call was from a VM, but not if the call was from dom0. Furthermore, a call from dom0 with no argument would skip the check for a too-long service name. In the future, service arguments should be required and not passing one should be treated as an error. That's for R5.0, though: there is too much code in dom0 that depends on the current behavior, and Mirage also depends on receiving a call with no argument at all. QREXEC_SERVICE_FULL_NAME (for executable services) and the call metadata (for socket services) still include the actual command, even if the command does not include "+". Therefore, code that needs to differentiate between "no argument" and "empty argument" can do so for calls from dom0. Fixes: QubesOS/qubes-issues#9090
1 parent b16e0a5 commit 2a302d8

File tree

3 files changed

+84
-39
lines changed

3 files changed

+84
-39
lines changed

lib/qubes-rpc-multiplexer

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export QREXEC_SERVICE_FULL_NAME="$1"
3636
SERVICE_WITHOUT_ARGUMENT="${1%%+*}"
3737
if [ "${QREXEC_SERVICE_FULL_NAME}" != "${SERVICE_WITHOUT_ARGUMENT}" ]; then
3838
export QREXEC_SERVICE_ARGUMENT="${QREXEC_SERVICE_FULL_NAME#*+}"
39+
else
40+
# Search for qubes.Service+ if given qubes.Service
41+
set -- "$1+" "$2"
3942
fi
4043

4144

libqrexec/exec.c

+45-38
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ static int load_service_config_raw(struct qrexec_parsed_command *cmd,
282282

283283
int ret = find_file(config_path, cmd->service_descriptor,
284284
config_full_path, sizeof(config_full_path), NULL);
285-
if (ret == -1 && strcmp(cmd->service_descriptor, cmd->service_name) != 0)
285+
if (ret == -1)
286286
ret = find_file(config_path, cmd->service_name,
287287
config_full_path, sizeof(config_full_path), NULL);
288288
if (ret < 0)
@@ -311,6 +311,21 @@ int load_service_config(struct qrexec_parsed_command *cmd,
311311
return rc;
312312
}
313313

314+
/* Duplicates a buffer and adds a NUL terminator.
315+
* Same as strndup(), except that it logs on failure (with PERROR())
316+
* and always copies exactly "len" bytes, even if some of them are NUL
317+
* bytes. This guarantees that the output buffer is of the expected
318+
* length and saves an unneeded call to strnlen(). */
319+
static char* memdupnul(const char *ptr, size_t len) {
320+
char *buf = malloc(len + 1);
321+
if (buf == NULL) {
322+
PERROR("malloc");
323+
return NULL;
324+
}
325+
memcpy(buf, ptr, len);
326+
buf[len] = '\0';
327+
return buf;
328+
}
314329

315330
struct qrexec_parsed_command *parse_qubes_rpc_command(
316331
const char *cmdline, bool strip_username) {
@@ -332,11 +347,9 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
332347
LOG(ERROR, "Bad command from dom0 (%s): no colon", cmdline);
333348
goto err;
334349
}
335-
cmd->username = strndup(cmdline, (size_t)(colon - cmdline));
336-
if (!cmd->username) {
337-
PERROR("strndup");
350+
cmd->username = memdupnul(cmdline, (size_t)(colon - cmdline));
351+
if (!cmd->username)
338352
goto err;
339-
}
340353
cmd->command = colon + 1;
341354
} else
342355
cmd->command = cmdline;
@@ -361,53 +374,47 @@ struct qrexec_parsed_command *parse_qubes_rpc_command(
361374
goto err;
362375
}
363376

364-
if (end - start > MAX_SERVICE_NAME_LEN) {
365-
LOG(ERROR, "Command too long (length %zu)",
366-
end - start);
377+
if (end <= start) {
378+
LOG(ERROR, "Service descriptor is empty (too many spaces after QUBESRPC?)");
367379
goto err;
368380
}
369381

370-
cmd->service_descriptor = strndup(start, end - start);
371-
if (!cmd->service_descriptor) {
372-
PERROR("strndup");
382+
size_t const descriptor_len = (size_t)(end - start);
383+
if (descriptor_len > MAX_SERVICE_NAME_LEN) {
384+
LOG(ERROR, "Command too long (length %zu)", descriptor_len);
373385
goto err;
374386
}
375387

376388
/* Parse service name ("qubes.Service") */
377389

378-
const char *plus = memchr(start, '+', end - start);
379-
if (plus) {
380-
if (plus - start == 0) {
381-
LOG(ERROR, "Service name empty");
382-
goto err;
383-
}
384-
if (plus - start > NAME_MAX) {
385-
LOG(ERROR, "Service name too long to execute (length %zu)",
386-
plus - start);
387-
goto err;
388-
}
389-
cmd->service_name = strndup(start, plus - start);
390-
if (!cmd->service_name) {
391-
PERROR("strndup");
392-
goto err;
393-
}
394-
} else {
395-
cmd->service_name = strndup(start, end - start);
396-
if (!cmd->service_name) {
397-
PERROR("strdup");
398-
goto err;
399-
}
390+
const char *const plus = memchr(start, '+', descriptor_len);
391+
size_t const name_len = plus != NULL ? (size_t)(plus - start) : descriptor_len;
392+
if (name_len > NAME_MAX) {
393+
LOG(ERROR, "Service name too long to execute (length %zu)", name_len);
394+
goto err;
395+
}
396+
if (name_len < 1) {
397+
LOG(ERROR, "Service name empty");
398+
goto err;
400399
}
400+
cmd->service_name = memdupnul(start, name_len);
401+
if (!cmd->service_name)
402+
goto err;
403+
404+
/* If there is no service argument, add a trailing "+" to the descriptor */
405+
cmd->service_descriptor = memdupnul(start, descriptor_len + (plus == NULL));
406+
if (!cmd->service_descriptor)
407+
goto err;
408+
if (plus == NULL)
409+
cmd->service_descriptor[descriptor_len] = '+';
401410

402411
/* Parse source domain */
403412

404413
start = end + 1; /* after the space */
405414
end = strchrnul(start, ' ');
406-
cmd->source_domain = strndup(start, end - start);
407-
if (!cmd->source_domain) {
408-
PERROR("strndup");
415+
cmd->source_domain = memdupnul(start, (size_t)(end - start));
416+
if (!cmd->source_domain)
409417
goto err;
410-
}
411418
}
412419

413420
return cmd;
@@ -480,7 +487,7 @@ static int execute_qrexec_service(
480487
int ret = find_file(qrexec_service_path, cmd->service_descriptor,
481488
service_full_path, sizeof(service_full_path),
482489
&statbuf);
483-
if (ret == -1 && strcmp(cmd->service_descriptor, cmd->service_name) != 0)
490+
if (ret == -1)
484491
ret = find_file(qrexec_service_path, cmd->service_name,
485492
service_full_path, sizeof(service_full_path),
486493
&statbuf);

qrexec/tests/socket/agent.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,6 @@ def test_exec_broken_specific_service(self):
585585
)
586586
self.check_dom0(dom0)
587587

588-
@unittest.expectedFailure
589588
def test_exec_null_argument_finds_service_for_empty_argument(self):
590589
self.make_executable_service(
591590
"local-rpc",
@@ -617,6 +616,42 @@ def test_exec_null_argument_finds_service_for_empty_argument(self):
617616
)
618617
self.check_dom0(dom0)
619618

619+
def test_socket_null_argument_finds_service_for_empty_argument(self):
620+
good_socket_path = os.path.join(
621+
self.tempdir, "rpc", "qubes.SocketService+"
622+
)
623+
bad_socket_path = os.path.join(
624+
self.tempdir, "rpc", "qubes.SocketService"
625+
)
626+
good_server = qrexec.socket_server(good_socket_path)
627+
self.addCleanup(good_server.close)
628+
bad_server = qrexec.socket_server(bad_socket_path)
629+
self.addCleanup(bad_server.close)
630+
631+
target, dom0 = self.execute_qubesrpc("qubes.SocketService", "domX")
632+
633+
good_server.accept()
634+
635+
message = b"stdin data"
636+
target.send_message(qrexec.MSG_DATA_STDIN, message)
637+
target.send_message(qrexec.MSG_DATA_STDIN, b"")
638+
expected = b"qubes.SocketService domX\0" + message
639+
self.assertEqual(good_server.recvall(len(expected)), expected)
640+
641+
good_server.sendall(b"stdout data")
642+
good_server.close()
643+
messages = target.recv_all_messages()
644+
# No stderr
645+
self.assertListEqual(
646+
util.sort_messages(messages),
647+
[
648+
(qrexec.MSG_DATA_STDOUT, b"stdout data"),
649+
(qrexec.MSG_DATA_STDOUT, b""),
650+
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
651+
],
652+
)
653+
self.check_dom0(dom0)
654+
620655
def test_connect_socket_no_metadata(self):
621656
socket_path = os.path.join(
622657
self.tempdir, "rpc", "qubes.SocketService+arg2"

0 commit comments

Comments
 (0)