mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
						commit
						cb46f0ecb0
					
				
					 5 changed files with 94 additions and 14 deletions
				
			
		| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
# Common utility functions used by various script execution tests
 | 
			
		||||
#  e.g. test_cmd_line, test_cmd_line_script and test_runpy
 | 
			
		||||
 | 
			
		||||
import collections
 | 
			
		||||
import importlib
 | 
			
		||||
import sys
 | 
			
		||||
import os
 | 
			
		||||
| 
						 | 
				
			
			@ -50,8 +51,12 @@ def interpreter_requires_environment():
 | 
			
		|||
    return __cached_interp_requires_environment
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_PythonRunResult = collections.namedtuple("_PythonRunResult",
 | 
			
		||||
                                          ("rc", "out", "err"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Executing the interpreter in a subprocess
 | 
			
		||||
def _assert_python(expected_success, *args, **env_vars):
 | 
			
		||||
def run_python_until_end(*args, **env_vars):
 | 
			
		||||
    env_required = interpreter_requires_environment()
 | 
			
		||||
    if '__isolated' in env_vars:
 | 
			
		||||
        isolated = env_vars.pop('__isolated')
 | 
			
		||||
| 
						 | 
				
			
			@ -85,9 +90,14 @@ def _assert_python(expected_success, *args, **env_vars):
 | 
			
		|||
        p.stderr.close()
 | 
			
		||||
    rc = p.returncode
 | 
			
		||||
    err = strip_python_stderr(err)
 | 
			
		||||
    if (rc and expected_success) or (not rc and not expected_success):
 | 
			
		||||
    return _PythonRunResult(rc, out, err), cmd_line
 | 
			
		||||
 | 
			
		||||
def _assert_python(expected_success, *args, **env_vars):
 | 
			
		||||
    res, cmd_line = run_python_until_end(*args, **env_vars)
 | 
			
		||||
    if (res.rc and expected_success) or (not res.rc and not expected_success):
 | 
			
		||||
        # Limit to 80 lines to ASCII characters
 | 
			
		||||
        maxlen = 80 * 100
 | 
			
		||||
        out, err = res.out, res.err
 | 
			
		||||
        if len(out) > maxlen:
 | 
			
		||||
            out = b'(... truncated stdout ...)' + out[-maxlen:]
 | 
			
		||||
        if len(err) > maxlen:
 | 
			
		||||
| 
						 | 
				
			
			@ -106,10 +116,10 @@ def _assert_python(expected_success, *args, **env_vars):
 | 
			
		|||
                             "---\n"
 | 
			
		||||
                             "%s\n"
 | 
			
		||||
                             "---"
 | 
			
		||||
                             % (rc, cmd_line,
 | 
			
		||||
                             % (res.rc, cmd_line,
 | 
			
		||||
                                out,
 | 
			
		||||
                                err))
 | 
			
		||||
    return rc, out, err
 | 
			
		||||
    return res
 | 
			
		||||
 | 
			
		||||
def assert_python_ok(*args, **env_vars):
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@
 | 
			
		|||
from collections import deque, UserList
 | 
			
		||||
from itertools import cycle, count
 | 
			
		||||
from test import support
 | 
			
		||||
from test.script_helper import assert_python_ok
 | 
			
		||||
from test.script_helper import assert_python_ok, run_python_until_end
 | 
			
		||||
 | 
			
		||||
import codecs
 | 
			
		||||
import io  # C implementation of io
 | 
			
		||||
| 
						 | 
				
			
			@ -3437,6 +3437,49 @@ def read(self, n=-1):
 | 
			
		|||
        b = bytearray(2)
 | 
			
		||||
        self.assertRaises(ValueError, bufio.readinto, b)
 | 
			
		||||
 | 
			
		||||
    @unittest.skipUnless(threading, 'Threading required for this test.')
 | 
			
		||||
    def check_daemon_threads_shutdown_deadlock(self, stream_name):
 | 
			
		||||
        # Issue #23309: deadlocks at shutdown should be avoided when a
 | 
			
		||||
        # daemon thread and the main thread both write to a file.
 | 
			
		||||
        code = """if 1:
 | 
			
		||||
            import sys
 | 
			
		||||
            import time
 | 
			
		||||
            import threading
 | 
			
		||||
 | 
			
		||||
            file = sys.{stream_name}
 | 
			
		||||
 | 
			
		||||
            def run():
 | 
			
		||||
                while True:
 | 
			
		||||
                    file.write('.')
 | 
			
		||||
                    file.flush()
 | 
			
		||||
 | 
			
		||||
            thread = threading.Thread(target=run)
 | 
			
		||||
            thread.daemon = True
 | 
			
		||||
            thread.start()
 | 
			
		||||
 | 
			
		||||
            time.sleep(0.5)
 | 
			
		||||
            file.write('!')
 | 
			
		||||
            file.flush()
 | 
			
		||||
            """.format_map(locals())
 | 
			
		||||
        res, _ = run_python_until_end("-c", code)
 | 
			
		||||
        err = res.err.decode()
 | 
			
		||||
        if res.rc != 0:
 | 
			
		||||
            # Failure: should be a fatal error
 | 
			
		||||
            self.assertIn("Fatal Python error: could not acquire lock "
 | 
			
		||||
                          "for <_io.BufferedWriter name='<{stream_name}>'> "
 | 
			
		||||
                          "at interpreter shutdown, possibly due to "
 | 
			
		||||
                          "daemon threads".format_map(locals()),
 | 
			
		||||
                          err)
 | 
			
		||||
        else:
 | 
			
		||||
            self.assertFalse(err.strip('.!'))
 | 
			
		||||
 | 
			
		||||
    def test_daemon_threads_shutdown_stdout_deadlock(self):
 | 
			
		||||
        self.check_daemon_threads_shutdown_deadlock('stdout')
 | 
			
		||||
 | 
			
		||||
    def test_daemon_threads_shutdown_stderr_deadlock(self):
 | 
			
		||||
        self.check_daemon_threads_shutdown_deadlock('stderr')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PyMiscIOTest(MiscIOTest):
 | 
			
		||||
    io = pyio
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,26 +8,27 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class TestScriptHelper(unittest.TestCase):
 | 
			
		||||
    def test_assert_python_expect_success(self):
 | 
			
		||||
        t = script_helper._assert_python(True, '-c', 'import sys; sys.exit(0)')
 | 
			
		||||
 | 
			
		||||
    def test_assert_python_ok(self):
 | 
			
		||||
        t = script_helper.assert_python_ok('-c', 'import sys; sys.exit(0)')
 | 
			
		||||
        self.assertEqual(0, t[0], 'return code was not 0')
 | 
			
		||||
 | 
			
		||||
    def test_assert_python_expect_failure(self):
 | 
			
		||||
    def test_assert_python_failure(self):
 | 
			
		||||
        # I didn't import the sys module so this child will fail.
 | 
			
		||||
        rc, out, err = script_helper._assert_python(False, '-c', 'sys.exit(0)')
 | 
			
		||||
        rc, out, err = script_helper.assert_python_failure('-c', 'sys.exit(0)')
 | 
			
		||||
        self.assertNotEqual(0, rc, 'return code should not be 0')
 | 
			
		||||
 | 
			
		||||
    def test_assert_python_raises_expect_success(self):
 | 
			
		||||
    def test_assert_python_ok_raises(self):
 | 
			
		||||
        # I didn't import the sys module so this child will fail.
 | 
			
		||||
        with self.assertRaises(AssertionError) as error_context:
 | 
			
		||||
            script_helper._assert_python(True, '-c', 'sys.exit(0)')
 | 
			
		||||
            script_helper.assert_python_ok('-c', 'sys.exit(0)')
 | 
			
		||||
        error_msg = str(error_context.exception)
 | 
			
		||||
        self.assertIn('command line:', error_msg)
 | 
			
		||||
        self.assertIn('sys.exit(0)', error_msg, msg='unexpected command line')
 | 
			
		||||
 | 
			
		||||
    def test_assert_python_raises_expect_failure(self):
 | 
			
		||||
    def test_assert_python_failure_raises(self):
 | 
			
		||||
        with self.assertRaises(AssertionError) as error_context:
 | 
			
		||||
            script_helper._assert_python(False, '-c', 'import sys; sys.exit(0)')
 | 
			
		||||
            script_helper.assert_python_failure('-c', 'import sys; sys.exit(0)')
 | 
			
		||||
        error_msg = str(error_context.exception)
 | 
			
		||||
        self.assertIn('Process return code is 0\n', error_msg)
 | 
			
		||||
        self.assertIn('import sys; sys.exit(0)', error_msg,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,11 @@ Release date: XXX
 | 
			
		|||
Core and Builtins
 | 
			
		||||
-----------------
 | 
			
		||||
 | 
			
		||||
- 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.
 | 
			
		||||
 | 
			
		||||
- Issue #22977: Fixed formatting Windows error messages on Wine.
 | 
			
		||||
  Patch by Martin Panter.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -318,14 +318,35 @@ typedef struct {
 | 
			
		|||
static int
 | 
			
		||||
_enter_buffered_busy(buffered *self)
 | 
			
		||||
{
 | 
			
		||||
    int relax_locking;
 | 
			
		||||
    PyLockStatus st;
 | 
			
		||||
    if (self->owner == PyThread_get_thread_ident()) {
 | 
			
		||||
        PyErr_Format(PyExc_RuntimeError,
 | 
			
		||||
                     "reentrant call inside %R", self);
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
    relax_locking = (_Py_Finalizing != NULL);
 | 
			
		||||
    Py_BEGIN_ALLOW_THREADS
 | 
			
		||||
    PyThread_acquire_lock(self->lock, 1);
 | 
			
		||||
    if (!relax_locking)
 | 
			
		||||
        st = PyThread_acquire_lock(self->lock, 1);
 | 
			
		||||
    else {
 | 
			
		||||
        /* When finalizing, we don't want a deadlock to happen with daemon
 | 
			
		||||
         * threads abruptly shut down while they owned the lock.
 | 
			
		||||
         * Therefore, only wait for a grace period (1 s.).
 | 
			
		||||
         * Note that non-daemon threads have already exited here, so this
 | 
			
		||||
         * shouldn't affect carefully written threaded I/O code.
 | 
			
		||||
         */
 | 
			
		||||
        st = PyThread_acquire_lock_timed(self->lock, 1e6, 0);
 | 
			
		||||
    }
 | 
			
		||||
    Py_END_ALLOW_THREADS
 | 
			
		||||
    if (relax_locking && st != PY_LOCK_ACQUIRED) {
 | 
			
		||||
        PyObject *msgobj = PyUnicode_FromFormat(
 | 
			
		||||
            "could not acquire lock for %A at interpreter "
 | 
			
		||||
            "shutdown, possibly due to daemon threads",
 | 
			
		||||
            (PyObject *) self);
 | 
			
		||||
        char *msg = PyUnicode_AsUTF8(msgobj);
 | 
			
		||||
        Py_FatalError(msg);
 | 
			
		||||
    }
 | 
			
		||||
    return 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue