700c240d37
This process should be started from user session (most likely qubes-session). New processes (of that user) will be created as children of that session making logind and such crap happy. This should also solve problems with EOF transmission (no additional "su" process) and prevent loading all the environment multiple times.
469 lines
12 KiB
C
469 lines
12 KiB
C
/*
|
|
* The Qubes OS Project, http://www.qubes-os.org
|
|
*
|
|
* Copyright (C) 2010 Rafal Wojtczuk <rafal@invisiblethingslab.com>
|
|
*
|
|
* 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 <sys/select.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <sys/wait.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#include <sys/stat.h>
|
|
#include <assert.h>
|
|
#include "qrexec.h"
|
|
#include <libvchan.h>
|
|
#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);
|
|
}
|
|
}
|