Skip to content

Commit dfdfb33

Browse files
committed
Support socket services with MSG_JUST_EXEC
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.
1 parent daee92e commit dfdfb33

File tree

4 files changed

+115
-51
lines changed

4 files changed

+115
-51
lines changed

agent/qrexec-agent-data.c

+10
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ static int handle_just_exec(struct qrexec_parsed_command *cmd)
136136

137137
if (cmd == NULL)
138138
return 127;
139+
140+
if (cmd->service_descriptor) {
141+
int socket_fd;
142+
struct buffer stdin_buffer;
143+
buffer_init(&stdin_buffer);
144+
if (!find_qrexec_service(cmd, &socket_fd, &stdin_buffer))
145+
return 127;
146+
if (socket_fd != -1)
147+
return write_all(socket_fd, stdin_buffer.data, stdin_buffer.buflen) ? 0 : 127;
148+
}
139149
switch (pid = fork()) {
140150
case -1:
141151
PERROR("fork");

libqrexec/exec.c

+31-39
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,6 @@ static int do_fork_exec(const char *user,
162162
return retval;
163163
}
164164

165-
#define QUBES_SOCKADDR_UN_MAX_PATH_LEN 1024
166-
167165
static int qubes_connect(int s, const char *connect_path, const size_t total_path_length) {
168166
// Avoiding an extra copy is NOT worth it!
169167
#define QUBES_TMP_DIRECTORY "/tmp/qrexec-XXXXXX"
@@ -205,11 +203,6 @@ static int qubes_connect(int s, const char *connect_path, const size_t total_pat
205203
return result;
206204
}
207205

208-
static int execute_qrexec_service(
209-
const struct qrexec_parsed_command *cmd,
210-
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
211-
struct buffer *stdin_buffer);
212-
213206
/*
214207
Find a file in the ':'-delimited list of paths given in path_list.
215208
Returns 0 on success, -1 if the file is definitely absent in all of the
@@ -461,83 +454,82 @@ int execute_parsed_qubes_rpc_command(
461454
int *stdout_fd, int *stderr_fd, struct buffer *stdin_buffer) {
462455
if (cmd->service_descriptor) {
463456
// Proper Qubes RPC call
464-
return execute_qrexec_service(
465-
cmd, pid, stdin_fd, stdout_fd, stderr_fd, stdin_buffer);
457+
if (!find_qrexec_service(cmd, stdin_fd, stdin_buffer))
458+
return -1;
459+
if (*stdin_fd > -1) {
460+
*stdout_fd = *stdin_fd;
461+
if (stderr_fd)
462+
*stderr_fd = -1;
463+
*pid = 0;
464+
return 0;
465+
}
466+
return do_fork_exec(cmd->username, cmd->command,
467+
pid, stdin_fd, stdout_fd, stderr_fd);
466468
} else {
467469
// Legacy qrexec behavior: spawn shell directly
468470
return do_fork_exec(cmd->username, cmd->command,
469471
pid, stdin_fd, stdout_fd, stderr_fd);
470472
}
471473
}
472474

473-
static int execute_qrexec_service(
475+
bool find_qrexec_service(
474476
const struct qrexec_parsed_command *cmd,
475-
int *pid, int *stdin_fd, int *stdout_fd, int *stderr_fd,
476-
struct buffer *stdin_buffer) {
477-
477+
int *socket_fd, struct buffer *stdin_buffer) {
478478
assert(cmd->service_descriptor);
479479

480+
char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN];
481+
struct buffer path_buffer = { .data = file_path, .buflen = (int)sizeof(file_path) };
480482
const char *qrexec_service_path = getenv("QREXEC_SERVICE_PATH");
481483
if (!qrexec_service_path)
482484
qrexec_service_path = QREXEC_SERVICE_PATH;
485+
*socket_fd = -1;
483486

484-
char service_full_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN];
485487
struct stat statbuf;
486488

487489
int ret = find_file(qrexec_service_path, cmd->service_descriptor,
488-
service_full_path, sizeof(service_full_path),
490+
path_buffer.data, (size_t)path_buffer.buflen,
489491
&statbuf);
490492
if (ret == -1)
491493
ret = find_file(qrexec_service_path, cmd->service_name,
492-
service_full_path, sizeof(service_full_path),
494+
path_buffer.data, (size_t)path_buffer.buflen,
493495
&statbuf);
494496
if (ret < 0) {
495497
LOG(ERROR, "Service not found: %s",
496498
cmd->service_descriptor);
497-
return -1;
499+
return false;
498500
}
499501

500502
if (S_ISSOCK(statbuf.st_mode)) {
501503
/* Socket-based service. */
502504
int s;
503505
if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
504506
PERROR("socket");
505-
return -1;
507+
return false;
506508
}
507-
if (qubes_connect(s, service_full_path, strlen(service_full_path))) {
509+
if (qubes_connect(s, path_buffer.data, strlen(path_buffer.data))) {
508510
PERROR("qubes_connect");
509511
close(s);
510-
return -1;
512+
return false;
511513
}
512514

513-
*stdout_fd = *stdin_fd = s;
514-
if (stderr_fd)
515-
*stderr_fd = -1;
516-
*pid = 0;
517-
set_nonblock(s);
518-
519515
if (cmd->send_service_descriptor) {
520516
/* send part after "QUBESRPC ", including trailing NUL */
521517
const char *desc = cmd->command + RPC_REQUEST_COMMAND_LEN + 1;
522518
buffer_append(stdin_buffer, desc, strlen(desc) + 1);
523519
}
524-
return 0;
525-
}
526520

527-
if (euidaccess(service_full_path, X_OK) == 0) {
528-
/*
529-
Executable-based service.
521+
*socket_fd = s;
522+
return true;
523+
}
530524

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

538-
LOG(ERROR, "Unknown service type (not executable, not a socket): %s",
539-
service_full_path);
540-
return -1;
530+
LOG(ERROR, "Unknown service type (not executable, not a socket): %.*s",
531+
path_buffer.buflen, path_buffer.data);
532+
return false;
541533
}
542534

543535
int exec_wait_for_session(const char *source_domain) {

libqrexec/libqrexec-utils.h

+19-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ int write_stdin(int fd, const char *data, int len, struct buffer *buffer);
127127

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

147+
/**
148+
* @brief Find the implementation of a Qubes RPC command. If it is a socket,
149+
* connect to it.
150+
* @param[in] cmdline Null-terminated command to execute.
151+
* @param[out] socket_fd On return, holds a file descriptor connected to the socket,
152+
* or -1 for executable services.
153+
* @param stdin_buffer This buffer will need to be prepended to the child process’s
154+
* stdin.
155+
* @return true if the implementation is found (and, for sockets, connected to)
156+
* successfully, false on failure.
157+
*/
158+
bool find_qrexec_service(
159+
const struct qrexec_parsed_command *cmd,
160+
int *socket_fd, struct buffer *stdin_buffer);
161+
162+
/** Suggested buffer size for the path buffer of find_qrexec_service. */
163+
#define QUBES_SOCKADDR_UN_MAX_PATH_LEN 1024
164+
147165
/**
148166
* @brief Execute a Qubes RPC command.
149167
* @param cmdline Null-terminated command to execute.

qrexec/tests/socket/agent.py

+55-11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import getpass
2828
import itertools
2929
import asyncio
30+
import shlex
3031

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

75+
def make_executable_service(self, *args):
76+
util.make_executable_service(self.tempdir, *args)
77+
7478
def setUp(self):
7579
self.tempdir = tempfile.mkdtemp()
7680
os.mkdir(os.path.join(self.tempdir, "local-rpc"))
@@ -139,7 +143,6 @@ def connect_client(self):
139143
self.addCleanup(client.close)
140144
return client
141145

142-
143146
@unittest.skipIf(os.environ.get("SKIP_SOCKET_TESTS"), "socket tests not set up")
144147
class TestAgent(TestAgentBase):
145148
def test_handshake(self):
@@ -148,17 +151,14 @@ def test_handshake(self):
148151
dom0 = self.connect_dom0()
149152
dom0.handshake()
150153

151-
def test_just_exec(self):
154+
def _test_just_exec(self, cmd):
152155
self.start_agent()
153156

154157
dom0 = self.connect_dom0()
155158
dom0.handshake()
156159

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

159-
cmd = ("touch " + os.path.join(self.tempdir, "new_file")).encode(
160-
"ascii"
161-
)
162162
dom0.send_message(
163163
qrexec.MSG_JUST_EXEC,
164164
struct.pack("<LL", self.target_domain, self.target_port)
@@ -170,17 +170,64 @@ def test_just_exec(self):
170170

171171
target = self.connect_target()
172172
target.handshake()
173+
return target, dom0
174+
175+
def test_just_exec_socket(self):
176+
socket_path = os.path.join(
177+
self.tempdir, "rpc", "qubes.SocketService+"
178+
)
179+
server = qrexec.socket_server(socket_path)
180+
181+
cmd = b"QUBESRPC qubes.SocketService a"
182+
target, dom0 = self._test_just_exec(cmd)
183+
server.accept()
184+
self.assertEqual(server.recvall(len(cmd)), cmd[9:] + b"\0")
185+
self.assertListEqual(
186+
target.recv_all_messages(),
187+
[
188+
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
189+
],
190+
)
191+
192+
self.check_dom0(dom0)
193+
194+
def test_just_exec(self):
195+
fifo = os.path.join(self.tempdir, "new_file")
196+
os.mkfifo(fifo, mode=0o600)
197+
cmd = ("echo a >> " + shlex.quote(fifo)).encode("ascii", "strict")
198+
target, dom0 = self._test_just_exec(cmd)
173199
self.assertListEqual(
174200
target.recv_all_messages(),
175201
[
176202
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
177203
],
178204
)
205+
with open(fifo, "rb") as f:
206+
self.assertEqual(f.read(), b"a\n")
207+
self.check_dom0(dom0)
179208

180-
util.wait_until(
181-
lambda: os.path.exists(os.path.join(self.tempdir, "new_file")),
182-
"file created",
209+
def test_just_exec_rpc(self):
210+
fifo = os.path.join(self.tempdir, "new_file")
211+
os.mkfifo(fifo, mode=0o600)
212+
util.make_executable_service(
213+
self.tempdir,
214+
"rpc",
215+
"qubes.Service",
216+
fr"""#!/bin/bash -eu
217+
printf %s\\n "$QREXEC_SERVICE_FULL_NAME" >> {shlex.quote(fifo)}
218+
""",
183219
)
220+
cmd = b"QUBESRPC qubes.Service+ domX"
221+
target, dom0 = self._test_just_exec(cmd)
222+
self.assertListEqual(
223+
target.recv_all_messages(),
224+
[
225+
(qrexec.MSG_DATA_EXIT_CODE, b"\0\0\0\0"),
226+
],
227+
)
228+
229+
with open(fifo, "rb") as f:
230+
self.assertEqual(f.read(), b"qubes.Service+\n")
184231
self.check_dom0(dom0)
185232

186233
def test_exec_cmdline(self):
@@ -305,9 +352,6 @@ def execute_qubesrpc(self, service: str, src_domain_name: str):
305352
target.handshake()
306353
return target, dom0
307354

308-
def make_executable_service(self, *args):
309-
util.make_executable_service(self.tempdir, *args)
310-
311355
def test_exec_service(self):
312356
util.make_executable_service(
313357
self.tempdir,

0 commit comments

Comments
 (0)