diff --git a/agent/qrexec-agent-data.c b/agent/qrexec-agent-data.c index f84cff61..4c364350 100644 --- a/agent/qrexec-agent-data.c +++ b/agent/qrexec-agent-data.c @@ -136,6 +136,20 @@ static int handle_just_exec(struct qrexec_parsed_command *cmd) if (cmd == NULL) return 127; + + char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN]; + struct buffer buf = { .data = file_path, .buflen = (int)sizeof(file_path) }; + struct buffer stdin_buffer; + buffer_init(&stdin_buffer); + if (cmd->service_descriptor) { + int socket_fd; + if (!find_qrexec_service(cmd, &socket_fd, &stdin_buffer, &buf)) + return 127; + if (socket_fd != -1) + return write_all(socket_fd, stdin_buffer.data, stdin_buffer.buflen) ? 0 : 127; + } else { + buf.data = NULL; + } switch (pid = fork()) { case -1: PERROR("fork"); diff --git a/libqrexec/buffer.c b/libqrexec/buffer.c index 1c49a07e..217d94dd 100644 --- a/libqrexec/buffer.c +++ b/libqrexec/buffer.c @@ -35,12 +35,12 @@ static char *limited_malloc(int len) (total_mem > BUFFER_LIMIT) || (len <= 0)) { LOG(ERROR, "attempt to allocate >BUFFER_LIMIT"); - exit(1); + abort(); } ret = malloc((size_t)len); if (!ret) { PERROR("malloc"); - exit(1); + abort(); } return ret; } @@ -83,11 +83,11 @@ void buffer_append(struct buffer *b, const char *data, int len) assert(data != NULL && "NULL data"); if (b->buflen < 0 || b->buflen > BUFFER_LIMIT) { LOG(ERROR, "buffer_append buflen %d", len); - exit(1); + abort(); } if (len < 0 || len > BUFFER_LIMIT) { LOG(ERROR, "buffer_append %d", len); - exit(1); + abort(); } if (len == 0) return; @@ -108,7 +108,7 @@ void buffer_remove(struct buffer *b, int len) char *qdata = NULL; if (len < 0 || len > b->buflen) { LOG(ERROR, "buffer_remove %d/%d", len, b->buflen); - exit(1); + abort(); } newsize = b->buflen - len; if (newsize > 0) { diff --git a/libqrexec/exec.c b/libqrexec/exec.c index 1ec94bf6..29ab93d8 100644 --- a/libqrexec/exec.c +++ b/libqrexec/exec.c @@ -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" @@ -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 @@ -461,8 +454,19 @@ 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); + char file_path[QUBES_SOCKADDR_UN_MAX_PATH_LEN]; + struct buffer buf = { .data = file_path, .buflen = (int)sizeof(file_path) }; + if (!find_qrexec_service(cmd, stdin_fd, stdin_buffer, &buf)) + 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, @@ -470,74 +474,64 @@ int execute_parsed_qubes_rpc_command( } } -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, + struct buffer *path_buffer) { assert(cmd->service_descriptor); + assert(path_buffer->buflen > NAME_MAX); 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) { + if ((s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) { PERROR("socket"); - return -1; + return false; } - 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; } - *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", + path_buffer->buflen, path_buffer->data); + return false; } int exec_wait_for_session(const char *source_domain) { diff --git a/libqrexec/libqrexec-utils.h b/libqrexec/libqrexec-utils.h index a74b3c31..02d0eaef 100644 --- a/libqrexec/libqrexec-utils.h +++ b/libqrexec/libqrexec-utils.h @@ -128,7 +128,7 @@ int fork_and_flush_stdin(int fd, 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. @@ -145,6 +145,28 @@ 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. + * @param path_buffer This buffer (NUL-terminated) holds the service's path. On + * entry it must be at least NAME_MAX bytes. It will not be freed or reallocated. + * Its contents should be ignored if stdout_fd is not -1. + * @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, + struct buffer *path_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. diff --git a/qrexec/tests/socket/agent.py b/qrexec/tests/socket/agent.py index 8aaa5fbc..5b350600 100644 --- a/qrexec/tests/socket/agent.py +++ b/qrexec/tests/socket/agent.py @@ -27,6 +27,7 @@ import getpass import itertools import asyncio +import shlex import psutil import pytest @@ -47,6 +48,9 @@ class TestAgentBase(unittest.TestCase): target_domain = 43 target_port = 1024 + 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")) @@ -115,6 +119,14 @@ def connect_client(self): self.addCleanup(client.close) return client + def check_dom0(self, dom0): + self.assertEqual( + dom0.recv_message(), + ( + qrexec.MSG_CONNECTION_TERMINATED, + struct.pack("