Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cb46f0e

Browse files
committed
Issue #23309: Avoid a deadlock at shutdown if a daemon thread is aborted
while it is holding a lock to a buffered I/O object, and the main thread tries to use the same I/O object (typically stdout or stderr). A fatal error is emitted instead.
2 parents 9c680b0 + 25f85d4 commit cb46f0e

5 files changed

Lines changed: 94 additions & 14 deletions

File tree

Lib/test/script_helper.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Common utility functions used by various script execution tests
22
# e.g. test_cmd_line, test_cmd_line_script and test_runpy
33

4+
import collections
45
import importlib
56
import sys
67
import os
@@ -50,8 +51,12 @@ def interpreter_requires_environment():
5051
return __cached_interp_requires_environment
5152

5253

54+
_PythonRunResult = collections.namedtuple("_PythonRunResult",
55+
("rc", "out", "err"))
56+
57+
5358
# Executing the interpreter in a subprocess
54-
def _assert_python(expected_success, *args, **env_vars):
59+
def run_python_until_end(*args, **env_vars):
5560
env_required = interpreter_requires_environment()
5661
if '__isolated' in env_vars:
5762
isolated = env_vars.pop('__isolated')
@@ -85,9 +90,14 @@ def _assert_python(expected_success, *args, **env_vars):
8590
p.stderr.close()
8691
rc = p.returncode
8792
err = strip_python_stderr(err)
88-
if (rc and expected_success) or (not rc and not expected_success):
93+
return _PythonRunResult(rc, out, err), cmd_line
94+
95+
def _assert_python(expected_success, *args, **env_vars):
96+
res, cmd_line = run_python_until_end(*args, **env_vars)
97+
if (res.rc and expected_success) or (not res.rc and not expected_success):
8998
# Limit to 80 lines to ASCII characters
9099
maxlen = 80 * 100
100+
out, err = res.out, res.err
91101
if len(out) > maxlen:
92102
out = b'(... truncated stdout ...)' + out[-maxlen:]
93103
if len(err) > maxlen:
@@ -106,10 +116,10 @@ def _assert_python(expected_success, *args, **env_vars):
106116
"---\n"
107117
"%s\n"
108118
"---"
109-
% (rc, cmd_line,
119+
% (res.rc, cmd_line,
110120
out,
111121
err))
112-
return rc, out, err
122+
return res
113123

114124
def assert_python_ok(*args, **env_vars):
115125
"""

Lib/test/test_io.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from collections import deque, UserList
3636
from itertools import cycle, count
3737
from test import support
38-
from test.script_helper import assert_python_ok
38+
from test.script_helper import assert_python_ok, run_python_until_end
3939

4040
import codecs
4141
import io # C implementation of io
@@ -3437,6 +3437,49 @@ def read(self, n=-1):
34373437
b = bytearray(2)
34383438
self.assertRaises(ValueError, bufio.readinto, b)
34393439

3440+
@unittest.skipUnless(threading, 'Threading required for this test.')
3441+
def check_daemon_threads_shutdown_deadlock(self, stream_name):
3442+
# Issue #23309: deadlocks at shutdown should be avoided when a
3443+
# daemon thread and the main thread both write to a file.
3444+
code = """if 1:
3445+
import sys
3446+
import time
3447+
import threading
3448+
3449+
file = sys.{stream_name}
3450+
3451+
def run():
3452+
while True:
3453+
file.write('.')
3454+
file.flush()
3455+
3456+
thread = threading.Thread(target=run)
3457+
thread.daemon = True
3458+
thread.start()
3459+
3460+
time.sleep(0.5)
3461+
file.write('!')
3462+
file.flush()
3463+
""".format_map(locals())
3464+
res, _ = run_python_until_end("-c", code)
3465+
err = res.err.decode()
3466+
if res.rc != 0:
3467+
# Failure: should be a fatal error
3468+
self.assertIn("Fatal Python error: could not acquire lock "
3469+
"for <_io.BufferedWriter name='<{stream_name}>'> "
3470+
"at interpreter shutdown, possibly due to "
3471+
"daemon threads".format_map(locals()),
3472+
err)
3473+
else:
3474+
self.assertFalse(err.strip('.!'))
3475+
3476+
def test_daemon_threads_shutdown_stdout_deadlock(self):
3477+
self.check_daemon_threads_shutdown_deadlock('stdout')
3478+
3479+
def test_daemon_threads_shutdown_stderr_deadlock(self):
3480+
self.check_daemon_threads_shutdown_deadlock('stderr')
3481+
3482+
34403483
class PyMiscIOTest(MiscIOTest):
34413484
io = pyio
34423485

Lib/test/test_script_helper.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,27 @@
88

99

1010
class TestScriptHelper(unittest.TestCase):
11-
def test_assert_python_expect_success(self):
12-
t = script_helper._assert_python(True, '-c', 'import sys; sys.exit(0)')
11+
12+
def test_assert_python_ok(self):
13+
t = script_helper.assert_python_ok('-c', 'import sys; sys.exit(0)')
1314
self.assertEqual(0, t[0], 'return code was not 0')
1415

15-
def test_assert_python_expect_failure(self):
16+
def test_assert_python_failure(self):
1617
# I didn't import the sys module so this child will fail.
17-
rc, out, err = script_helper._assert_python(False, '-c', 'sys.exit(0)')
18+
rc, out, err = script_helper.assert_python_failure('-c', 'sys.exit(0)')
1819
self.assertNotEqual(0, rc, 'return code should not be 0')
1920

20-
def test_assert_python_raises_expect_success(self):
21+
def test_assert_python_ok_raises(self):
2122
# I didn't import the sys module so this child will fail.
2223
with self.assertRaises(AssertionError) as error_context:
23-
script_helper._assert_python(True, '-c', 'sys.exit(0)')
24+
script_helper.assert_python_ok('-c', 'sys.exit(0)')
2425
error_msg = str(error_context.exception)
2526
self.assertIn('command line:', error_msg)
2627
self.assertIn('sys.exit(0)', error_msg, msg='unexpected command line')
2728

28-
def test_assert_python_raises_expect_failure(self):
29+
def test_assert_python_failure_raises(self):
2930
with self.assertRaises(AssertionError) as error_context:
30-
script_helper._assert_python(False, '-c', 'import sys; sys.exit(0)')
31+
script_helper.assert_python_failure('-c', 'import sys; sys.exit(0)')
3132
error_msg = str(error_context.exception)
3233
self.assertIn('Process return code is 0\n', error_msg)
3334
self.assertIn('import sys; sys.exit(0)', error_msg,

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ Release date: XXX
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #23309: Avoid a deadlock at shutdown if a daemon thread is aborted
14+
while it is holding a lock to a buffered I/O object, and the main thread
15+
tries to use the same I/O object (typically stdout or stderr). A fatal
16+
error is emitted instead.
17+
1318
- Issue #22977: Fixed formatting Windows error messages on Wine.
1419
Patch by Martin Panter.
1520

Modules/_io/bufferedio.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,14 +318,35 @@ typedef struct {
318318
static int
319319
_enter_buffered_busy(buffered *self)
320320
{
321+
int relax_locking;
322+
PyLockStatus st;
321323
if (self->owner == PyThread_get_thread_ident()) {
322324
PyErr_Format(PyExc_RuntimeError,
323325
"reentrant call inside %R", self);
324326
return 0;
325327
}
328+
relax_locking = (_Py_Finalizing != NULL);
326329
Py_BEGIN_ALLOW_THREADS
327-
PyThread_acquire_lock(self->lock, 1);
330+
if (!relax_locking)
331+
st = PyThread_acquire_lock(self->lock, 1);
332+
else {
333+
/* When finalizing, we don't want a deadlock to happen with daemon
334+
* threads abruptly shut down while they owned the lock.
335+
* Therefore, only wait for a grace period (1 s.).
336+
* Note that non-daemon threads have already exited here, so this
337+
* shouldn't affect carefully written threaded I/O code.
338+
*/
339+
st = PyThread_acquire_lock_timed(self->lock, 1e6, 0);
340+
}
328341
Py_END_ALLOW_THREADS
342+
if (relax_locking && st != PY_LOCK_ACQUIRED) {
343+
PyObject *msgobj = PyUnicode_FromFormat(
344+
"could not acquire lock for %A at interpreter "
345+
"shutdown, possibly due to daemon threads",
346+
(PyObject *) self);
347+
char *msg = PyUnicode_AsUTF8(msgobj);
348+
Py_FatalError(msg);
349+
}
329350
return 1;
330351
}
331352

0 commit comments

Comments
 (0)