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.
  • Loading branch information
DemiMarie committed Feb 12, 2024
1 parent 95bd4c2 commit c8cc9c4
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 90 deletions.
2 changes: 1 addition & 1 deletion daemon/qrexec-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,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 @@ -47,6 +47,20 @@ void negotiate_connection_params(int s, int other_domid, unsigned type,
*data_domain = params.connect_domain;
}

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
1 change: 1 addition & 0 deletions daemon/qrexec-daemon-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ void negotiate_connection_params(int s, int other_domid, unsigned type,
int *data_domain, int *data_port);
void send_service_connect(int s, const char *conn_ident,
int connect_domain, int connect_port);
bool target_refers_to_dom0(const char *target);
8 changes: 4 additions & 4 deletions daemon/qrexec-daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -1105,8 +1105,7 @@ static void handle_execute_service(
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 @@ -1124,7 +1123,8 @@ static void handle_execute_service(
target_domain) <= 0)
_exit(126);
char *cid;
if (asprintf(&cid, "%s,%s,%d", request_id->ident, remote_domain_name, remote_domain_id) <= 0)
if (asprintf(&cid, "%s,%s,%d", request_id->ident, remote_domain_name,
remote_domain_id) <= 0)
_exit(126);

const char *to_exec[] = {
Expand All @@ -1143,7 +1143,7 @@ static void handle_execute_service(
char *buf;
if (strncmp("@dispvm:", target, sizeof("@dispvm:") - 1) == 0) {
disposable = true;
buf = qubesd_call(target + 8, "admin.vm.CreateDisposable", "", &resp_len);
buf = qubesd_call2(target + 8, "admin.vm.CreateDisposable", "", "uuid", 4, &resp_len);
if (!buf) // error already printed by qubesd_call
_exit(126);
if (memcmp(buf, "0", 2) == 0) {
Expand Down
16 changes: 11 additions & 5 deletions libqrexec/ioall.c
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,12 @@ bool qubes_sendmsg_all(struct msghdr *const msg, int const sock)
return true;
}

char *qubesd_call(const char *dest, char *method, char *arg, size_t *len)
char *qubesd_call(const char *dest, char *method, char *arg, size_t *out_len)
{
return qubesd_call2(dest, method, arg, "", 0, out_len);
}

char *qubesd_call2(const char *dest, char *method, char *arg, const char *payload, size_t len, size_t *out_len)
{
char *buf = NULL;
char *word;
Expand All @@ -272,6 +277,7 @@ char *qubesd_call(const char *dest, char *method, char *arg, size_t *len)
{ .iov_base = arg, .iov_len = arg ? strlen(arg) : 0 },
{ .iov_base = word, .iov_len = wordlen },
{ .iov_base = (void *)dest, .iov_len = strlen(dest) + 1 },
{ .iov_base = (void *)payload, .iov_len = len },
};

struct sockaddr_un qubesd_sock = {
Expand Down Expand Up @@ -314,17 +320,17 @@ char *qubesd_call(const char *dest, char *method, char *arg, size_t *len)

#define BUF_SIZE 35
#define BUF_MAX 65535
buf = qubes_read_all_to_malloc(sock, BUF_SIZE, BUF_MAX, len);
if (buf && (*len < 2 || strlen(buf) >= *len)) {
buf = qubes_read_all_to_malloc(sock, BUF_SIZE, BUF_MAX, out_len);
if (buf && (*out_len < 2 || strlen(buf) >= *out_len)) {
LOG(ERROR,
"Truncated response to %s: got %zu bytes",
method,
*len);
*len = 0;
*out_len);
free(buf);
buf = NULL;
}
out:
*out_len = 0;
if (sock != -1)
close(sock);
return buf;
Expand Down
24 changes: 19 additions & 5 deletions libqrexec/libqrexec-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,17 +305,31 @@ void qrexec_log(int level, int errnoval, const char *file, int line,
void setup_logging(const char *program_name);
int qubes_toml_config_parse(const char *config_full_path, int *wait_for_session, char **user);

/**
* Make an Admin API call to qubesd with no payload. The returned buffer must be released by
* the caller using free().
*
* @param[in] dest The destination VM name.
* @param[in] method The method name.
* @param[in] arg The service argument
* @param[in] payload The payload of the API call.
* @return The value on success. On failure returns NULL and sets errno.
*/
char *qubesd_call(const char *dest, char *method, char *arg, size_t *out_len);

/**
* Make an Admin API call to qubesd. The returned buffer must be released by
* the caller using free().
*
* @param dest The destination VM name.
* @param method The method name.
* @param arg The service argument
* @param len The length of the data returned
* @param[in] dest The destination VM name.
* @param[in] method The method name.
* @param[in] arg The service argument
* @param[in] payload The payload of the API call.
* @param[in] len The length of the payload.
* @param[out] len The length of the data returned.
* @return The value on success. On failure returns NULL and sets errno.
*/
char *qubesd_call(const char *dest, char *method, char *arg, size_t *len);
char *qubesd_call2(const char *dest, char *method, char *arg, const char *payload, size_t len, size_t *out_len);

/**
* Read all data from the file descriptor until EOF, then close it.
Expand Down
80 changes: 59 additions & 21 deletions qrexec/policy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import itertools
import logging
import pathlib
import re
import string

from typing import (
Expand Down Expand Up @@ -235,11 +236,11 @@ def __new__(cls, token: str, *, filepath: Optional[pathlib.Path]=None,
orig_token = token

# first, adjust some aliases
if token == "dom0":
if token in ("dom0", "uuid:00000000-0000-0000-0000-000000000000"):
# TODO: log a warning in Qubes 4.1
token = "@adminvm"

# if user specified just qube name, use it directly
# if user specified just qube name or UUID, use it directly
if not (token.startswith("@") or token == "*"):
return super().__new__(cls, token)

Expand Down Expand Up @@ -300,13 +301,28 @@ def __init__(self, token: str, *, filepath: Optional[pathlib.Path]=None,
# This replaces is_match() and is_match_single().
def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional["VMToken"]=None
) -> bool:
"""Check if this token matches opposite token"""
# pylint: disable=unused-argument
# pylint: disable=unused-argument,too-many-return-statements
if self == "@adminvm":
return other == "@adminvm"
info = system_info["domains"]
if self.startswith("uuid:"):
if other.startswith("uuid:"):
return self == other
try:
return self[5:] == info[str(other)]["uuid"]
except KeyError:
return False
if other.startswith("uuid:"):
try:
return other[5:] == info[str(self)]["uuid"]
except KeyError:
return False
return self == other

def is_special_value(self) -> bool:
Expand Down Expand Up @@ -339,9 +355,12 @@ def expand(self, *, system_info: FullSystemInfo) -> Iterable[VMToken]:
This is used as part of :py:meth:`Policy.collect_targets_for_ask()`.
"""
if self in system_info["domains"]:
yield IntendedTarget(self)

info = system_info["domains"]
if self in info:
if self.startswith("uuid:"):
yield IntendedTarget(type(self)(info[self]))
else:
yield IntendedTarget(self)

class Target(_BaseTarget):
# pylint: disable=missing-docstring
Expand All @@ -362,10 +381,12 @@ def __new__(
return super().__new__(cls, value, filepath=filepath, lineno=lineno) # type: ignore


_uuid_regex = re.compile(r"\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\Z")

# this method (with overloads in subclasses) was verify_target_value
class IntendedTarget(VMToken):
# pylint: disable=missing-docstring
def verify(self, *, system_info: FullSystemInfo) -> VMToken:
def verify(self, *, system_info: FullSystemInfo) -> Optional[VMToken]:
"""Check if given value names valid target
This function check if given value is not only syntactically correct,
Expand Down Expand Up @@ -410,7 +431,7 @@ class WildcardVM(Source, Target):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
Expand Down Expand Up @@ -443,7 +464,7 @@ class AnyVM(Source, Target):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
Expand Down Expand Up @@ -476,7 +497,7 @@ class TypeVM(Source, Target):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
Expand All @@ -499,7 +520,7 @@ class TagVM(Source, Target):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
Expand All @@ -522,7 +543,7 @@ class DispVM(Target, Redirect, IntendedTarget):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
Expand Down Expand Up @@ -556,7 +577,7 @@ class DispVMTemplate(Source, Target, Redirect, IntendedTarget):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
Expand Down Expand Up @@ -590,14 +611,13 @@ class DispVMTag(Source, Target):

def match(
self,
other: Optional[str],
other: str,
*,
system_info: FullSystemInfo,
source: Optional[VMToken]=None
) -> bool:
if isinstance(other, DispVM):
assert source is not None
other = other.get_dispvm_template(source, system_info=system_info)
if isinstance(other, DispVM) and source is not None:
return self == other.get_dispvm_template(source, system_info=system_info)

if not isinstance(other, DispVMTemplate):
# 1) original other may have been neither @dispvm:<name> nor @dispvm
Expand Down Expand Up @@ -698,12 +718,30 @@ async def execute(self) -> str:
if request.source == target:
raise AccessDenied("loopback qrexec connection not supported")

return f"""\
if target in {"@adminvm", "dom0", "uuid:00000000-0000-0000-0000-000000000000"}:
return f"""\
user={self.user or 'DEFAULT'}
result=allow
target=@adminvm
autostart={self.autostart}
requested_target={request.target}"""
elif target.startswith("@dispvm:"):
target_info = request.system_info["domains"][target[8:]]
return f"""\
user={self.user or 'DEFAULT'}
result=allow
target={self.target}
target=@dispvm:uuid:{target_info['uuid']}
autostart={self.autostart}
requested_target={self.request.target}"""
requested_target={request.target}"""
else:
target_info = request.system_info["domains"][target]
return f"""\
user={self.user or 'DEFAULT'}
result=allow
target=uuid:{target_info['uuid']}
autostart={self.autostart}
requested_target={request.target}"""


class AskResolution(AbstractResolution):
"""Resolution returned for :py:class:`Rule` with :py:class:`Ask`.
Expand Down
Loading

0 comments on commit c8cc9c4

Please sign in to comment.