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 Jul 29, 2024
1 parent 9e09690 commit 62ce536
Show file tree
Hide file tree
Showing 16 changed files with 634 additions and 294 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;
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,
"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;
}
if (use_uuid) {
if (resp_len != sizeof("uuid:00000000-0000-0000-0000-000000000000") + 1) {
LOG(ERROR, "invalid UUID length");
return false;
}
if (memcmp(buf + 2, "uuid:", 5) != 0) {
LOG(ERROR, "invalid UUID target %s", buf + 2);
return false;
}
}
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);
}

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();
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();
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;
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);
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();
} 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);
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);
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;
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,
remote_domain_id,
cmd,
(size_t)service_length + 1,
request_id->ident, false, false)
request_id->ident, false, false,
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 62ce536

Please sign in to comment.