Skip to content

Commit

Permalink
Support socket services with MSG_JUST_EXEC
Browse files Browse the repository at this point in the history
This separates _finding_ a qrexec service (and, if necessary, connecting
to a socket) from actually _executing_ the service and processing I/O.
This allows qrexec-agent to handle MSG_JUST_EXEC correctly, because it
can connect to a socket-based service without having to be prepared for
an executable service to be run immediately.  Instead, qrexec-agent
spawns an executable service using its own spawning routines that do
support MSG_JUST_EXEC.

This also adds tests for RPC services (both executable and socket) with
MSG_JUST_EXEC.  Previously, these were not tested, despite being
essential to a running system.  For instance, the Qubes app menu uses
MSG_JUST_EXEC to invoke the qubes.StartApp service every time it
launches an application in a non-disposable VM from dom0.
  • Loading branch information
DemiMarie committed Apr 17, 2024
1 parent daee92e commit dfdfb33
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 51 deletions.
10 changes: 10 additions & 0 deletions agent/qrexec-agent-data.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ static int handle_just_exec(struct qrexec_parsed_command *cmd)

if (cmd == NULL)
return 127;

if (cmd->service_descriptor) {
int socket_fd;
struct buffer stdin_buffer;
buffer_init(&stdin_buffer);
if (!find_qrexec_service(cmd, &socket_fd, &stdin_buffer))
return 127;
if (socket_fd != -1)
return write_all(socket_fd, stdin_buffer.data, stdin_buffer.buflen) ? 0 : 127;
}
switch (pid = fork()) {
case -1:
PERROR("fork");
Expand Down
70 changes: 31 additions & 39 deletions libqrexec/exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ static int do_fork_exec(const char *user,
return retval;
}

#define QUBES_SOCKADDR_UN_MAX_PATH_LEN 1024

static int qubes_connect(int s, const char *connect_path, const size_t total_path_length) {
// Avoiding an extra copy is NOT worth it!
#define QUBES_TMP_DIRECTORY "/tmp/qrexec-XXXXXX"
Expand Down Expand Up @@ -205,11 +203,6 @@ static int qubes_connect(int s, const char *connect_path, const size_t total_pat
return result;
}

static int execute_qrexec_service(
const struct qrexec_parsed_command *cmd,
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
struct buffer *stdin_buffer);

/*
Find a file in the ':'-delimited list of paths given in path_list.
Returns 0 on success, -1 if the file is definitely absent in all of the
Expand Down Expand Up @@ -461,83 +454,82 @@ int execute_parsed_qubes_rpc_command(
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer) {
if (cmd->service_descriptor) {
// Proper Qubes RPC call
return execute_qrexec_service(
cmd, pid, stdin_fd, stdout_fd, stderr_fd, stdin_buffer);
if (!find_qrexec_service(cmd, stdin_fd, stdin_buffer))
return -1;
if (*stdin_fd > -1) {
*stdout_fd = *stdin_fd;
if (stderr_fd)
*stderr_fd = -1;
*pid = 0;
return 0;
}
return do_fork_exec(cmd->username, cmd->command,
pid, stdin_fd, stdout_fd, stderr_fd);
} else {
// Legacy qrexec behavior: spawn shell directly
return do_fork_exec(cmd->username, cmd->command,
pid, stdin_fd, stdout_fd, stderr_fd);
}
}

static int execute_qrexec_service(
bool find_qrexec_service(
const struct qrexec_parsed_command *cmd,
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
struct buffer *stdin_buffer) {

int *socket_fd, struct buffer *stdin_buffer) {
assert(cmd->service_descriptor);

char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN];
struct buffer path_buffer = { .data = file_path, .buflen = (int)sizeof(file_path) };
const char *qrexec_service_path = getenv("QREXEC_SERVICE_PATH");
if (!qrexec_service_path)
qrexec_service_path = QREXEC_SERVICE_PATH;
*socket_fd = -1;

char service_full_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN];
struct stat statbuf;

int ret = find_file(qrexec_service_path, cmd->service_descriptor,
service_full_path, sizeof(service_full_path),
path_buffer.data, (size_t)path_buffer.buflen,
&statbuf);
if (ret == -1)
ret = find_file(qrexec_service_path, cmd->service_name,
service_full_path, sizeof(service_full_path),
path_buffer.data, (size_t)path_buffer.buflen,
&statbuf);
if (ret < 0) {
LOG(ERROR, "Service not found: %s",
cmd->service_descriptor);
return -1;
return false;
}

if (S_ISSOCK(statbuf.st_mode)) {
/* Socket-based service. */
int s;
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
PERROR("socket");
return -1;
return false;

Check warning on line 507 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L507

Added line #L507 was not covered by tests
}
if (qubes_connect(s, service_full_path, strlen(service_full_path))) {
if (qubes_connect(s, path_buffer.data, strlen(path_buffer.data))) {
PERROR("qubes_connect");
close(s);
return -1;
return false;

Check warning on line 512 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L512

Added line #L512 was not covered by tests
}

*stdout_fd = *stdin_fd = s;
if (stderr_fd)
*stderr_fd = -1;
*pid = 0;
set_nonblock(s);

if (cmd->send_service_descriptor) {
/* send part after "QUBESRPC ", including trailing NUL */
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
buffer_append(stdin_buffer, desc, strlen(desc) + 1);
}
return 0;
}

if (euidaccess(service_full_path, X_OK) == 0) {
/*
Executable-based service.
*socket_fd = s;
return true;
}

Note that this delegates to qubes-rpc-multiplexer, which, for the
moment, searches for the right file again.
*/
return do_fork_exec(cmd->username, cmd->command,
pid, stdin_fd, stdout_fd, stderr_fd);
if (euidaccess(path_buffer.data, X_OK) == 0) {
/* Executable-based service. */
return true;
}

LOG(ERROR, "Unknown service type (not executable, not a socket): %s",
service_full_path);
return -1;
LOG(ERROR, "Unknown service type (not executable, not a socket): %.*s",

Check warning on line 530 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L530

Added line #L530 was not covered by tests
path_buffer.buflen, path_buffer.data);
return false;

Check warning on line 532 in libqrexec/exec.c

View check run for this annotation

Codecov / codecov/patch

libqrexec/exec.c#L532

Added line #L532 was not covered by tests
}

int exec_wait_for_session(const char *source_domain) {
Expand Down
20 changes: 19 additions & 1 deletion libqrexec/libqrexec-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ int write_stdin(int fd, const char *data, int len, struct buffer *buffer);

/**
* @brief Execute an already-parsed Qubes RPC command.
* @param cmdline Null-terminated command to execute.
* @param cmd Already-parsed command to execute.
* @param pid On return, holds the PID of the child process.
* @param stdin_fd On return, holds a file descriptor connected to the child's
* stdin.
Expand All @@ -144,6 +144,24 @@ int execute_parsed_qubes_rpc_command(
const struct qrexec_parsed_command *cmd, int *pid, int *stdin_fd,
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer);

/**
* @brief Find the implementation of a Qubes RPC command. If it is a socket,
* connect to it.
* @param[in] cmdline Null-terminated command to execute.
* @param[out] socket_fd On return, holds a file descriptor connected to the socket,
* or -1 for executable services.
* @param stdin_buffer This buffer will need to be prepended to the child process’s
* stdin.
* @return true if the implementation is found (and, for sockets, connected to)
* successfully, false on failure.
*/
bool find_qrexec_service(
const struct qrexec_parsed_command *cmd,
int *socket_fd, struct buffer *stdin_buffer);

/** Suggested buffer size for the path buffer of find_qrexec_service. */
#define QUBES_SOCKADDR_UN_MAX_PATH_LEN 1024

/**
* @brief Execute a Qubes RPC command.
* @param cmdline Null-terminated command to execute.
Expand Down
66 changes: 55 additions & 11 deletions qrexec/tests/socket/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import getpass
import itertools
import asyncio
import shlex

import psutil
import pytest
Expand Down Expand Up @@ -71,6 +72,9 @@ def assertExpectedStdout(self, target, expected_stdout: bytes, *, exit_code=0):
self.assertEqual(msg_type, qrexec.MSG_DATA_STDOUT)
stdout_entries.append(msg_body)

def make_executable_service(self, *args):
util.make_executable_service(self.tempdir, *args)

def setUp(self):
self.tempdir = tempfile.mkdtemp()
os.mkdir(os.path.join(self.tempdir, "local-rpc"))
Expand Down Expand Up @@ -139,7 +143,6 @@ def connect_client(self):
self.addCleanup(client.close)
return client


@unittest.skipIf(os.environ.get("SKIP_SOCKET_TESTS"), "socket tests not set up")
class TestAgent(TestAgentBase):
def test_handshake(self):
Expand All @@ -148,17 +151,14 @@ def test_handshake(self):
dom0 = self.connect_dom0()
dom0.handshake()

def test_just_exec(self):
def _test_just_exec(self, cmd):
self.start_agent()

dom0 = self.connect_dom0()
dom0.handshake()

user = getpass.getuser().encode("ascii")

cmd = ("touch " + os.path.join(self.tempdir, "new_file")).encode(
"ascii"
)
dom0.send_message(
qrexec.MSG_JUST_EXEC,
struct.pack("<LL", self.target_domain, self.target_port)
Expand All @@ -170,17 +170,64 @@ def test_just_exec(self):

target = self.connect_target()
target.handshake()
return target, dom0

def test_just_exec_socket(self):
socket_path = os.path.join(
self.tempdir, "rpc", "qubes.SocketService+"
)
server = qrexec.socket_server(socket_path)

cmd = b"QUBESRPC qubes.SocketService a"
target, dom0 = self._test_just_exec(cmd)
server.accept()
self.assertEqual(server.recvall(len(cmd)), cmd[9:] + b"\0")
self.assertListEqual(
target.recv_all_messages(),
[
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
],
)

self.check_dom0(dom0)

def test_just_exec(self):
fifo = os.path.join(self.tempdir, "new_file")
os.mkfifo(fifo, mode=0o600)
cmd = ("echo a >> " + shlex.quote(fifo)).encode("ascii", "strict")
target, dom0 = self._test_just_exec(cmd)
self.assertListEqual(
target.recv_all_messages(),
[
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
],
)
with open(fifo, "rb") as f:
self.assertEqual(f.read(), b"a\n")
self.check_dom0(dom0)

util.wait_until(
lambda: os.path.exists(os.path.join(self.tempdir, "new_file")),
"file created",
def test_just_exec_rpc(self):
fifo = os.path.join(self.tempdir, "new_file")
os.mkfifo(fifo, mode=0o600)
util.make_executable_service(
self.tempdir,
"rpc",
"qubes.Service",
fr"""#!/bin/bash -eu
printf %s\\n "$QREXEC_SERVICE_FULL_NAME" >> {shlex.quote(fifo)}
""",
)
cmd = b"QUBESRPC qubes.Service+ domX"
target, dom0 = self._test_just_exec(cmd)
self.assertListEqual(
target.recv_all_messages(),
[
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
],
)

with open(fifo, "rb") as f:
self.assertEqual(f.read(), b"qubes.Service+\n")
self.check_dom0(dom0)

def test_exec_cmdline(self):
Expand Down Expand Up @@ -305,9 +352,6 @@ def execute_qubesrpc(self, service: str, src_domain_name: str):
target.handshake()
return target, dom0

def make_executable_service(self, *args):
util.make_executable_service(self.tempdir, *args)

def test_exec_service(self):
util.make_executable_service(
self.tempdir,
Expand Down

0 comments on commit dfdfb33

Please sign in to comment.