Skip to content

Commit 894bcb1

Browse files
committed
Fix SIGUSR1 after stdin_fd closed
Just shut it down for writing, and save it so that it can later be used for reading. This also ensures deterministic behavior when --use-stdin-socket is used. Previously, the socket would be shut down for reading or writing but not both, depending on whether reading or writing stopped first. This could be quite confusing for callers. Instead, always shut down the socket for both reading and writing, guaranteeing deterministic behavior. Fixes: a3bbcb5 ("Prefer close() to shutdown()") Fixes: QubesOS/qubes-issues#9183
1 parent 89c450b commit 894bcb1

File tree

2 files changed

+101
-24
lines changed

2 files changed

+101
-24
lines changed

libqrexec/process_io.c

+40-24
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,31 @@ static _Noreturn void handle_vchan_error(const char *op)
4242
/*
4343
* Closing the file descriptors:
4444
*
45-
* - If this is a single socket used for both stdin and stdout, call shutdown()
46-
* to indicate that no more data will be sent or received.
47-
* - Otherwise, restore the blocking status of the FD.
45+
* - If this file descriptor was inherited _and_ is not used for both stdin
46+
* and stdout, restore the blocking status of the FD and then close it.
47+
* - Otherwise, call shutdown() to indicate that no more data will be sent or
48+
* received in the given direction.
4849
*/
49-
static void close_stdio(int fd, int other_fd, int direction) {
50+
static bool close_stdio(int fd, int direction, bool single_socket) {
5051
if (fd == -1)
51-
return;
52-
if (fd == other_fd) {
53-
/* Do not close the FD, as it is still in use. */
54-
/* ENOTCONN can happen with TCP and is harmless */
55-
if (shutdown(fd, direction) == -1 && errno != ENOTSOCK && errno != ENOTCONN)
56-
PERROR("shutdown");
57-
return;
58-
}
59-
60-
if (fd < 3)
52+
return false;
53+
if (single_socket || fd > 2) {
54+
if (shutdown(fd, direction) == -1) {
55+
if (errno == ENOTSOCK && !single_socket) {
56+
close(fd);
57+
return true;
58+
} else if (errno != ENOTCONN) {
59+
// ENOTCONN can happen with TCP and is harmless
60+
PERROR("shutdown");
61+
}
62+
}
63+
return false;
64+
} else {
65+
/* Close the file descriptor */
6166
set_block(fd);
62-
close(fd);
67+
close(fd);
68+
return true;
69+
}
6370
}
6471

6572
static void close_stderr(int fd) {
@@ -96,6 +103,10 @@ int process_io(const struct process_io_request *req) {
96103

97104
pid_t local_status = -1;
98105
pid_t remote_status = -1;
106+
/* Saved version of stdin_fd. If we get SIGHUP,
107+
* this replaces stdout_fd. Set to -1 only if stdin_fd
108+
* is closed, not merely shut down. */
109+
int saved_stdin_fd = stdin_fd;
99110
int stdout_msg_type = is_service ? MSG_DATA_STDOUT : MSG_DATA_STDIN;
100111
int ret;
101112
struct pollfd fds[FD_NUM];
@@ -116,22 +127,26 @@ int process_io(const struct process_io_request *req) {
116127
sigprocmask(SIG_BLOCK, &pollmask, NULL);
117128
sigemptyset(&pollmask);
118129

130+
bool use_stdio_socket = stdin_fd == stdout_fd;
119131
set_nonblock(stdin_fd);
120-
if (stdout_fd != stdin_fd)
132+
if (!use_stdio_socket)
121133
set_nonblock(stdout_fd);
122134
if (stderr_fd >= 0) {
123135
assert(is_service); // if this is a client, stderr_fd is *always* -1
124136
set_nonblock(stderr_fd);
125137
}
126138

127139
/* Convenience macros that eliminate a ton of error-prone boilerplate */
128-
#define close_stdin() do { \
129-
close_stdio(stdin_fd, stdout_fd, SHUT_WR); \
130-
stdin_fd = -1; \
140+
#define close_stdin() do { \
141+
/* if stdin_fd was actually closed, set saved_stdin_fd \
142+
* to -1 to avoid use-after-close */ \
143+
if (close_stdio(stdin_fd, SHUT_WR, use_stdio_socket)) \
144+
saved_stdin_fd = -1; \
145+
stdin_fd = -1; \
131146
} while (0)
132-
#define close_stdout() do { \
133-
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
134-
stdout_fd = -1; \
147+
#define close_stdout() do { \
148+
close_stdio(stdout_fd, SHUT_RD, use_stdio_socket); \
149+
stdout_fd = -1; \
135150
} while (0)
136151
#pragma GCC poison close_stdio
137152

@@ -187,9 +202,10 @@ int process_io(const struct process_io_request *req) {
187202
}
188203

189204
/* child signaled desire to use single socket for both stdin and stdout */
190-
if (sigusr1 && *sigusr1 && stdin_fd != stdout_fd) {
205+
if (sigusr1 && *sigusr1 && !use_stdio_socket) {
191206
close_stdout();
192-
stdout_fd = stdin_fd;
207+
stdout_fd = saved_stdin_fd;
208+
use_stdio_socket = true;
193209
*sigusr1 = 0;
194210
}
195211

qrexec/tests/socket/agent.py

+61
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,32 @@ def test_run_client_eof(self):
13411341
self.client.wait()
13421342
self.assertEqual(self.client.returncode, 42)
13431343

1344+
def test_run_client_bidirectional_shutdown(self):
1345+
try:
1346+
remote, local = socket.socketpair()
1347+
target_client = self.run_service(stdio=remote)
1348+
initial_data = b"stdout data\n"
1349+
target_client.send_message(qrexec.MSG_DATA_STDOUT, initial_data)
1350+
# FIXME: data can be received in multiple messages
1351+
self.assertEqual(local.recv(len(initial_data)), initial_data)
1352+
initial_data = b"stdin data\n"
1353+
local.sendall(initial_data)
1354+
local.shutdown(socket.SHUT_WR)
1355+
self.assertStdoutMessages(target_client, initial_data, qrexec.MSG_DATA_STDIN)
1356+
# Check that EOF received
1357+
self.assertEqual(target_client.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
1358+
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
1359+
# Check that EOF got propagated on this side too
1360+
self.assertEqual(local.recv(1), b"")
1361+
target_client.send_message(
1362+
qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42)
1363+
)
1364+
self.client.wait()
1365+
self.assertEqual(self.client.returncode, 42)
1366+
finally:
1367+
remote.close()
1368+
local.close()
1369+
13441370
def test_run_client_replace_chars(self):
13451371
target_client = self.run_service(options=["-t"])
13461372
target_client.send_message(
@@ -1424,6 +1450,41 @@ def test_stdio_socket(self):
14241450
self.client.wait()
14251451
self.assertEqual(self.client.returncode, 42)
14261452

1453+
def test_stdio_socket_closed_stdin(self):
1454+
fifo = os.path.join(self.tempdir, "new_file")
1455+
os.mkfifo(fifo, mode=0o600)
1456+
target = self.run_service(
1457+
local_program=[
1458+
"/bin/sh",
1459+
"-euc",
1460+
"""\
1461+
echo hello world
1462+
# Wait for stdin to be closed for writing
1463+
read -r x
1464+
cat
1465+
# Request that stdin be used to send stdout data
1466+
kill -USR1 "$QREXEC_AGENT_PID"
1467+
echo "received: $x" >&0
1468+
# Avoid a race condition by ensuring the program does not exit too soon
1469+
read < "$1"
1470+
""",
1471+
"sh",
1472+
fifo,
1473+
]
1474+
)
1475+
self.assertStdoutMessages(target, b"hello world\n", qrexec.MSG_DATA_STDIN)
1476+
1477+
target.send_message(qrexec.MSG_DATA_STDOUT, b"stdin\n")
1478+
target.send_message(qrexec.MSG_DATA_STDOUT, b"")
1479+
1480+
self.assertStdoutMessages(target, b"received: stdin\n", qrexec.MSG_DATA_STDIN)
1481+
with open(fifo, "wb"): pass
1482+
self.assertEqual(target.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
1483+
1484+
target.send_message(qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42))
1485+
self.client.wait()
1486+
self.assertEqual(self.client.returncode, 42)
1487+
14271488
def test_run_client_with_local_proc_service_failed(self):
14281489
target_client = self.run_service(local_program=["/bin/cat"])
14291490
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")

0 commit comments

Comments
 (0)