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 Apr 19, 2024
1 parent 9401273 commit a73a3bc
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 150 deletions.
2 changes: 1 addition & 1 deletion daemon/qrexec-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,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 Down
14 changes: 14 additions & 0 deletions daemon/qrexec-daemon-common.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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;
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
2 changes: 2 additions & 0 deletions daemon/qrexec-daemon-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ __attribute__((warn_unused_result))
int handle_agent_handshake(libvchan_t *vchan, bool remote_send_first);
__attribute__((warn_unused_result))
int prepare_local_fds(struct qrexec_parsed_command *command, struct buffer *stdin_buffer);
__attribute__((warn_unused_result))
bool target_refers_to_dom0(const char *target);
139 changes: 86 additions & 53 deletions daemon/qrexec-daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*
*/

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
Expand All @@ -40,6 +41,8 @@

#define QREXEC_MIN_VERSION QREXEC_PROTOCOL_V2
#define QREXEC_SOCKET_PATH "/run/qubes/policy.sock"
#define QREXEC_DISPVM_PREFIX "@dispvm:"
#define QREXEC_DISPVM_PREFIX_SIZE (sizeof QREXEC_DISPVM_PREFIX - 1)

#ifdef COVERAGE
void __gcov_dump(void);
Expand Down Expand Up @@ -134,28 +137,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_qrexec_socket(void)
static void unlink_or_exit(const char *path)
{
char socket_address[40];
char link_to_socket_name[strlen(remote_domain_name) + sizeof(socket_address)];
int v = unlink(path);
if (v != 0 && !(v == -1 && errno == ENOENT))
err(1, "unlink(%s)", path);
}

int v = snprintf(socket_address, sizeof(socket_address),
"qrexec.%d", remote_domain_id);
if (v < (int)sizeof("qrexec.1") - 1 || v >= (int)sizeof(socket_address))
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();
v = snprintf(link_to_socket_name, sizeof(link_to_socket_name),
"qrexec.%s", remote_domain_name);
if (v < (int)sizeof("qrexec.") - 1 || v >= (int)sizeof(link_to_socket_name))
return res;
}

static void unlink_qrexec_socket(void)
{
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();
v = unlink(socket_address);
if (v != 0 && !(v == -1 && errno == ENOENT))
err(1, "unlink(%s)", socket_address);
v = unlink(link_to_socket_name);
if (v != 0 && !(v == -1 && errno == ENOENT))
err(1, "unlink(%s)", link_to_socket_name);
unlink_or_exit(socket_name);
free(socket_name);
}

static void handle_vchan_error(const char *op)
Expand All @@ -164,27 +183,22 @@ 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[strlen(domname) + sizeof(socket_address)];
int res;

if ((unsigned)snprintf(socket_address, sizeof(socket_address),
"qrexec.%d", domid) >= sizeof(socket_address))
errx(1, "socket name too long");
if ((unsigned)snprintf(link_to_socket_name, sizeof link_to_socket_name,
"qrexec.%s", domname) >= sizeof link_to_socket_name)
errx(1, "socket link name too long");
res = unlink(link_to_socket_name);
if (res != 0 && !(res == -1 && errno == ENOENT))
err(1, "unlink(%s)", 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;
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);
}
free(link_to_socket_name);
}
int fd = get_server_socket(socket_address);
umask(0077);
Expand Down Expand Up @@ -405,7 +419,7 @@ static void init(int xid)

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 @@ -812,11 +826,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 @@ -861,6 +876,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();
} else if (!strncmp(current_response, "autostart=", sizeof("autostart=") - 1)) {
current_response += sizeof("autostart=") - 1;
if (*autostart != -1)
Expand Down Expand Up @@ -957,6 +978,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 @@ -984,7 +1006,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);
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 @@ -1055,7 +1077,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 @@ -1091,19 +1113,18 @@ _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(126);

/* Replace the target domain with the version normalized by the policy engine */
target_domain = requested_target;
char *cmd = NULL;
bool disposable = false;
size_t resp_len;

/*
Expand All @@ -1113,8 +1134,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);
if (target_is_dom0) {
char *type;
bool target_is_keyword = target_domain[0] == '@';
Expand All @@ -1139,15 +1159,24 @@ _Noreturn static void handle_execute_service_child(
5 /* 5 second timeout */,
false /* return 0 not remote status code */));
} else {
char *buf;
if (strncmp("@dispvm:", target, sizeof("@dispvm:") - 1) == 0) {
disposable = true;
buf = qubesd_call(target + 8, "admin.vm.CreateDisposable", "", &resp_len);
bool const use_uuid = target_uuid != NULL;
const char *const selected_target = use_uuid ? target_uuid : target;
bool const disposable = strncmp(QREXEC_DISPVM_PREFIX, selected_target, QREXEC_DISPVM_PREFIX_SIZE) == 0;
if (disposable) {
char *buf = qubesd_call2(selected_target + QREXEC_DISPVM_PREFIX_SIZE,
"admin.vm.CreateDisposable", "",
"uuid", use_uuid ? 4 : 0, &resp_len);
if (!buf) // error already printed by qubesd_call
daemon__exit(126);
if (memcmp(buf, "0", 2) == 0) {
/* we exec later so memory leaks do not matter */
target = buf + 2;
if (strlen(buf + 2) != resp_len - 2) {
LOG(ERROR, "NUL byte in qubesd response");
daemon__exit(126);
}
if (use_uuid && resp_len != sizeof("uuid:00000000-0000-0000-0000-000000000000") + 1) {
LOG(ERROR, "invalid UUID length");
daemon__exit(126);
}
} else {
if (memcmp(buf, "2", 2) == 0) {
LOG(ERROR, "qubesd could not create disposable VM: %s", buf + 2);
Expand All @@ -1164,7 +1193,7 @@ _Noreturn static void handle_execute_service_child(
remote_domain_name) <= 0)
daemon__exit(126);
if (autostart) {
buf = qubesd_call(target, "admin.vm.Start", "", &resp_len);
char *buf = qubesd_call(target, "admin.vm.Start", "", &resp_len);
if (!buf) // error already printed by qubesd_call
daemon__exit(126);
if (!((memcmp(buf, "0", 2) == 0) ||
Expand Down Expand Up @@ -1539,7 +1568,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 @@ -1549,6 +1578,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 @@ -1585,7 +1615,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 @@ -1601,6 +1631,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 a73a3bc

Please sign in to comment.