diff --git a/qrexec/Makefile b/qrexec/Makefile index 06e2489..abf1961 100644 --- a/qrexec/Makefile +++ b/qrexec/Makefile @@ -2,9 +2,11 @@ CC=gcc CFLAGS+=-I. -g -Wall -Wextra -Werror -pie -fPIC `pkg-config --cflags vchan-$(BACKEND_VMM)` LIBS=`pkg-config --libs vchan-$(BACKEND_VMM)` -lqrexec-utils -all: qrexec-agent qrexec-client-vm +all: qrexec-agent qrexec-client-vm qrexec-fork-server qrexec-agent: qrexec-agent.o qrexec-agent-data.o $(CC) -pie -g -o qrexec-agent qrexec-agent.o qrexec-agent-data.o $(LIBS) +qrexec-fork-server: qrexec-fork-server.o qrexec-agent-data.o + $(CC) -pie -g -o qrexec-fork-server qrexec-fork-server.o qrexec-agent-data.o $(LIBS) qrexec-client-vm: qrexec-client-vm.o $(CC) -pie -g -o qrexec-client-vm qrexec-client-vm.o clean: @@ -12,9 +14,10 @@ clean: install: install -d $(DESTDIR)/etc/qubes-rpc - install -d $(DESTDIR)/usr/lib/qubes + install -d $(DESTDIR)/usr/lib/qubes $(DESTDIR)/usr/bin install qrexec-agent $(DESTDIR)/usr/lib/qubes install qrexec-client-vm $(DESTDIR)/usr/lib/qubes ln -s qrexec-client-vm $(DESTDIR)/usr/lib/qubes/qrexec_client_vm + install qrexec-fork-server $(DESTDIR)/usr/bin install qubes-rpc-multiplexer $(DESTDIR)/usr/lib/qubes diff --git a/qrexec/qrexec-agent-data.c b/qrexec/qrexec-agent-data.c index 5620255..1378e29 100644 --- a/qrexec/qrexec-agent-data.c +++ b/qrexec/qrexec-agent-data.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include "qrexec.h" @@ -38,52 +39,71 @@ #define VCHAN_BUFFER_SIZE 65536 static volatile int child_exited; +static volatile int stdio_socket_requested; int stdout_msg_type = MSG_DATA_STDOUT; pid_t child_process_pid; static void sigchld_handler(int __attribute__((__unused__))x) { - child_exited = 1; - signal(SIGCHLD, sigchld_handler); + child_exited = 1; + signal(SIGCHLD, sigchld_handler); } - -void no_colon_in_cmd() +static void sigusr1_handler(int __attribute__((__unused__))x) { - fprintf(stderr, - "cmdline is supposed to be in user:command form\n"); - exit(1); + stdio_socket_requested = 1; + signal(SIGUSR1, SIG_IGN); } -void do_exec(char *cmd) +int handle_handshake(libvchan_t *ctrl) { - char buf[strlen(QUBES_RPC_MULTIPLEXER_PATH) + strlen(cmd) - strlen(RPC_REQUEST_COMMAND) + 1]; - char *realcmd = index(cmd, ':'); - if (!realcmd) - no_colon_in_cmd(); - /* mark end of username and move to command */ - *realcmd = 0; - realcmd++; - /* ignore "nogui:" prefix in linux agent */ - if (strncmp(realcmd, "nogui:", 6) == 0) - realcmd+=6; - /* replace magic RPC cmd with RPC multiplexer path */ - if (strncmp(realcmd, RPC_REQUEST_COMMAND " ", strlen(RPC_REQUEST_COMMAND)+1)==0) { - strcpy(buf, QUBES_RPC_MULTIPLEXER_PATH); - strcpy(buf + strlen(QUBES_RPC_MULTIPLEXER_PATH), realcmd + strlen(RPC_REQUEST_COMMAND)); - realcmd = buf; - } - signal(SIGCHLD, SIG_DFL); - signal(SIGPIPE, SIG_DFL); + struct msg_header hdr; + struct peer_info info; - execl("/bin/su", "su", "-", cmd, "-c", realcmd, NULL); - perror("execl"); - exit(1); + /* send own HELLO */ + hdr.type = MSG_HELLO; + hdr.len = sizeof(info); + info.version = QREXEC_PROTOCOL_VERSION; + + if (libvchan_send(ctrl, &hdr, sizeof(hdr)) != sizeof(hdr)) { + fprintf(stderr, "Failed to send HELLO hdr to agent\n"); + return -1; + } + + if (libvchan_send(ctrl, &info, sizeof(info)) != sizeof(info)) { + fprintf(stderr, "Failed to send HELLO hdr to agent\n"); + return -1; + } + + /* receive MSG_HELLO from remote */ + if (libvchan_recv(ctrl, &hdr, sizeof(hdr)) != sizeof(hdr)) { + fprintf(stderr, "Failed to read agent HELLO hdr\n"); + return -1; + } + + if (hdr.type != MSG_HELLO || hdr.len != sizeof(info)) { + fprintf(stderr, "Invalid HELLO packet received: type %d, len %d\n", hdr.type, hdr.len); + return -1; + } + + if (libvchan_recv(ctrl, &info, sizeof(info)) != sizeof(info)) { + fprintf(stderr, "Failed to read agent HELLO body\n"); + return -1; + } + + if (info.version != QREXEC_PROTOCOL_VERSION) { + fprintf(stderr, "Incompatible agent protocol version (remote %d, local %d)\n", info.version, QREXEC_PROTOCOL_VERSION); + return -1; + } + + + return 0; } + int handle_just_exec(char *cmdline) { - int fdn, pid; + int fdn, pid; switch (pid = fork()) { case -1: @@ -97,20 +117,20 @@ int handle_just_exec(char *cmdline) exit(1); default:; } - fprintf(stderr, "executed (nowait) %s pid %d\n", cmdline, pid); + fprintf(stderr, "executed (nowait) %s pid %d\n", cmdline, pid); return 0; } void send_exit_code(libvchan_t *data_vchan, int status) { - struct msg_header hdr; - hdr.type = MSG_DATA_EXIT_CODE; - hdr.len = sizeof(status); - if (libvchan_send(data_vchan, &hdr, sizeof(hdr)) < 0) - handle_vchan_error("write hdr"); - if (libvchan_send(data_vchan, &status, sizeof(status)) < 0) - handle_vchan_error("write status"); - fprintf(stderr, "send exit code %d\n", status); + struct msg_header hdr; + hdr.type = MSG_DATA_EXIT_CODE; + hdr.len = sizeof(status); + if (libvchan_send(data_vchan, &hdr, sizeof(hdr)) < 0) + handle_vchan_error("write hdr"); + if (libvchan_send(data_vchan, &status, sizeof(status)) < 0) + handle_vchan_error("write status"); + fprintf(stderr, "send exit code %d\n", status); } /* handle data from specified FD and send over vchan link @@ -146,7 +166,10 @@ int handle_input(libvchan_t *vchan, int fd, int msg_type) return -1; if (len == 0) { - close(fd); + if (shutdown(fd, SHUT_RD) < 0) { + if (errno == ENOTSOCK) + close(fd); + } return 0; } } @@ -188,14 +211,20 @@ int handle_remote_data(libvchan_t *data_vchan, int stdin_fd) /* discard the data */ continue; if (hdr.len == 0) { - close(stdin_fd); + if (shutdown(stdin_fd, SHUT_WR) < 0) { + if (errno == ENOTSOCK) + close(stdin_fd); + } stdin_fd = -1; return 0; } else { /* FIXME: use buffered write here to prevent deadlock */ if (!write_all(stdin_fd, buf, hdr.len)) { - if (errno == EPIPE) { - close(stdin_fd); + if (errno == EPIPE || errno == ECONNRESET) { + if (shutdown(stdin_fd, SHUT_WR) < 0) { + if (errno == ENOTSOCK) + close(stdin_fd); + } stdin_fd = -1; } else { perror("write"); @@ -248,7 +277,10 @@ void process_child_io(libvchan_t *data_vchan, if (pid == child_process_pid) { child_process_status = WEXITSTATUS(status); if (stdin_fd >= 0) { - close(stdin_fd); + if (shutdown(stdin_fd, SHUT_WR) < 0) { + if (errno == ENOTSOCK) + close(stdin_fd); + } stdin_fd = -1; } } @@ -264,6 +296,13 @@ void process_child_io(libvchan_t *data_vchan, } break; } + /* child signaled desire to use single socket for both stdin and stdout */ + if (stdio_socket_requested) { + if (stdout_fd != -1) + close(stdout_fd); + stdout_fd = stdin_fd; + stdio_socket_requested = 0; + } /* otherwise handle the events */ FD_ZERO(&rdset); @@ -337,7 +376,10 @@ void process_child_io(libvchan_t *data_vchan, break; case -2: /* remote process exited, no sense in sending more data to it */ - close(stdout_fd); + if (shutdown(stdout_fd, SHUT_RD) < 0) { + if (errno == ENOTSOCK) + close(stdout_fd); + } stdout_fd = -1; close(stderr_fd); stderr_fd = -1; @@ -353,6 +395,7 @@ pid_t handle_new_process(int type, int connect_domain, int connect_port, libvchan_t *data_vchan; pid_t pid; int stdin_fd, stdout_fd, stderr_fd; + char pid_s[10]; if (type == MSG_SERVICE_CONNECT) { if (cmdline_len != sizeof(*svc_params)) { @@ -394,6 +437,10 @@ pid_t handle_new_process(int type, int connect_domain, int connect_port, handle_handshake(data_vchan); signal(SIGCHLD, sigchld_handler); + signal(SIGUSR1, sigusr1_handler); + snprintf(pid_s, sizeof(pid_s), "%d", getpid()); + setenv("QREXEC_AGENT_PID", pid_s, 1); + /* TODO: use setresuid to allow child process to actually send the signal? */ switch (type) { case MSG_JUST_EXEC: diff --git a/qrexec/qrexec-agent.c b/qrexec/qrexec-agent.c index f00e133..d10d59e 100644 --- a/qrexec/qrexec-agent.c +++ b/qrexec/qrexec-agent.c @@ -19,7 +19,10 @@ * */ +#define _GNU_SOURCE #include +#include +#include #include #include #include @@ -38,7 +41,8 @@ #include "qrexec-agent.h" struct _connection_info { - int pid; + int pid; /* pid of child process handling the data */ + int fd; /* socket to process handling the data (wait for EOF here) */ int connect_domain; int connect_port; }; @@ -55,7 +59,38 @@ int passfd_socket; int meminfo_write_started = 0; -void do_exec(const char *cmd); +void no_colon_in_cmd() +{ + fprintf(stderr, + "cmdline is supposed to be in user:command form\n"); + exit(1); +} + +void do_exec(const char *cmd) +{ + char buf[strlen(QUBES_RPC_MULTIPLEXER_PATH) + strlen(cmd) - strlen(RPC_REQUEST_COMMAND) + 1]; + char *realcmd = index(cmd, ':'); + if (!realcmd) + no_colon_in_cmd(); + /* mark end of username and move to command */ + *realcmd = 0; + realcmd++; + /* ignore "nogui:" prefix in linux agent */ + if (strncmp(realcmd, "nogui:", 6) == 0) + realcmd+=6; + /* replace magic RPC cmd with RPC multiplexer path */ + if (strncmp(realcmd, RPC_REQUEST_COMMAND " ", strlen(RPC_REQUEST_COMMAND)+1)==0) { + strcpy(buf, QUBES_RPC_MULTIPLEXER_PATH); + strcpy(buf + strlen(QUBES_RPC_MULTIPLEXER_PATH), realcmd + strlen(RPC_REQUEST_COMMAND)); + realcmd = buf; + } + signal(SIGCHLD, SIG_DFL); + signal(SIGPIPE, SIG_DFL); + + execl("/bin/su", "su", "-", cmd, "-c", realcmd, NULL); + perror("execl"); + exit(1); +} void handle_vchan_error(const char *op) { @@ -63,51 +98,6 @@ void handle_vchan_error(const char *op) exit(1); } -int handle_handshake(libvchan_t *ctrl) -{ - struct msg_header hdr; - struct peer_info info; - - /* send own HELLO */ - hdr.type = MSG_HELLO; - hdr.len = sizeof(info); - info.version = QREXEC_PROTOCOL_VERSION; - - if (libvchan_send(ctrl, &hdr, sizeof(hdr)) != sizeof(hdr)) { - fprintf(stderr, "Failed to send HELLO hdr to agent\n"); - return -1; - } - - if (libvchan_send(ctrl, &info, sizeof(info)) != sizeof(info)) { - fprintf(stderr, "Failed to send HELLO hdr to agent\n"); - return -1; - } - - /* receive MSG_HELLO from remote */ - if (libvchan_recv(ctrl, &hdr, sizeof(hdr)) != sizeof(hdr)) { - fprintf(stderr, "Failed to read agent HELLO hdr\n"); - return -1; - } - - if (hdr.type != MSG_HELLO || hdr.len != sizeof(info)) { - fprintf(stderr, "Invalid HELLO packet received: type %d, len %d\n", hdr.type, hdr.len); - return -1; - } - - if (libvchan_recv(ctrl, &info, sizeof(info)) != sizeof(info)) { - fprintf(stderr, "Failed to read agent HELLO body\n"); - return -1; - } - - if (info.version != QREXEC_PROTOCOL_VERSION) { - fprintf(stderr, "Incompatible agent protocol version (remote %d, local %d)\n", info.version, QREXEC_PROTOCOL_VERSION); - return -1; - } - - - return 0; -} - void init() { /* FIXME: This 0 is remote domain ID */ @@ -161,13 +151,70 @@ void wake_meminfo_writer() meminfo_write_started = 1; } -void register_vchan_connection(pid_t pid, int domain, int port) +int try_fork_server(int type, int connect_domain, int connect_port, + char *cmdline, int cmdline_len) { + char username[cmdline_len]; + char *colon; + char *fork_server_socket_path; + int s, len; + struct sockaddr_un remote; + struct qrexec_cmd_info info; + + strncpy(username, cmdline, cmdline_len); + colon = index(username, ':'); + if (!colon) + return -1; + *colon = '\0'; + + if (asprintf(&fork_server_socket_path, QREXEC_FORK_SERVER_SOCKET, username) < 0) { + fprintf(stderr, "Memory allocation failed\n"); + return -1; + } + + remote.sun_family = AF_UNIX; + strncpy(remote.sun_path, fork_server_socket_path, + sizeof(remote.sun_path)); + free(fork_server_socket_path); + + if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + perror("socket"); + return -1; + } + len = strlen(remote.sun_path) + sizeof(remote.sun_family); + if (connect(s, (struct sockaddr *) &remote, len) == -1) { + if (errno != ECONNREFUSED) + perror("connect"); + close(s); + return -1; + } + + info.type = type; + info.connect_domain = connect_domain; + info.connect_port = connect_port; + info.cmdline_len = cmdline_len-(strlen(username)+1); + if (!write_all(s, &info, sizeof(info))) { + perror("write"); + close(s); + return -1; + } + if (!write_all(s, colon+1, info.cmdline_len)) { + perror("write"); + close(s); + return -1; + } + + return s; +} + + +void register_vchan_connection(pid_t pid, int fd, int domain, int port) { int i; for (i = 0; i < MAX_FDS; i++) { if (connection_info[i].pid == 0) { connection_info[i].pid = pid; + connection_info[i].fd = fd; connection_info[i].connect_domain = domain; connection_info[i].connect_port = port; return; @@ -190,11 +237,23 @@ void handle_server_exec_request(struct msg_header *hdr) if (libvchan_recv(ctrl_vchan, buf, hdr->len-sizeof(params)) < 0) handle_vchan_error("read exec cmd"); + if ((hdr->type == MSG_EXEC_CMDLINE || hdr->type == MSG_JUST_EXEC) && + !strstr(buf, ":nogui:")) { + int child_socket = try_fork_server(hdr->type, + params.connect_domain, params.connect_port, + buf, hdr->len-sizeof(params)); + if (child_socket >= 0) { + register_vchan_connection(-1, child_socket, + params.connect_domain, params.connect_port); + return; + } + } + child_agent = handle_new_process(hdr->type, params.connect_domain, params.connect_port, buf, hdr->len-sizeof(params)); - register_vchan_connection(child_agent, + register_vchan_connection(child_agent, -1, params.connect_domain, params.connect_port); } @@ -262,27 +321,31 @@ int find_connection(int pid) return -1; } +void release_connection(int id) { + struct msg_header hdr; + struct exec_params params; + + hdr.type = MSG_CONNECTION_TERMINATED; + hdr.len = sizeof(struct exec_params); + params.connect_domain = connection_info[id].connect_domain; + params.connect_port = connection_info[id].connect_port; + if (libvchan_send(ctrl_vchan, &hdr, sizeof(hdr)) < 0) + handle_vchan_error("send"); + if (libvchan_send(ctrl_vchan, ¶ms, sizeof(params)) < 0) + handle_vchan_error("send"); + connection_info[id].pid = 0; +} void reap_children() { int status; int pid; int id; - struct msg_header hdr; - struct exec_params params; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { id = find_connection(pid); if (id < 0) continue; - hdr.type = MSG_CONNECTION_TERMINATED; - hdr.len = sizeof(struct exec_params); - params.connect_domain = connection_info[id].connect_domain; - params.connect_port = connection_info[id].connect_port; - if (libvchan_send(ctrl_vchan, &hdr, sizeof(hdr)) < 0) - handle_vchan_error("send"); - if (libvchan_send(ctrl_vchan, ¶ms, sizeof(params)) < 0) - handle_vchan_error("send"); - connection_info[id].pid = 0; + release_connection(id); } child_exited = 0; } @@ -290,6 +353,7 @@ void reap_children() int fill_fds_for_select(fd_set * rdset, fd_set * wrset) { int max = -1; + int i; FD_ZERO(rdset); FD_ZERO(wrset); @@ -299,6 +363,14 @@ int fill_fds_for_select(fd_set * rdset, fd_set * wrset) FD_SET(passfd_socket, rdset); if (passfd_socket > max) max = passfd_socket; + + for (i = 0; i < MAX_FDS; i++) { + if (connection_info[i].pid != 0 && connection_info[i].fd != -1) { + FD_SET(connection_info[i].fd, rdset); + if (connection_info[i].fd > max) + max = connection_info[i].fd; + } + } return max; } @@ -339,6 +411,24 @@ void handle_trigger_io() } } +void handle_terminated_fork_client(fd_set *rdset) { + int i, ret; + char buf[2]; + + for (i = 0; i < MAX_FDS; i++) { + if (connection_info[i].pid && connection_info[i].fd >= 0 && + FD_ISSET(connection_info[i].fd, rdset)) { + ret = read(connection_info[i].fd, buf, sizeof(buf)); + if (ret == 0 || (ret == -1 && errno == ECONNRESET)) { + close(connection_info[i].fd); + release_connection(i); + } else { + fprintf(stderr, "Unexpected read on fork-server connection: %d(%s)\n", ret, strerror(errno)); + } + } + } +} + int main() { fd_set rdset, wrset; @@ -372,5 +462,7 @@ int main() if (FD_ISSET(trigger_fd, &rdset)) handle_trigger_io(); + + handle_terminated_fork_client(&rdset); } } diff --git a/qrexec/qrexec-agent.h b/qrexec/qrexec-agent.h index 38c554b..d2f10e8 100644 --- a/qrexec/qrexec-agent.h +++ b/qrexec/qrexec-agent.h @@ -19,9 +19,20 @@ * */ +#define QREXEC_FORK_SERVER_SOCKET "/var/run/qubes/qrexec-server.%s.sock" + int handle_handshake(libvchan_t *ctrl); void handle_vchan_error(const char *op); +void do_exec(const char *cmd); pid_t handle_new_process(int type, int connect_domain, int connect_port, char *cmdline, int cmdline_len); + +struct qrexec_cmd_info { + int type; + int connect_domain; + int connect_port; + int cmdline_len; + char cmdline[0]; +}; diff --git a/qrexec/qrexec-fork-server.c b/qrexec/qrexec-fork-server.c new file mode 100644 index 0000000..da03238 --- /dev/null +++ b/qrexec/qrexec-fork-server.c @@ -0,0 +1,110 @@ +/* + * The Qubes OS Project, http://www.qubes-os.org + * + * Copyright (C) 2015 Marek Marczykowski-Górecki + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include "qrexec.h" +#include +#include "libqrexec-utils.h" +#include "qrexec-agent.h" + +void do_exec(const char *cmd) +{ + char buf[strlen(QUBES_RPC_MULTIPLEXER_PATH) + strlen(cmd) - strlen(RPC_REQUEST_COMMAND) + 1]; + /* replace magic RPC cmd with RPC multiplexer path */ + if (strncmp(cmd, RPC_REQUEST_COMMAND " ", strlen(RPC_REQUEST_COMMAND)+1)==0) { + strcpy(buf, QUBES_RPC_MULTIPLEXER_PATH); + strcpy(buf + strlen(QUBES_RPC_MULTIPLEXER_PATH), cmd + strlen(RPC_REQUEST_COMMAND)); + cmd = buf; + } + signal(SIGCHLD, SIG_DFL); + signal(SIGPIPE, SIG_DFL); + + execl("/bin/sh", "sh", "-c", cmd, NULL); + perror("execl"); + exit(1); +} + +void handle_vchan_error(const char *op) +{ + fprintf(stderr, "Error while vchan %s, exiting\n", op); + exit(1); +} + +void handle_single_command(int fd, struct qrexec_cmd_info *info) { + char cmdline[info->cmdline_len+1]; + + if (!read_all(fd, cmdline, info->cmdline_len)) + return; + cmdline[info->cmdline_len] = 0; + + handle_new_process(info->type, info->connect_domain, + info->connect_port, + cmdline, info->cmdline_len); +} + +int main(int argc, char **argv) { + int s, fd; + char *socket_path; + struct qrexec_cmd_info info; + struct sockaddr_un peer; + unsigned int addrlen; + + + if (argc == 2) { + socket_path = argv[1]; + } else if (argc == 1) { + /* this will be leaked, but we don't care as the process will then terminate */ + if (asprintf(&socket_path, QREXEC_FORK_SERVER_SOCKET, getenv("USER")) < 0) { + fprintf(stderr, "Memory allocation failed\n"); + exit(1); + } + } else { + fprintf(stderr, "Usage: %s [socket path]\n", argv[0]); + exit(1); + } + + s = get_server_socket(socket_path); + if (fcntl(s, F_SETFD, O_CLOEXEC) < 0) { + perror("fcntl"); + exit(1); + } + signal(SIGCHLD, SIG_IGN); + register_exec_func(do_exec); + + while ((fd = accept(s, (struct sockaddr *) &peer, &addrlen)) >= 0) { + if (read_all(fd, &info, sizeof(info))) { + handle_single_command(fd, &info); + } + close(fd); + addrlen = sizeof(peer); + } + close(s); + unlink(socket_path); + return 0; +} diff --git a/rpm_spec/core-vm.spec b/rpm_spec/core-vm.spec index 3584c25..a9484cf 100644 --- a/rpm_spec/core-vm.spec +++ b/rpm_spec/core-vm.spec @@ -400,6 +400,7 @@ rm -f %{name}-%{version} /usr/bin/qvm-mru-entry /usr/bin/xenstore-watch-qubes /usr/bin/qubes-desktop-run +/usr/bin/qrexec-fork-server %dir /usr/lib/qubes /usr/lib/qubes/vusb-ctl.py* /usr/lib/qubes/dispvm-prerun.sh