qrexec: implement buffered write to child stdin to prevent deadlock

Otherwise if the child process isn't reading its stdin at that time, it
would deadlock the whole qrexec connection (for example preventing
reading the data from the child, which may be a cause of that deadlock).

QubesOS/qubes-issues#1347
This commit is contained in:
Marek Marczykowski-Górecki 2015-10-30 14:16:53 +01:00
parent 4a7c2e2d42
commit 0c288aa355
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724

View File

@ -170,10 +170,14 @@ void do_exec(const char *prog)
static void do_exit(int code) static void do_exit(int code)
{ {
int status; int status;
// sever communication lines; wait for child, if any /* restore flags, as we may have not the only copy of this file descriptor
// so that qrexec-daemon can count (recursively) spawned processes correctly */
if (local_stdin_fd != -1)
set_block(local_stdin_fd);
close(local_stdin_fd); close(local_stdin_fd);
close(local_stdout_fd); close(local_stdout_fd);
// sever communication lines; wait for child, if any
// so that qrexec-daemon can count (recursively) spawned processes correctly
waitpid(-1, &status, 0); waitpid(-1, &status, 0);
exit(code); exit(code);
} }
@ -269,9 +273,15 @@ static void handle_input(libvchan_t *vchan)
{ {
char buf[MAX_DATA_CHUNK]; char buf[MAX_DATA_CHUNK];
int ret; int ret;
size_t max_len;
struct msg_header hdr; struct msg_header hdr;
ret = read(local_stdout_fd, buf, sizeof(buf)); max_len = libvchan_buffer_space(vchan)-sizeof(hdr);
if (max_len > sizeof(buf))
max_len = sizeof(buf);
if (max_len == 0)
return;
ret = read(local_stdout_fd, buf, max_len);
if (ret < 0) { if (ret < 0) {
perror("read"); perror("read");
do_exit(1); do_exit(1);
@ -328,12 +338,25 @@ void do_replace_esc(char *buf, int len) {
buf[i] = '_'; buf[i] = '_';
} }
static void handle_vchan_data(libvchan_t *vchan) static int handle_vchan_data(libvchan_t *vchan, struct buffer *stdin_buf)
{ {
int status; int status;
struct msg_header hdr; struct msg_header hdr;
char buf[MAX_DATA_CHUNK]; char buf[MAX_DATA_CHUNK];
if (local_stdin_fd != -1) {
switch(flush_client_data(local_stdin_fd, stdin_buf)) {
case WRITE_STDIN_ERROR:
perror("write stdin");
close(local_stdin_fd);
local_stdin_fd = -1;
break;
case WRITE_STDIN_BUFFERED:
return WRITE_STDIN_BUFFERED;
case WRITE_STDIN_OK:
break;
}
}
if (libvchan_recv(vchan, &hdr, sizeof hdr) < 0) { if (libvchan_recv(vchan, &hdr, sizeof hdr) < 0) {
perror("read vchan"); perror("read vchan");
do_exit(1); do_exit(1);
@ -356,16 +379,29 @@ static void handle_vchan_data(libvchan_t *vchan)
if (replace_esc_stdout) if (replace_esc_stdout)
do_replace_esc(buf, hdr.len); do_replace_esc(buf, hdr.len);
if (hdr.len == 0) { if (hdr.len == 0) {
/* restore flags, as we may have not the only copy of this file descriptor
*/
if (local_stdin_fd != -1)
set_block(local_stdin_fd);
close(local_stdin_fd); close(local_stdin_fd);
local_stdin_fd = -1; local_stdin_fd = -1;
} else if (!write_all(local_stdin_fd, buf, hdr.len)) { } else {
if (errno == EPIPE) { switch (write_stdin(local_stdin_fd, buf, hdr.len, stdin_buf)) {
// remote side have closed its stdin, handle data in oposite case WRITE_STDIN_BUFFERED:
// direction (if any) before exit return WRITE_STDIN_BUFFERED;
local_stdin_fd = -1; case WRITE_STDIN_ERROR:
} else { if (errno == EPIPE) {
perror("write local stdout"); // local process have closed its stdin, handle data in oposite
do_exit(1); // direction (if any) before exit
close(local_stdin_fd);
local_stdin_fd = -1;
} else {
perror("write local stdout");
do_exit(1);
}
break;
case WRITE_STDIN_OK:
break;
} }
} }
break; break;
@ -381,12 +417,17 @@ static void handle_vchan_data(libvchan_t *vchan)
else else
memcpy(&status, buf, sizeof(status)); memcpy(&status, buf, sizeof(status));
flush_client_data(local_stdin_fd, stdin_buf);
do_exit(status); do_exit(status);
break; break;
default: default:
fprintf(stderr, "unknown msg %d\n", hdr.type); fprintf(stderr, "unknown msg %d\n", hdr.type);
do_exit(1); do_exit(1);
} }
/* intentionally do not distinguish between _ERROR and _OK, because in case
* of write error, we simply eat the data - no way to report it to the
* other side */
return WRITE_STDIN_OK;
} }
static void check_child_status(libvchan_t *vchan) static void check_child_status(libvchan_t *vchan)
@ -409,36 +450,52 @@ static void check_child_status(libvchan_t *vchan)
static void select_loop(libvchan_t *vchan) static void select_loop(libvchan_t *vchan)
{ {
fd_set select_set; fd_set select_set;
fd_set wr_set;
int max_fd; int max_fd;
int ret; int ret;
int vchan_fd; int vchan_fd;
sigset_t selectmask; sigset_t selectmask;
struct timespec zero_timeout = { 0, 0 }; struct timespec zero_timeout = { 0, 0 };
struct timespec select_timeout = { 10, 0 }; struct timespec select_timeout = { 10, 0 };
struct buffer stdin_buf;
sigemptyset(&selectmask); sigemptyset(&selectmask);
sigaddset(&selectmask, SIGCHLD); sigaddset(&selectmask, SIGCHLD);
sigprocmask(SIG_BLOCK, &selectmask, NULL); sigprocmask(SIG_BLOCK, &selectmask, NULL);
sigemptyset(&selectmask); sigemptyset(&selectmask);
buffer_init(&stdin_buf);
/* remember to set back to blocking mode before closing the FD - this may
* be not the only copy and some processes may misbehave when get
* nonblocking FD for input/output
*/
set_nonblock(local_stdin_fd);
for (;;) { for (;;) {
vchan_fd = libvchan_fd_for_select(vchan); vchan_fd = libvchan_fd_for_select(vchan);
FD_ZERO(&select_set); FD_ZERO(&select_set);
FD_ZERO(&wr_set);
FD_SET(vchan_fd, &select_set); FD_SET(vchan_fd, &select_set);
max_fd = vchan_fd; max_fd = vchan_fd;
if (local_stdout_fd != -1 && libvchan_buffer_space(vchan)) { if (local_stdout_fd != -1 &&
(size_t)libvchan_buffer_space(vchan) > sizeof(struct msg_header)) {
FD_SET(local_stdout_fd, &select_set); FD_SET(local_stdout_fd, &select_set);
if (local_stdout_fd > max_fd) if (local_stdout_fd > max_fd)
max_fd = local_stdout_fd; max_fd = local_stdout_fd;
} }
if (child_exited && local_stdout_fd == -1) if (child_exited && local_stdout_fd == -1)
check_child_status(vchan); check_child_status(vchan);
if (libvchan_data_ready(vchan) > 0) { if (local_stdin_fd != -1 && buffer_len(&stdin_buf)) {
FD_SET(local_stdin_fd, &wr_set);
if (local_stdin_fd > max_fd)
max_fd = local_stdin_fd;
}
if ((local_stdin_fd == -1 || buffer_len(&stdin_buf) == 0) &&
libvchan_data_ready(vchan) > 0) {
/* check for other FDs, but exit immediately */ /* check for other FDs, but exit immediately */
ret = pselect(max_fd + 1, &select_set, NULL, NULL, ret = pselect(max_fd + 1, &select_set, &wr_set, NULL,
&zero_timeout, &selectmask); &zero_timeout, &selectmask);
} else } else
ret = pselect(max_fd + 1, &select_set, NULL, NULL, ret = pselect(max_fd + 1, &select_set, &wr_set, NULL,
&select_timeout, &selectmask); &select_timeout, &selectmask);
if (ret < 0) { if (ret < 0) {
if (errno == EINTR && local_pid > 0) { if (errno == EINTR && local_pid > 0) {
@ -456,8 +513,18 @@ static void select_loop(libvchan_t *vchan)
} }
if (FD_ISSET(vchan_fd, &select_set)) if (FD_ISSET(vchan_fd, &select_set))
libvchan_wait(vchan); libvchan_wait(vchan);
if (buffer_len(&stdin_buf) &&
local_stdin_fd != -1 &&
FD_ISSET(local_stdin_fd, &wr_set)) {
if (flush_client_data(local_stdin_fd, &stdin_buf) == WRITE_STDIN_ERROR) {
perror("write stdin");
close(local_stdin_fd);
local_stdin_fd = -1;
}
}
while (libvchan_data_ready(vchan)) while (libvchan_data_ready(vchan))
handle_vchan_data(vchan); if (handle_vchan_data(vchan, &stdin_buf) != WRITE_STDIN_OK)
break;
if (local_stdout_fd != -1 if (local_stdout_fd != -1
&& FD_ISSET(local_stdout_fd, &select_set)) && FD_ISSET(local_stdout_fd, &select_set))