/* * The Qubes OS Project, http://www.qubes-os.org * * Copyright (C) 2010 Rafal Wojtczuk * * 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 #include #include #include #include #include #include #include "qrexec.h" #include #include "libqrexec-utils.h" #include "qrexec-agent.h" struct _connection_info { 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; }; int max_process_fd = -1; /* */ struct _connection_info connection_info[MAX_FDS]; libvchan_t *ctrl_vchan; int trigger_fd; int passfd_socket; int meminfo_write_started = 0; 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) { fprintf(stderr, "Error while vchan %s, exiting\n", op); exit(1); } void init() { /* FIXME: This 0 is remote domain ID */ ctrl_vchan = libvchan_server_init(0, VCHAN_BASE_PORT, 4096, 4096); if (!ctrl_vchan) handle_vchan_error("server_init"); if (handle_handshake(ctrl_vchan) < 0) exit(1); umask(0); mkfifo(QREXEC_AGENT_TRIGGER_PATH, 0666); passfd_socket = get_server_socket(QREXEC_AGENT_FDPASS_PATH); umask(077); trigger_fd = open(QREXEC_AGENT_TRIGGER_PATH, O_RDONLY | O_NONBLOCK); register_exec_func(do_exec); /* wait for qrexec daemon */ while (!libvchan_is_open(ctrl_vchan)) libvchan_wait(ctrl_vchan); } void wake_meminfo_writer() { FILE *f; int pid; if (meminfo_write_started) /* wake meminfo-writer only once */ return; f = fopen(MEMINFO_WRITER_PIDFILE, "r"); if (f == NULL) { /* no meminfo-writer found, ignoring */ return; } if (fscanf(f, "%d", &pid) < 1) { fclose(f); /* no meminfo-writer found, ignoring */ return; } fclose(f); if (pid <= 1 || pid > 0xffff) { /* check within acceptable range */ return; } if (kill(pid, SIGUSR1) < 0) { /* Can't send signal */ return; } meminfo_write_started = 1; } 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; } } fprintf(stderr, "No free slot for child %d (connection to %d:%d)\n", pid, domain, port); } void handle_server_exec_request(struct msg_header *hdr) { struct exec_params params; char buf[hdr->len-sizeof(params)]; pid_t child_agent; assert(hdr->len >= sizeof(params)); if (libvchan_recv(ctrl_vchan, ¶ms, sizeof(params)) < 0) handle_vchan_error("read exec params"); 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, -1, params.connect_domain, params.connect_port); } void handle_service_refused(struct msg_header *hdr) { struct service_params params; int stdin_fd, stdout_fd, stderr_fd; if (hdr->len != sizeof(params)) { fprintf(stderr, "Invalid msg 0x%x length (%d)\n", MSG_SERVICE_REFUSED, hdr->len); exit(1); } if (libvchan_recv(ctrl_vchan, ¶ms, sizeof(params)) < 0) handle_vchan_error("read exec params"); sscanf(params.ident, "%d %d %d", &stdin_fd, &stdout_fd, &stderr_fd); /* TODO: send some signal? some response? */ close(stdin_fd); close(stdout_fd); close(stderr_fd); } void handle_server_cmd() { struct msg_header s_hdr; if (libvchan_recv(ctrl_vchan, &s_hdr, sizeof(s_hdr)) < 0) handle_vchan_error("read s_hdr"); // fprintf(stderr, "got %x %x %x\n", s_hdr.type, s_hdr.client_id, // s_hdr.len); switch (s_hdr.type) { case MSG_EXEC_CMDLINE: case MSG_JUST_EXEC: case MSG_SERVICE_CONNECT: wake_meminfo_writer(); handle_server_exec_request(&s_hdr); break; case MSG_SERVICE_REFUSED: handle_service_refused(&s_hdr); break; default: fprintf(stderr, "msg type from daemon is %d ?\n", s_hdr.type); exit(1); } } volatile int child_exited; void sigchld_handler(int x __attribute__((__unused__))) { child_exited = 1; signal(SIGCHLD, sigchld_handler); } int find_connection(int pid) { int i; for (i = 0; i < MAX_FDS; i++) if (connection_info[i].pid == pid) return i; 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; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { id = find_connection(pid); if (id < 0) continue; release_connection(id); } child_exited = 0; } int fill_fds_for_select(fd_set * rdset, fd_set * wrset) { int max = -1; int i; FD_ZERO(rdset); FD_ZERO(wrset); FD_SET(trigger_fd, rdset); if (trigger_fd > max) max = trigger_fd; 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; } void handle_new_passfd() { int fd = do_accept(passfd_socket); if (fd >= MAX_FDS) { fprintf(stderr, "too many clients ?\n"); exit(1); } // let client know what fd has been allocated if (write(fd, &fd, sizeof(fd)) != sizeof(fd)) { perror("write to client"); } } void handle_trigger_io() { struct msg_header hdr; struct trigger_service_params params; int ret; hdr.len = sizeof(params); ret = read(trigger_fd, ¶ms, sizeof(params)); if (ret == sizeof(params)) { hdr.type = MSG_TRIGGER_SERVICE; if (libvchan_send(ctrl_vchan, &hdr, sizeof(hdr)) < 0) handle_vchan_error("write hdr"); if (libvchan_send(ctrl_vchan, ¶ms, sizeof(params)) < 0) handle_vchan_error("write params"); } // trigger_fd is nonblock - so no need to reopen // not really, need to reopen at EOF if (ret <= 0) { close(trigger_fd); trigger_fd = open(QREXEC_AGENT_TRIGGER_PATH, O_RDONLY | O_NONBLOCK); } } 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; int max; sigset_t chld_set; init(); signal(SIGCHLD, sigchld_handler); signal(SIGPIPE, SIG_IGN); sigemptyset(&chld_set); sigaddset(&chld_set, SIGCHLD); for (;;) { sigprocmask(SIG_BLOCK, &chld_set, NULL); if (child_exited) reap_children(); max = fill_fds_for_select(&rdset, &wrset); if (libvchan_buffer_space(ctrl_vchan) <= (int)sizeof(struct msg_header)) FD_ZERO(&rdset); wait_for_vchan_or_argfd(ctrl_vchan, max, &rdset, &wrset); sigprocmask(SIG_UNBLOCK, &chld_set, NULL); if (FD_ISSET(passfd_socket, &rdset)) handle_new_passfd(); while (libvchan_data_ready(ctrl_vchan)) handle_server_cmd(); if (FD_ISSET(trigger_fd, &rdset)) handle_trigger_io(); handle_terminated_fork_client(&rdset); } }