Skip to content

Commit 7d01e17

Browse files
committed
Merge remote-tracking branch 'origin/pr/157'
* origin/pr/157: Fix SIGUSR1 after stdin_fd closed
2 parents 64d6e06 + 9e72d82 commit 7d01e17

File tree

2 files changed

+127
-38
lines changed

2 files changed

+127
-38
lines changed

libqrexec/process_io.c

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

6673
static void close_stderr(int fd) {
@@ -105,6 +112,10 @@ int qrexec_process_io(const struct process_io_request *req,
105112

106113
pid_t local_status = -1;
107114
pid_t remote_status = -1;
115+
/* Saved version of stdin_fd. If we get SIGHUP,
116+
* this replaces stdout_fd. Set to -1 only if stdin_fd
117+
* is closed, not merely shut down. */
118+
int saved_stdin_fd = stdin_fd;
108119
int stdout_msg_type = is_service ? MSG_DATA_STDOUT : MSG_DATA_STDIN;
109120
int ret;
110121
struct pollfd fds[FD_NUM];
@@ -125,8 +136,17 @@ int qrexec_process_io(const struct process_io_request *req,
125136
sigprocmask(SIG_BLOCK, &pollmask, NULL);
126137
sigemptyset(&pollmask);
127138

139+
/* Invariants:
140+
*
141+
* - if stdin_fd == stdout_fd, then use_stdio_socket is true.
142+
* - if use_stdio_socket is true, then either:
143+
* - stdin_fd == stdout_fd right now.
144+
* - stdin_fd == stdout_fd at some point in the past,
145+
* and one of them is currently -1.
146+
*/
147+
bool use_stdio_socket = stdin_fd == stdout_fd;
128148
set_nonblock(stdin_fd);
129-
if (stdout_fd != stdin_fd)
149+
if (!use_stdio_socket)
130150
set_nonblock(stdout_fd);
131151
if (is_service && local_pid == 0) {
132152
assert(stdin_fd == stdout_fd);
@@ -142,27 +162,30 @@ int qrexec_process_io(const struct process_io_request *req,
142162
}
143163

144164
/* Convenience macros that eliminate a ton of error-prone boilerplate */
145-
#define close_stdin() do { \
146-
if (exit_on_stdin_eof) { \
147-
/* Set stdin_fd and stdout_fd to -1. \
148-
* No need to close them as the process \
149-
* will soon exit. */ \
150-
stdin_fd = stdout_fd = -1; \
151-
} else { \
152-
close_stdio(stdin_fd, stdout_fd, SHUT_WR); \
153-
stdin_fd = -1; \
154-
} \
165+
#define close_stdin() do { \
166+
if (exit_on_stdin_eof) { \
167+
/* Set stdin_fd and stdout_fd to -1. \
168+
* No need to close them as the process \
169+
* will soon exit. */ \
170+
stdin_fd = stdout_fd = -1; \
171+
} else { \
172+
/* if stdin_fd was actually closed, set saved_stdin_fd \
173+
* to -1 to avoid use-after-close */ \
174+
if (close_stdio(stdin_fd, SHUT_WR, use_stdio_socket)) \
175+
saved_stdin_fd = -1; \
176+
stdin_fd = -1; \
177+
} \
155178
} while (0)
156-
#define close_stdout() do { \
157-
if (exit_on_stdout_eof) { \
158-
/* Set stdin_fd and stdout_fd to -1. \
159-
* No need to close them as the process \
160-
* will soon exit. */ \
161-
stdin_fd = stdout_fd = -1; \
162-
} else { \
163-
close_stdio(stdout_fd, stdin_fd, SHUT_RD); \
164-
stdout_fd = -1; \
165-
} \
179+
#define close_stdout() do { \
180+
if (exit_on_stdout_eof) { \
181+
/* Set stdin_fd and stdout_fd to -1. \
182+
* No need to close them as the process \
183+
* will soon exit. */ \
184+
stdin_fd = stdout_fd = -1; \
185+
} else { \
186+
close_stdio(stdout_fd, SHUT_RD, use_stdio_socket); \
187+
stdout_fd = -1; \
188+
} \
166189
} while (0)
167190
#pragma GCC poison close_stdio
168191

@@ -218,9 +241,10 @@ int qrexec_process_io(const struct process_io_request *req,
218241
}
219242

220243
/* child signaled desire to use single socket for both stdin and stdout */
221-
if (sigusr1 && *sigusr1 && stdin_fd != stdout_fd) {
244+
if (sigusr1 && *sigusr1 && !use_stdio_socket) {
222245
close_stdout();
223-
stdout_fd = stdin_fd;
246+
stdout_fd = saved_stdin_fd;
247+
use_stdio_socket = true;
224248
*sigusr1 = 0;
225249
}
226250

qrexec/tests/socket/agent.py

+65
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,36 @@ def test_run_client_eof(self):
14391439
self.client.wait()
14401440
self.assertEqual(self.client.returncode, 42)
14411441

1442+
def test_run_client_bidirectional_shutdown(self):
1443+
try:
1444+
remote, local = socket.socketpair()
1445+
target_client = self.run_service(stdio=remote)
1446+
initial_data = b"stdout data\n"
1447+
target_client.send_message(qrexec.MSG_DATA_STDOUT, initial_data)
1448+
# FIXME: data can be received in multiple messages
1449+
self.assertEqual(local.recv(len(initial_data)), initial_data)
1450+
initial_data = b"stdin data\n"
1451+
local.sendall(initial_data)
1452+
local.shutdown(socket.SHUT_WR)
1453+
self.assertStdoutMessages(target_client, initial_data, qrexec.MSG_DATA_STDIN)
1454+
# Check that EOF received
1455+
self.assertEqual(target_client.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
1456+
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")
1457+
# Check that EOF got propagated on this side too, even though
1458+
# we still have a reference to the socket. This indicates that
1459+
# qrexec-client-vm shut down the socket for writing.
1460+
self.assertEqual(local.recv(1), b"")
1461+
with self.assertRaises(BrokenPipeError):
1462+
remote.send(b"a")
1463+
target_client.send_message(
1464+
qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42)
1465+
)
1466+
self.client.wait()
1467+
self.assertEqual(self.client.returncode, 42)
1468+
finally:
1469+
remote.close()
1470+
local.close()
1471+
14421472
def test_run_client_replace_chars(self):
14431473
target_client = self.run_service(options=["-t"])
14441474
target_client.send_message(
@@ -1522,6 +1552,41 @@ def test_stdio_socket(self):
15221552
self.client.wait()
15231553
self.assertEqual(self.client.returncode, 42)
15241554

1555+
def test_stdio_socket_closed_stdin(self):
1556+
fifo = os.path.join(self.tempdir, "new_file")
1557+
os.mkfifo(fifo, mode=0o600)
1558+
target = self.run_service(
1559+
local_program=[
1560+
"/bin/sh",
1561+
"-euc",
1562+
"""\
1563+
echo hello world
1564+
# Wait for stdin to be closed for writing
1565+
read -r x
1566+
cat
1567+
# Request that stdin be used to send stdout data
1568+
kill -USR1 "$QREXEC_AGENT_PID"
1569+
echo "received: $x" >&0
1570+
# Avoid a race condition by ensuring the program does not exit too soon
1571+
read < "$1"
1572+
""",
1573+
"sh",
1574+
fifo,
1575+
]
1576+
)
1577+
self.assertStdoutMessages(target, b"hello world\n", qrexec.MSG_DATA_STDIN)
1578+
1579+
target.send_message(qrexec.MSG_DATA_STDOUT, b"stdin\n")
1580+
target.send_message(qrexec.MSG_DATA_STDOUT, b"")
1581+
1582+
self.assertStdoutMessages(target, b"received: stdin\n", qrexec.MSG_DATA_STDIN)
1583+
with open(fifo, "wb"): pass
1584+
self.assertEqual(target.recv_message(), (qrexec.MSG_DATA_STDIN, b""))
1585+
1586+
target.send_message(qrexec.MSG_DATA_EXIT_CODE, struct.pack("<L", 42))
1587+
self.client.wait()
1588+
self.assertEqual(self.client.returncode, 42)
1589+
15251590
def test_run_client_with_local_proc_service_failed(self):
15261591
target_client = self.run_service(local_program=["/bin/cat"])
15271592
target_client.send_message(qrexec.MSG_DATA_STDOUT, b"")

0 commit comments

Comments
 (0)