Skip to content

Commit

Permalink
Implement UUID support in qrexec
Browse files Browse the repository at this point in the history
This allows using UUIDs in qrexec policy, using the syntax uuid:VM_UUID.
This works anywhere a VM name is expected.  Since ':' is not allowed in
VM names, there is no ambiguity.  This requires the corresponding change
to qubes-core-admin so that qubesd supports UUIDs in the admin and
internal APIs.

Fixes: QubesOS/qubes-issues#8510
  • Loading branch information
DemiMarie committed Aug 18, 2024
1 parent 45527be commit 578de59
Show file tree
Hide file tree
Showing 16 changed files with 615 additions and 291 deletions.
5 changes: 3 additions & 2 deletions daemon/qrexec-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ int main(int argc, char **argv)
usage(argv[0]);
}

if (strcmp(domname, "dom0") == 0 || strcmp(domname, "@adminvm") == 0) {
if (target_refers_to_dom0(domname)) {
if (request_id == NULL) {
fprintf(stderr, "ERROR: when target domain is 'dom0', -c must be specified\n");
usage(argv[0]);
Expand All @@ -278,10 +278,11 @@ int main(int argc, char **argv)
exit_with_code);
} else {
if (request_id) {
bool const use_uuid = strncmp(domname, "uuid:", 5) == 0;
rc = qrexec_execute_vm(domname, false, src_domain_id,
remote_cmdline, strlen(remote_cmdline) + 1,
request_id, just_exec,
wait_connection_end) ? 0 : 137;
wait_connection_end, use_uuid) ? 0 : 137;
} else {
s = connect_unix_socket(domname);
if (!negotiate_connection_params(s,
Expand Down
37 changes: 34 additions & 3 deletions daemon/qrexec-daemon-common.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include "qrexec-daemon-common.h"

const char *socket_dir = QREXEC_DAEMON_SOCKET_DIR;
#define QREXEC_DISPVM_PREFIX "@dispvm:"
#define QREXEC_DISPVM_PREFIX_SIZE (sizeof QREXEC_DISPVM_PREFIX - 1)

/* ask the daemon to allocate vchan port */
bool negotiate_connection_params(int s, int other_domid, unsigned type,
Expand Down Expand Up @@ -49,6 +51,20 @@ bool negotiate_connection_params(int s, int other_domid, unsigned type,
return true;
}

bool target_refers_to_dom0(const char *target)
{
switch (target[0]) {
case '@':
return strcmp(target + 1, "adminvm") == 0;

Check warning on line 58 in daemon/qrexec-daemon-common.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon-common.c#L57-L58

Added lines #L57 - L58 were not covered by tests
case 'd':
return strcmp(target + 1, "om0") == 0;
case 'u':
return strcmp(target + 1, "uid:00000000-0000-0000-0000-000000000000") == 0;
default:
return false;
}
}

int handle_daemon_handshake(int fd)
{
struct msg_header hdr;
Expand Down Expand Up @@ -93,7 +109,7 @@ int handle_daemon_handshake(int fd)
bool qrexec_execute_vm(const char *target, bool autostart, int remote_domain_id,
const char *cmd, size_t const service_length,
const char *request_id, bool just_exec,
bool wait_connection_end)
bool wait_connection_end, bool use_uuid)
{
if (service_length < 2 || (size_t)service_length > MAX_QREXEC_CMD_LEN) {
/* This is arbitrary, but it helps reduce the risk of overflows in other code */
Expand All @@ -119,11 +135,26 @@ bool qrexec_execute_vm(const char *target, bool autostart, int remote_domain_id,
}
// Otherwise, we kill the VM immediately after starting it.
wait_connection_end = true;
buf = qubesd_call(target + 8, "admin.vm.CreateDisposable", "", &resp_len);
buf = qubesd_call2(target + QREXEC_DISPVM_PREFIX_SIZE,

Check warning on line 138 in daemon/qrexec-daemon-common.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon-common.c#L138

Added line #L138 was not covered by tests
"admin.vm.CreateDisposable", "",
"uuid", use_uuid ? 4 : 0, &resp_len);
if (buf == NULL) // error already printed by qubesd_call
return false;
if (memcmp(buf, "0", 2) == 0) {
/* we exit later so memory leaks do not matter */
if (strlen(buf + 2) != resp_len - 2) {
LOG(ERROR, "NUL byte in qubesd response");
return false;

Check warning on line 146 in daemon/qrexec-daemon-common.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon-common.c#L144-L146

Added lines #L144 - L146 were not covered by tests
}
if (use_uuid) {
if (resp_len != sizeof("uuid:00000000-0000-0000-0000-000000000000") + 1) {
LOG(ERROR, "invalid UUID length");
return false;

Check warning on line 151 in daemon/qrexec-daemon-common.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon-common.c#L148-L151

Added lines #L148 - L151 were not covered by tests
}
if (memcmp(buf + 2, "uuid:", 5) != 0) {
LOG(ERROR, "invalid UUID target %s", buf + 2);
return false;

Check warning on line 155 in daemon/qrexec-daemon-common.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon-common.c#L153-L155

Added lines #L153 - L155 were not covered by tests
}
}
target = buf + 2;
} else {
if (memcmp(buf, "2", 2) == 0) {
Expand Down
4 changes: 3 additions & 1 deletion daemon/qrexec-daemon-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ int prepare_local_fds(struct qrexec_parsed_command *command, struct buffer *stdi
__attribute__((warn_unused_result))
bool qrexec_execute_vm(const char *target, bool autostart, int remote_domain_id,
const char *cmd, size_t service_length, const char *request_id,
bool just_exec, bool wait_connection_end);
bool just_exec, bool wait_connection_end, bool use_uuid);
/** FD for stdout of remote process */
extern int local_stdin_fd;
__attribute__((warn_unused_result))
bool target_refers_to_dom0(const char *target);
118 changes: 76 additions & 42 deletions daemon/qrexec-daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/

#include <inttypes.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
Expand Down Expand Up @@ -135,24 +136,44 @@ static
#endif
int protocol_version;

static char *remote_domain_name; // guess what
static const char *remote_domain_name; // guess what
static const char *remote_domain_uuid;
static int remote_domain_id;

static void unlink_or_exit(const char *path)
{
int v = unlink(path);
if (v != 0 && !(v == -1 && errno == ENOENT))
err(1, "unlink(%s)", path);

Check warning on line 147 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L147

Added line #L147 was not covered by tests
}

static char __attribute__((format(printf, 1, 2))) *xasprintf(const char *fmt, ...)
{
va_list x;
char *res;
va_start(x, fmt);
int r = vasprintf(&res, fmt, x);
va_end(x);
if (r < 0)
abort();

Check warning on line 158 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L158

Added line #L158 was not covered by tests
return res;
}

static void unlink_qrexec_socket(void)
{
char *socket_address;
char *link_to_socket_name;

if (asprintf(&socket_address, "%s/qrexec.%d", socket_dir, remote_domain_id) < 0)
err(1, "asprintf");
if (unlink(socket_address) != 0 && errno != ENOENT)
err(1, "unlink(%s)", socket_address);
free(socket_address);
if (asprintf(&link_to_socket_name, "%s/qrexec.%s", socket_dir, remote_domain_name) < 0)
err(1, "asprintf");
if (unlink(link_to_socket_name) != 0 && errno != ENOENT)
err(1, "unlink(%s)", link_to_socket_name);
free(link_to_socket_name);
char *socket_name;
const char *p[2] = {remote_domain_name, remote_domain_uuid};
int i;

for (i = 0; i < 2; ++i) {
char *link_to_socket_name = xasprintf("qrexec.%s%s", i > 0 ? "uuid:" : "", p[i]);
unlink_or_exit(link_to_socket_name);
free(link_to_socket_name);
}
if (asprintf(&socket_name, "qrexec.%d", remote_domain_id) < 0)
abort();

Check warning on line 174 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L174

Added line #L174 was not covered by tests
unlink_or_exit(socket_name);
free(socket_name);
}

static void handle_vchan_error(const char *op)
Expand All @@ -161,27 +182,25 @@ static void handle_vchan_error(const char *op)
exit(1);
}


static int create_qrexec_socket(int domid, const char *domname)
static int create_qrexec_socket(int domid, const char *domname, const char *domuuid)
{
char socket_address[40];
char *link_to_socket_name;

snprintf(socket_address, sizeof(socket_address),
"%s/qrexec.%d", socket_dir, domid);
if (asprintf(&link_to_socket_name,
"%s/qrexec.%s", socket_dir, domname) < 0)
err(1, "asprintf");
unlink(link_to_socket_name);

/* When running as root, make the socket accessible; perms on /var/run/qubes still apply */
umask(0);
if (symlink(socket_address, link_to_socket_name)) {
PERROR("symlink(%s,%s)", socket_address, link_to_socket_name);

const char *p[2] = { domuuid, domname };
char *socket_address = xasprintf("qrexec.%d", domid);
for (int i = 0; i < 2; ++i) {
if (p[i] == NULL)
continue;

Check warning on line 194 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L194

Added line #L194 was not covered by tests
char *link_to_socket_name = xasprintf("qrexec.%s%s", i ? "" : "uuid:", p[i]);
unlink_or_exit(link_to_socket_name);
if (symlink(socket_address, link_to_socket_name)) {
PERROR("symlink(%s,%s)", socket_address, link_to_socket_name);

Check warning on line 198 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L198

Added line #L198 was not covered by tests
}
free(link_to_socket_name);
}
int fd = get_server_socket(socket_address);
umask(0077);
free(link_to_socket_name);
return fd;
}

Expand Down Expand Up @@ -407,7 +426,7 @@ static void init(int xid, bool opt_direct)

atexit(unlink_qrexec_socket);
qrexec_daemon_unix_socket_fd =
create_qrexec_socket(xid, remote_domain_name);
create_qrexec_socket(xid, remote_domain_name, remote_domain_uuid);

struct sigaction sigchld_action = {
.sa_handler = signal_handler,
Expand Down Expand Up @@ -856,11 +875,12 @@ static int parse_policy_response(
size_t result_bytes,
bool daemon,
char **user,
char **target_uuid,
char **target,
char **requested_target,
int *autostart
) {
*user = *target = *requested_target = NULL;
*user = *target_uuid = *target = *requested_target = NULL;
int result = *autostart = -1;
const char *const msg = daemon ? "qrexec-policy-daemon" : "qrexec-policy-exec";
// At least one byte must be returned
Expand Down Expand Up @@ -905,6 +925,12 @@ static int parse_policy_response(
*target = strdup(current_response + (sizeof("target=") - 1));
if (*target == NULL)
abort();
} else if (!strncmp(current_response, "target_uuid=", sizeof("target_uuid=") - 1)) {
if (*target_uuid != NULL)
goto bad_response;
*target_uuid = strdup(current_response + 12);
if (*target_uuid == NULL)
abort();

Check warning on line 933 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L928-L933

Added lines #L928 - L933 were not covered by tests
} else if (!strncmp(current_response, "autostart=", sizeof("autostart=") - 1)) {
current_response += sizeof("autostart=") - 1;
if (*autostart != -1)
Expand Down Expand Up @@ -1001,6 +1027,7 @@ static enum policy_response connect_daemon_socket(
const char *target_domain,
const char *service_name,
char **user,
char **target_uuid,
char **target,
char **requested_target,
int *autostart
Expand Down Expand Up @@ -1028,7 +1055,7 @@ static enum policy_response connect_daemon_socket(
size_t result_bytes;
// this closes the socket
char *result = qubes_read_all_to_malloc(daemon_socket, 64, 4096, &result_bytes);
int policy_result = parse_policy_response(result, result_bytes, true, user, target, requested_target, autostart);
int policy_result = parse_policy_response(result, result_bytes, true, user, target_uuid, target, requested_target, autostart);

Check warning on line 1058 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L1058

Added line #L1058 was not covered by tests
if (policy_result != RESPONSE_MALFORMED) {
// This leaks 'result', but as the code execs later anyway this isn't a problem.
// 'result' cannot be freed as 'user', 'target', and 'requested_target' point into
Expand Down Expand Up @@ -1099,7 +1126,7 @@ static enum policy_response connect_daemon_socket(
// This leaks 'result', but as the code execs later anyway this isn't a problem.
// 'result' cannot be freed as 'user', 'target', and 'requested_target' point into
// the same buffer.
return parse_policy_response(result, result_bytes, true, user, target, requested_target, autostart);
return parse_policy_response(result, result_bytes, true, user, target_uuid, target, requested_target, autostart);
}
}

Expand Down Expand Up @@ -1132,11 +1159,11 @@ _Noreturn static void handle_execute_service_child(
for (i = 3; i < MAX_FDS; i++)
close(i);

char *user, *target, *requested_target;
int autostart;
char *user = NULL, *target = NULL, *requested_target = NULL, *target_uuid = NULL;
int autostart = -1;
int policy_response =
connect_daemon_socket(remote_domain_name, target_domain, service_name,
&user, &target, &requested_target, &autostart);
&user, &target_uuid, &target, &requested_target, &autostart);

if (policy_response != RESPONSE_ALLOW)
daemon__exit(QREXEC_EXIT_REQUEST_REFUSED);
Expand All @@ -1152,8 +1179,7 @@ _Noreturn static void handle_execute_service_child(
const char *const trailer = strchr(service_name, '+') ? "" : "+";

/* Check if the target is dom0, which requires special handling. */
bool target_is_dom0 = strcmp(target, "@adminvm") == 0 ||
strcmp(target, "dom0") == 0;
bool target_is_dom0 = target_refers_to_dom0(target);

Check warning on line 1182 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L1182

Added line #L1182 was not covered by tests
if (target_is_dom0) {
char *type;
bool target_is_keyword = target_domain[0] == '@';
Expand All @@ -1178,17 +1204,21 @@ _Noreturn static void handle_execute_service_child(
5 /* 5 second timeout */,
false /* return 0 not remote status code */));
} else {
bool const use_uuid = target_uuid != NULL;
const char *const selected_target = use_uuid ? target_uuid : target;

Check warning on line 1208 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L1207-L1208

Added lines #L1207 - L1208 were not covered by tests
int service_length = asprintf(&cmd, "%s:QUBESRPC %s%s %s",
user,
service_name,
trailer,
remote_domain_name);
if (service_length < 0)
daemon__exit(QREXEC_EXIT_PROBLEM);
daemon__exit(qrexec_execute_vm(target, autostart, remote_domain_id,
daemon__exit(qrexec_execute_vm(selected_target, autostart,

Check warning on line 1216 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L1216

Added line #L1216 was not covered by tests
remote_domain_id,
cmd,
(size_t)service_length + 1,
request_id->ident, false, false)
request_id->ident, false, false,

Check warning on line 1220 in daemon/qrexec-daemon.c

View check run for this annotation

Codecov / codecov/patch

daemon/qrexec-daemon.c#L1220

Added line #L1220 was not covered by tests
use_uuid)
? 0 : QREXEC_EXIT_PROBLEM);
}
}
Expand Down Expand Up @@ -1511,7 +1541,7 @@ static int handle_agent_restart(int xid) {
err(1, "sigaction");

qrexec_daemon_unix_socket_fd =
create_qrexec_socket(xid, remote_domain_name);
create_qrexec_socket(xid, remote_domain_name, remote_domain_uuid);
return 0;
}

Expand All @@ -1521,6 +1551,7 @@ static struct option longopts[] = {
{ "socket-dir", required_argument, 0, 'd' + 128 },
{ "policy-program", required_argument, 0, 'p' },
{ "direct", no_argument, 0, 'D' },
{ "uuid", required_argument, 0, 'u' },
{ NULL, 0, 0, 0 },
};

Expand Down Expand Up @@ -1556,7 +1587,7 @@ int main(int argc, char **argv)

setup_logging("qrexec-daemon");

while ((opt=getopt_long(argc, argv, "hqp:D", longopts, NULL)) != -1) {
while ((opt=getopt_long(argc, argv, "hqp:Du:", longopts, NULL)) != -1) {
switch (opt) {
case 'q':
opt_quiet = 1;
Expand All @@ -1572,6 +1603,9 @@ int main(int argc, char **argv)
case 'D':
opt_direct = 1;
break;
case 'u':
remote_domain_uuid = optarg;
break;
case 'h':
default: /* '?' */
usage(argv[0]);
Expand Down
Loading

0 comments on commit 578de59

Please sign in to comment.