Skip to content

Commit 66a82e8

Browse files
committed
Ensure that EOF is propagated to stdout
It is critical that EOF from the service gets propagated to the stdout of qrexec-client-vm or qrexec-client. Otherwise, the caller will not recognize that EOF has happened and may wait forever for data that will never arrive. In QubesOS/qubes-issues#9169, this caused curl to hang when reading a plaintext HTTP/1.0 response from tinyproxy, which relies on EOF to delimit the response. The bug will trigger iff all of the following conditions are met: 1. The remote side must close the writing side of the connection, either by closing or shutting down a socket. 2. If the remote service is executable, it must _not_ exit. 3. qrexec-client-vm or qrexec-client must _not_ get EOF on stdin. 4. Either the remote side does not close its stdin, or the local side does not continue to write data. 5. The same file description is used for both stdin and stdout of the local qrexec-client or qrexec-client-vm process. 6. qrexec-client or qrexec-client-vm connect the stdin and stdout of the remote process to their own stdin and stdout, not the stdin and stdout of a locally spawned command. To fix this bug, add flags to qrexec-client and qrexec-client-vm that cause the socket passed as file descriptor 0 to be used for both stdin and stdout. Fixes: QubesOS/qubes-issues#9169
1 parent 47809d6 commit 66a82e8

File tree

4 files changed

+61
-5
lines changed

4 files changed

+61
-5
lines changed

agent/qrexec-client-vm.c

+26-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*
2020
*/
21+
#include <assert.h>
2122
#include <sys/socket.h>
2223
#include <sys/wait.h>
2324
#include <sys/un.h>
@@ -94,6 +95,7 @@ static void convert_target_name_keyword(char *target)
9495
enum {
9596
opt_no_filter_stdout = 't'+128,
9697
opt_no_filter_stderr = 'T'+128,
98+
opt_use_stdin_socket = 'u'+128,
9799
};
98100

99101
static struct option longopts[] = {
@@ -104,6 +106,7 @@ static struct option longopts[] = {
104106
{ "no-filter-escape-chars-stderr", no_argument, 0, opt_no_filter_stderr},
105107
{ "agent-socket", required_argument, 0, 'a'},
106108
{ "prefix-data", required_argument, 0, 'p' },
109+
{ "use-fd0-socket", no_argument, 0, opt_use_stdin_socket },
107110
{ "help", no_argument, 0, 'h' },
108111
{ NULL, 0, 0, 0},
109112
};
@@ -141,6 +144,7 @@ int main(int argc, char **argv)
141144
int inpipe[2], outpipe[2];
142145
int buffer_size = 0;
143146
int opt;
147+
int stdout_fd = 1;
144148
const char *agent_trigger_path = QREXEC_AGENT_TRIGGER_PATH, *prefix_data = NULL;
145149

146150
setup_logging("qrexec-client-vm");
@@ -198,6 +202,23 @@ int main(int argc, char **argv)
198202
exit(1);
199203
}
200204
break;
205+
case opt_use_stdin_socket:
206+
{
207+
int type;
208+
if (stdout_fd == 0)
209+
errx(2, "--use-fd0-socket passed twice");
210+
socklen_t len = sizeof(type);
211+
if (getsockopt(0, SOL_SOCKET, SO_TYPE, &type, &len)) {
212+
if (errno == ENOTSOCK)
213+
errx(2, "--use-fd0-socket passed, but stdin not a socket");
214+
err(2, "getsockopt(0, SOL_SOCKET, SO_TYPE)");
215+
}
216+
assert(len == sizeof(type));
217+
if (type != SOCK_STREAM)
218+
errx(2, "stdin was a socket of type %d, not SOCK_STREAM (%d)", type, SOCK_STREAM);
219+
stdout_fd = 0;
220+
}
221+
break;
201222
case '?':
202223
usage(argv[0], 2);
203224
}
@@ -209,6 +230,10 @@ int main(int argc, char **argv)
209230
if (argc - optind > 2) {
210231
start_local_process = 1;
211232
}
233+
if (start_local_process && stdout_fd != 1) {
234+
fprintf(stderr, "cannot spawn a local process with --use-fd0-socket\n");
235+
usage(argv[0], 2);
236+
}
212237

213238
if (!start_local_process) {
214239
if (replace_chars_stdout == -1 && isatty(1))
@@ -303,7 +328,7 @@ int main(int argc, char **argv)
303328
} else {
304329
ret = handle_data_client(MSG_SERVICE_CONNECT,
305330
exec_params.connect_domain, exec_params.connect_port,
306-
1, 0, -1, buffer_size, 0, prefix_data);
331+
stdout_fd, 0, -1, buffer_size, 0, prefix_data);
307332
}
308333

309334
close(trigger_fd);

daemon/qrexec-client.c

+31-3
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,16 @@ static _Noreturn void do_exec(const char *prog, const char *username __attribute
7272
PERROR("exec bash");
7373
exit(1);
7474
}
75+
enum {
76+
opt_socket_dir = 'd'+128,
77+
opt_use_stdin_socket = 'u'+128,
78+
};
7579

7680
static struct option longopts[] = {
7781
{ "help", no_argument, 0, 'h' },
78-
{ "socket-dir", required_argument, 0, 'd'+128 },
82+
{ "socket-dir", required_argument, 0, opt_socket_dir },
7983
{ "no-exit-code", no_argument, 0, 'E' },
84+
{ "use-fd0-socket", no_argument, 0, opt_use_stdin_socket },
8085
{ NULL, 0, 0, 0 },
8186
};
8287

@@ -96,7 +101,8 @@ _Noreturn static void usage(const char *const name)
96101
" -c - connect to existing process (response to trigger service call)\n"
97102
" -w timeout - override default connection timeout of 5s (set 0 for no timeout)\n"
98103
" -k - kill the domain right before exiting\n"
99-
" --socket-dir=PATH - directory for qrexec socket, default: %s\n",
104+
" --socket-dir=PATH - directory for qrexec socket, default: %s\n"
105+
" --use-fd0-socket - use fd 0 (which must be socket) for both stdin and stdout\n",
100106
name ? name : "qrexec-client", QREXEC_DAEMON_SOCKET_DIR);
101107
exit(1);
102108
}
@@ -217,9 +223,26 @@ int main(int argc, char **argv)
217223
case 'W':
218224
wait_connection_end = 1;
219225
break;
220-
case 'd' + 128:
226+
case opt_socket_dir:
221227
socket_dir = strdup(optarg);
222228
break;
229+
case opt_use_stdin_socket:
230+
{
231+
int type;
232+
if (local_stdin_fd != 1)
233+
errx(2, "--use-fd0-socket passed twice");
234+
socklen_t len = sizeof(type);
235+
if (getsockopt(0, SOL_SOCKET, SO_TYPE, &type, &len)) {
236+
if (errno == ENOTSOCK)
237+
errx(2, "--use-fd0-socket passed, but stdin not a socket");
238+
err(2, "getsockopt(0, SOL_SOCKET, SO_TYPE)");
239+
}
240+
assert(len == sizeof(type) && "wrong socket option length?");
241+
if (type != SOCK_STREAM)
242+
errx(2, "stdin was a socket of type %d, not SOCK_STREAM (%d)", type, SOCK_STREAM);
243+
local_stdin_fd = 0;
244+
}
245+
break;
223246
case 'k':
224247
kill = true;
225248
break;
@@ -241,6 +264,11 @@ int main(int argc, char **argv)
241264
usage(argv[0]);
242265
}
243266

267+
if ((local_cmdline != NULL) && (local_stdin_fd != 1)) {
268+
fprintf(stderr, "ERROR: at most one of -l and --use-fd0-socket can be specified\n");
269+
usage(argv[0]);
270+
}
271+
244272
if (strcmp(domname, "dom0") == 0 || strcmp(domname, "@adminvm") == 0) {
245273
if (request_id == NULL) {
246274
fprintf(stderr, "ERROR: when target domain is 'dom0', -c must be specified\n");

daemon/qrexec-daemon-common.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ bool send_service_connect(int s, const char *conn_ident,
155155

156156
#define QREXEC_DATA_MIN_VERSION QREXEC_PROTOCOL_V2
157157

158-
static int local_stdin_fd = 1, local_stdout_fd = 0;
158+
static int local_stdout_fd = 0;
159+
int local_stdin_fd = 1;
159160
static pid_t local_pid = 0;
160161

161162
static volatile sig_atomic_t sigchld = 0;

daemon/qrexec-daemon-common.h

+2
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ __attribute__((warn_unused_result))
3838
int handle_agent_handshake(libvchan_t *vchan, bool remote_send_first);
3939
__attribute__((warn_unused_result))
4040
int prepare_local_fds(struct qrexec_parsed_command *command, struct buffer *stdin_buffer);
41+
/* FD for stdout of remote process */
42+
extern int local_stdin_fd;

0 commit comments

Comments
 (0)