Skip to content

Commit 702ce87

Browse files
ambvzhangyoufu
andauthored
[3.11] gh-96522: Fix deadlock in pty.spawn (GH-96639) (#104655)
(cherry picked from commit 9c5aa89) Co-authored-by: Youfu Zhang <1315097+zhangyoufu@users.noreply.github.com>
1 parent aaeaf01 commit 702ce87

File tree

4 files changed

+56
-22
lines changed

4 files changed

+56
-22
lines changed

Lib/pty.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,6 @@ def fork():
121121
# Parent and child process.
122122
return pid, master_fd
123123

124-
def _writen(fd, data):
125-
"""Write all the data to a descriptor."""
126-
while data:
127-
n = os.write(fd, data)
128-
data = data[n:]
129-
130124
def _read(fd):
131125
"""Default read function."""
132126
return os.read(fd, 1024)
@@ -136,9 +130,42 @@ def _copy(master_fd, master_read=_read, stdin_read=_read):
136130
Copies
137131
pty master -> standard output (master_read)
138132
standard input -> pty master (stdin_read)"""
139-
fds = [master_fd, STDIN_FILENO]
140-
while fds:
141-
rfds, _wfds, _xfds = select(fds, [], [])
133+
if os.get_blocking(master_fd):
134+
# If we write more than tty/ndisc is willing to buffer, we may block
135+
# indefinitely. So we set master_fd to non-blocking temporarily during
136+
# the copy operation.
137+
os.set_blocking(master_fd, False)
138+
try:
139+
_copy(master_fd, master_read=master_read, stdin_read=stdin_read)
140+
finally:
141+
# restore blocking mode for backwards compatibility
142+
os.set_blocking(master_fd, True)
143+
return
144+
high_waterlevel = 4096
145+
stdin_avail = master_fd != STDIN_FILENO
146+
stdout_avail = master_fd != STDOUT_FILENO
147+
i_buf = b''
148+
o_buf = b''
149+
while 1:
150+
rfds = []
151+
wfds = []
152+
if stdin_avail and len(i_buf) < high_waterlevel:
153+
rfds.append(STDIN_FILENO)
154+
if stdout_avail and len(o_buf) < high_waterlevel:
155+
rfds.append(master_fd)
156+
if stdout_avail and len(o_buf) > 0:
157+
wfds.append(STDOUT_FILENO)
158+
if len(i_buf) > 0:
159+
wfds.append(master_fd)
160+
161+
rfds, wfds, _xfds = select(rfds, wfds, [])
162+
163+
if STDOUT_FILENO in wfds:
164+
try:
165+
n = os.write(STDOUT_FILENO, o_buf)
166+
o_buf = o_buf[n:]
167+
except OSError:
168+
stdout_avail = False
142169

143170
if master_fd in rfds:
144171
# Some OSes signal EOF by returning an empty byte string,
@@ -150,15 +177,18 @@ def _copy(master_fd, master_read=_read, stdin_read=_read):
150177
if not data: # Reached EOF.
151178
return # Assume the child process has exited and is
152179
# unreachable, so we clean up.
153-
else:
154-
os.write(STDOUT_FILENO, data)
180+
o_buf += data
181+
182+
if master_fd in wfds:
183+
n = os.write(master_fd, i_buf)
184+
i_buf = i_buf[n:]
155185

156-
if STDIN_FILENO in rfds:
186+
if stdin_avail and STDIN_FILENO in rfds:
157187
data = stdin_read(STDIN_FILENO)
158188
if not data:
159-
fds.remove(STDIN_FILENO)
189+
stdin_avail = False
160190
else:
161-
_writen(master_fd, data)
191+
i_buf += data
162192

163193
def spawn(argv, master_read=_read, stdin_read=_read):
164194
"""Create a spawned process."""

Lib/test/test_pty.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ def setUp(self):
312312
self.orig_pty_waitpid = pty.waitpid
313313
self.fds = [] # A list of file descriptors to close.
314314
self.files = []
315-
self.select_rfds_lengths = []
316-
self.select_rfds_results = []
315+
self.select_input = []
316+
self.select_output = []
317317
self.tcsetattr_mode_setting = None
318318

319319
def tearDown(self):
@@ -350,8 +350,8 @@ def _socketpair(self):
350350

351351
def _mock_select(self, rfds, wfds, xfds):
352352
# This will raise IndexError when no more expected calls exist.
353-
self.assertEqual(self.select_rfds_lengths.pop(0), len(rfds))
354-
return self.select_rfds_results.pop(0), [], []
353+
self.assertEqual((rfds, wfds, xfds), self.select_input.pop(0))
354+
return self.select_output.pop(0)
355355

356356
def _make_mock_fork(self, pid):
357357
def mock_fork():
@@ -374,11 +374,13 @@ def test__copy_to_each(self):
374374
os.write(masters[1], b'from master')
375375
os.write(write_to_stdin_fd, b'from stdin')
376376

377-
# Expect two select calls, the last one will cause IndexError
377+
# Expect three select calls, the last one will cause IndexError
378378
pty.select = self._mock_select
379-
self.select_rfds_lengths.append(2)
380-
self.select_rfds_results.append([mock_stdin_fd, masters[0]])
381-
self.select_rfds_lengths.append(2)
379+
self.select_input.append(([mock_stdin_fd, masters[0]], [], []))
380+
self.select_output.append(([mock_stdin_fd, masters[0]], [], []))
381+
self.select_input.append(([mock_stdin_fd, masters[0]], [mock_stdout_fd, masters[0]], []))
382+
self.select_output.append(([], [mock_stdout_fd, masters[0]], []))
383+
self.select_input.append(([mock_stdin_fd, masters[0]], [], []))
382384

383385
with self.assertRaises(IndexError):
384386
pty._copy(masters[0])

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,7 @@ Yuxiao Zeng
20342034
Uwe Zessin
20352035
Cheng Zhang
20362036
George Zhang
2037+
Youfu Zhang
20372038
Charlie Zhao
20382039
Kai Zhu
20392040
Tarek Ziadé
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix potential deadlock in pty.spawn()

0 commit comments

Comments
 (0)