From 066e5b1a917ec2134e8997d2cadd815724314252 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 14 Jun 2019 18:55:22 +0200 Subject: bpo-37266: Daemon threads are now denied in subinterpreters (GH-14049) In a subinterpreter, spawning a daemon thread now raises an exception. Daemon threads were never supported in subinterpreters. Previously, the subinterpreter finalization crashed with a Pyton fatal error if a daemon thread was still running. * Add _thread._is_main_interpreter() * threading.Thread.start() now raises RuntimeError if the thread is a daemon thread and the method is called from a subinterpreter. * The _thread module now uses Argument Clinic for the new function. * Use textwrap.dedent() in test_threading.SubinterpThreadingTests --- Doc/library/threading.rst | 8 +++ Doc/whatsnew/3.9.rst | 8 +++ Lib/_dummy_thread.py | 4 ++ Lib/test/test_threading.py | 66 ++++++++++++---------- Lib/threading.py | 6 ++ .../2019-06-13-11-59-52.bpo-37266.goLjef.rst | 4 ++ Modules/_threadmodule.c | 24 ++++++++ Modules/clinic/_threadmodule.c.h | 22 ++++++++ 8 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst create mode 100644 Modules/clinic/_threadmodule.c.h diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 9ffd5cd..f80eb22 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -280,6 +280,8 @@ since it is impossible to detect the termination of alien threads. base class constructor (``Thread.__init__()``) before doing anything else to the thread. + Daemon threads must not be used in subinterpreters. + .. versionchanged:: 3.3 Added the *daemon* argument. @@ -294,6 +296,12 @@ since it is impossible to detect the termination of alien threads. This method will raise a :exc:`RuntimeError` if called more than once on the same thread object. + Raise a :exc:`RuntimeError` if the thread is a daemon thread and the + method is called from a subinterpreter. + + .. versionchanged:: 3.9 + In a subinterpreter, spawning a daemon thread now raises an exception. + .. method:: run() Method representing the thread's activity. diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 999519f..ef30743 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -86,6 +86,14 @@ New Modules Improved Modules ================ +threading +--------- + +In a subinterpreter, spawning a daemon thread now raises an exception. Daemon +threads were never supported in subinterpreters. Previously, the subinterpreter +finalization crashed with a Pyton fatal error if a daemon thread was still +running. + Optimizations ============= diff --git a/Lib/_dummy_thread.py b/Lib/_dummy_thread.py index a2cae54..2407f9b 100644 --- a/Lib/_dummy_thread.py +++ b/Lib/_dummy_thread.py @@ -161,3 +161,7 @@ def interrupt_main(): else: global _interrupt _interrupt = True + + +def _is_main_interpreter(): + return True diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 0a0a62b..a04d496 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -17,6 +17,7 @@ import weakref import os import subprocess import signal +import textwrap from test import lock_tests from test import support @@ -928,14 +929,19 @@ class ThreadJoinOnShutdown(BaseTestCase): class SubinterpThreadingTests(BaseTestCase): + def pipe(self): + r, w = os.pipe() + self.addCleanup(os.close, r) + self.addCleanup(os.close, w) + if hasattr(os, 'set_blocking'): + os.set_blocking(r, False) + return (r, w) def test_threads_join(self): # Non-daemon threads should be joined at subinterpreter shutdown # (issue #18808) - r, w = os.pipe() - self.addCleanup(os.close, r) - self.addCleanup(os.close, w) - code = r"""if 1: + r, w = self.pipe() + code = textwrap.dedent(r""" import os import random import threading @@ -953,7 +959,7 @@ class SubinterpThreadingTests(BaseTestCase): threading.Thread(target=f).start() random_sleep() - """ % (w,) + """ % (w,)) ret = test.support.run_in_subinterp(code) self.assertEqual(ret, 0) # The thread was joined properly. @@ -964,10 +970,8 @@ class SubinterpThreadingTests(BaseTestCase): # Python code returned but before the thread state is deleted. # To achieve this, we register a thread-local object which sleeps # a bit when deallocated. - r, w = os.pipe() - self.addCleanup(os.close, r) - self.addCleanup(os.close, w) - code = r"""if 1: + r, w = self.pipe() + code = textwrap.dedent(r""" import os import random import threading @@ -992,34 +996,38 @@ class SubinterpThreadingTests(BaseTestCase): threading.Thread(target=f).start() random_sleep() - """ % (w,) + """ % (w,)) ret = test.support.run_in_subinterp(code) self.assertEqual(ret, 0) # The thread was joined properly. self.assertEqual(os.read(r, 1), b"x") - @cpython_only - def test_daemon_threads_fatal_error(self): - subinterp_code = r"""if 1: - import os + def test_daemon_thread(self): + r, w = self.pipe() + code = textwrap.dedent(f""" import threading - import time + import sys - def f(): - # Make sure the daemon thread is still running when - # Py_EndInterpreter is called. - time.sleep(10) - threading.Thread(target=f, daemon=True).start() - """ - script = r"""if 1: - import _testcapi + channel = open({w}, "w", closefd=False) + + def func(): + pass + + thread = threading.Thread(target=func, daemon=True) + try: + thread.start() + except RuntimeError as exc: + print("ok: %s" % exc, file=channel, flush=True) + else: + thread.join() + print("fail: RuntimeError not raised", file=channel, flush=True) + """) + ret = test.support.run_in_subinterp(code) + self.assertEqual(ret, 0) - _testcapi.run_in_subinterp(%r) - """ % (subinterp_code,) - with test.support.SuppressCrashReport(): - rc, out, err = assert_python_failure("-c", script) - self.assertIn("Fatal Python error: Py_EndInterpreter: " - "not the last thread", err.decode()) + msg = os.read(r, 100).decode().rstrip() + self.assertEqual("ok: daemon thread are not supported " + "in subinterpreters", msg) class ThreadingExceptionTests(BaseTestCase): diff --git a/Lib/threading.py b/Lib/threading.py index 7c6d404..01a15a6 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -34,6 +34,7 @@ _start_new_thread = _thread.start_new_thread _allocate_lock = _thread.allocate_lock _set_sentinel = _thread._set_sentinel get_ident = _thread.get_ident +_is_main_interpreter = _thread._is_main_interpreter try: get_native_id = _thread.get_native_id _HAVE_THREAD_NATIVE_ID = True @@ -846,6 +847,11 @@ class Thread: if self._started.is_set(): raise RuntimeError("threads can only be started once") + + if self.daemon and not _is_main_interpreter(): + raise RuntimeError("daemon thread are not supported " + "in subinterpreters") + with _active_limbo_lock: _limbo[self] = self try: diff --git a/Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst b/Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst new file mode 100644 index 0000000..f419181 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst @@ -0,0 +1,4 @@ +In a subinterpreter, spawning a daemon thread now raises an exception. Daemon +threads were never supported in subinterpreters. Previously, the subinterpreter +finalization crashed with a Pyton fatal error if a daemon thread was still +running. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index d5e40ef..9ab8e7a 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -8,6 +8,14 @@ #include "structmember.h" /* offsetof */ #include "pythread.h" +#include "clinic/_threadmodule.c.h" + +/*[clinic input] +module _thread +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=be8dbe5cc4b16df7]*/ + + static PyObject *ThreadError; static PyObject *str_dict; @@ -1442,6 +1450,21 @@ PyDoc_STRVAR(excepthook_doc, \n\ Handle uncaught Thread.run() exception."); +/*[clinic input] +_thread._is_main_interpreter + +Return True if the current interpreter is the main Python interpreter. +[clinic start generated code]*/ + +static PyObject * +_thread__is_main_interpreter_impl(PyObject *module) +/*[clinic end generated code: output=7dd82e1728339adc input=cc1eb00fd4598915]*/ +{ + _PyRuntimeState *runtime = &_PyRuntime; + PyInterpreterState *interp = _PyRuntimeState_GetThreadState(runtime)->interp; + return PyBool_FromLong(interp == runtime->interpreters.main); +} + static PyMethodDef thread_methods[] = { {"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread, METH_VARARGS, start_new_doc}, @@ -1471,6 +1494,7 @@ static PyMethodDef thread_methods[] = { METH_NOARGS, _set_sentinel_doc}, {"_excepthook", thread_excepthook, METH_O, excepthook_doc}, + _THREAD__IS_MAIN_INTERPRETER_METHODDEF {NULL, NULL} /* sentinel */ }; diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h new file mode 100644 index 0000000..07ea08b --- /dev/null +++ b/Modules/clinic/_threadmodule.c.h @@ -0,0 +1,22 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +PyDoc_STRVAR(_thread__is_main_interpreter__doc__, +"_is_main_interpreter($module, /)\n" +"--\n" +"\n" +"Return True if the current interpreter is the main Python interpreter."); + +#define _THREAD__IS_MAIN_INTERPRETER_METHODDEF \ + {"_is_main_interpreter", (PyCFunction)_thread__is_main_interpreter, METH_NOARGS, _thread__is_main_interpreter__doc__}, + +static PyObject * +_thread__is_main_interpreter_impl(PyObject *module); + +static PyObject * +_thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _thread__is_main_interpreter_impl(module); +} +/*[clinic end generated code: output=505840d1b9101789 input=a9049054013a1b77]*/ -- cgit v0.12