Skip to content

Commit c2f197c

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 8120940 commit c2f197c

File tree

5 files changed

+108
-28
lines changed

5 files changed

+108
-28
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-stdin-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-stdin-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-stdin-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-stdin-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-stdin-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-stdin-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-stdin-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-stdin-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-stdin-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;

qrexec/tests/socket/agent.py

+47-23
Original file line numberDiff line numberDiff line change
@@ -1173,11 +1173,24 @@ class TestClientVm(unittest.TestCase):
11731173
target_domain = 43
11741174
target_port = 1024
11751175

1176+
def assertStdoutMessages(self, target, expected_stdout: bytes, ty=qrexec.MSG_DATA_STDOUT):
1177+
bytes_recvd, expected = 0, len(expected_stdout)
1178+
while bytes_recvd < expected:
1179+
msg_type, msg_body = target.recv_message()
1180+
self.assertEqual(msg_type, ty)
1181+
l = len(msg_body)
1182+
self.assertEqual(msg_body, expected_stdout[bytes_recvd:l])
1183+
bytes_recvd += l
1184+
self.assertEqual(bytes_recvd, expected)
1185+
11761186
def setUp(self):
11771187
self.tempdir = tempfile.mkdtemp()
11781188
self.addCleanup(shutil.rmtree, self.tempdir)
11791189

1180-
def start_client(self, args):
1190+
def start_client(self, args, stdio=subprocess.PIPE):
1191+
if stdio is not subprocess.PIPE:
1192+
self.assertIsInstance(stdio, socket.socket)
1193+
args.insert(0, "--use-stdin-socket")
11811194
env = os.environ.copy()
11821195
env["LD_LIBRARY_PATH"] = os.path.join(ROOT_PATH, "libqrexec")
11831196
env["VCHAN_DOMAIN"] = str(self.domain)
@@ -1193,8 +1206,8 @@ def start_client(self, args):
11931206
self.client = subprocess.Popen(
11941207
cmd,
11951208
env=env,
1196-
stdin=subprocess.PIPE,
1197-
stdout=subprocess.PIPE,
1209+
stdin=stdio,
1210+
stdout=stdio,
11981211
stderr=subprocess.PIPE,
11991212
pass_fds=(stderr_dup,),
12001213
)
@@ -1219,7 +1232,7 @@ def connect_target_client(self):
12191232
self.addCleanup(target_client.close)
12201233
return target_client
12211234

1222-
def run_service(self, *, local_program=None, options=None):
1235+
def run_service(self, *, local_program=None, options=None, stdio=subprocess.PIPE):
12231236
server = self.connect_server()
12241237

12251238
args = options or []
@@ -1228,7 +1241,7 @@ def run_service(self, *, local_program=None, options=None):
12281241
if local_program:
12291242
args += local_program
12301243

1231-
self.start_client(args)
1244+
self.start_client(args, stdio=stdio)
12321245
server.accept()
12331246

12341247
message_type, data = server.recv_message()
@@ -1257,6 +1270,30 @@ def test_run_client(self):
12571270
self.client.wait()
12581271
self.assertEqual(self.client.returncode, 42)
12591272

1273+
def test_run_client_eof(self):
1274+
remote, local = socket.socketpair()
1275+
target_client = self.run_service(stdio=remote)
1276+
remote.close()
1277+
initial_data = b"stdout data\n"
1278+
target_client.send_message(qrexec.MSG_DATA_STDOUT, initial_data)
1279+
# FIXME: data can be received in multiple messages
1280+
self.assertEqual(local.recv(len(initial_data)), initial_data)
1281+
initial_data = b"stdin data\n"
1282+
local.sendall(initial_data)
1283+
self.assertStdoutMessages(target_client, initial_data, qrexec.MSG_DATA_STDIN)
1284+
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
1285+
# Check that EOF got propagated
1286+
self.assertEqual(local.recv(1), b"")
1287+
# Close local
1288+
local.close()
1289+
# Check that EOF got propagated on this side too
1290+
self.assertEqual(target_client.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
1291+
target_client.send_message(
1292+
qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42)
1293+
)
1294+
self.client.wait()
1295+
self.assertEqual(self.client.returncode, 42)
1296+
12601297
def test_run_client_replace_chars(self):
12611298
target_client = self.run_service(options=["-t"])
12621299
target_client.send_message(
@@ -1301,10 +1338,7 @@ def test_run_client_with_local_proc(self):
13011338
target_client = self.run_service(local_program=["/bin/cat"])
13021339
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
13031340
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
1304-
self.assertEqual(
1305-
target_client.recv_message(),
1306-
(qrexec.MSG_DATA_STDIN, b"stdout data\n"),
1307-
)
1341+
self.assertStdoutMessages(target_client, b"stdout data\n", qrexec.MSG_DATA_STDIN)
13081342
self.assertEqual(
13091343
target_client.recv_message(), (qrexec.MSG_DATA_STDIN, b"")
13101344
)
@@ -1331,16 +1365,12 @@ def test_stdio_socket(self):
13311365
""",
13321366
]
13331367
)
1334-
self.assertEqual(
1335-
target.recv_message(), (qrexec.MSG_DATA_STDIN, b"hello world\n")
1336-
)
1368+
self.assertStdoutMessages(target, b"hello world\n", qrexec.MSG_DATA_STDIN)
13371369

13381370
target.send_message(qrexec.MSG_DATA_STDOUT, b"stdin\n")
13391371
target.send_message(qrexec.MSG_DATA_STDOUT, b"")
13401372

1341-
self.assertEqual(
1342-
target.recv_message(), (qrexec.MSG_DATA_STDIN, b"received: stdin\n")
1343-
)
1373+
self.assertStdoutMessages(target, b"received: stdin\n", qrexec.MSG_DATA_STDIN)
13441374
self.assertEqual(target.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
13451375

13461376
target.send_message(qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42))
@@ -1412,10 +1442,7 @@ def test_run_client_vchan_disconnect(self):
14121442
target_client = self.run_service()
14131443
self.client.stdin.write(b"stdin data\n")
14141444
self.client.stdin.flush()
1415-
self.assertEqual(
1416-
target_client.recv_message(),
1417-
(qrexec.MSG_DATA_STDIN, b"stdin data\n"),
1418-
)
1445+
self.assertStdoutMessages(target_client, b"stdin data\n", qrexec.MSG_DATA_STDIN)
14191446
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
14201447
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
14211448
self.assertEqual(self.client.stdout.read(), b"stdout data\n")
@@ -1427,10 +1454,7 @@ def test_run_client_vchan_disconnect(self):
14271454
def test_run_client_with_local_proc_disconnect(self):
14281455
target_client = self.run_service(local_program=["/bin/cat"])
14291456
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"stdout data\n")
1430-
self.assertEqual(
1431-
target_client.recv_message(),
1432-
(qrexec.MSG_DATA_STDIN, b"stdout data\n"),
1433-
)
1457+
self.assertStdoutMessages(target_client, b"stdout data\n", qrexec.MSG_DATA_STDIN)
14341458
target_client.close()
14351459
self.client.wait()
14361460
self.assertEqual(self.client.returncode, 255)

0 commit comments

Comments
 (0)