Commit 066e5b1a authored by Victor Stinner's avatar Victor Stinner Committed by GitHub

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
parent 212646ca
...@@ -280,6 +280,8 @@ since it is impossible to detect the termination of alien threads. ...@@ -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 base class constructor (``Thread.__init__()``) before doing anything else to
the thread. the thread.
Daemon threads must not be used in subinterpreters.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Added the *daemon* argument. Added the *daemon* argument.
...@@ -294,6 +296,12 @@ since it is impossible to detect the termination of alien threads. ...@@ -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 This method will raise a :exc:`RuntimeError` if called more than once
on the same thread object. 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:: run()
Method representing the thread's activity. Method representing the thread's activity.
......
...@@ -86,6 +86,14 @@ New Modules ...@@ -86,6 +86,14 @@ New Modules
Improved 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 Optimizations
============= =============
......
...@@ -161,3 +161,7 @@ def interrupt_main(): ...@@ -161,3 +161,7 @@ def interrupt_main():
else: else:
global _interrupt global _interrupt
_interrupt = True _interrupt = True
def _is_main_interpreter():
return True
...@@ -17,6 +17,7 @@ import weakref ...@@ -17,6 +17,7 @@ import weakref
import os import os
import subprocess import subprocess
import signal import signal
import textwrap
from test import lock_tests from test import lock_tests
from test import support from test import support
...@@ -928,14 +929,19 @@ class ThreadJoinOnShutdown(BaseTestCase): ...@@ -928,14 +929,19 @@ class ThreadJoinOnShutdown(BaseTestCase):
class SubinterpThreadingTests(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): def test_threads_join(self):
# Non-daemon threads should be joined at subinterpreter shutdown # Non-daemon threads should be joined at subinterpreter shutdown
# (issue #18808) # (issue #18808)
r, w = os.pipe() r, w = self.pipe()
self.addCleanup(os.close, r) code = textwrap.dedent(r"""
self.addCleanup(os.close, w)
code = r"""if 1:
import os import os
import random import random
import threading import threading
...@@ -953,7 +959,7 @@ class SubinterpThreadingTests(BaseTestCase): ...@@ -953,7 +959,7 @@ class SubinterpThreadingTests(BaseTestCase):
threading.Thread(target=f).start() threading.Thread(target=f).start()
random_sleep() random_sleep()
""" % (w,) """ % (w,))
ret = test.support.run_in_subinterp(code) ret = test.support.run_in_subinterp(code)
self.assertEqual(ret, 0) self.assertEqual(ret, 0)
# The thread was joined properly. # The thread was joined properly.
...@@ -964,10 +970,8 @@ class SubinterpThreadingTests(BaseTestCase): ...@@ -964,10 +970,8 @@ class SubinterpThreadingTests(BaseTestCase):
# Python code returned but before the thread state is deleted. # Python code returned but before the thread state is deleted.
# To achieve this, we register a thread-local object which sleeps # To achieve this, we register a thread-local object which sleeps
# a bit when deallocated. # a bit when deallocated.
r, w = os.pipe() r, w = self.pipe()
self.addCleanup(os.close, r) code = textwrap.dedent(r"""
self.addCleanup(os.close, w)
code = r"""if 1:
import os import os
import random import random
import threading import threading
...@@ -992,34 +996,38 @@ class SubinterpThreadingTests(BaseTestCase): ...@@ -992,34 +996,38 @@ class SubinterpThreadingTests(BaseTestCase):
threading.Thread(target=f).start() threading.Thread(target=f).start()
random_sleep() random_sleep()
""" % (w,) """ % (w,))
ret = test.support.run_in_subinterp(code) ret = test.support.run_in_subinterp(code)
self.assertEqual(ret, 0) self.assertEqual(ret, 0)
# The thread was joined properly. # The thread was joined properly.
self.assertEqual(os.read(r, 1), b"x") self.assertEqual(os.read(r, 1), b"x")
@cpython_only def test_daemon_thread(self):
def test_daemon_threads_fatal_error(self): r, w = self.pipe()
subinterp_code = r"""if 1: code = textwrap.dedent(f"""
import os
import threading import threading
import time import sys
def f(): channel = open({w}, "w", closefd=False)
# Make sure the daemon thread is still running when
# Py_EndInterpreter is called. def func():
time.sleep(10) pass
threading.Thread(target=f, daemon=True).start()
""" thread = threading.Thread(target=func, daemon=True)
script = r"""if 1: try:
import _testcapi 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) msg = os.read(r, 100).decode().rstrip()
""" % (subinterp_code,) self.assertEqual("ok: daemon thread are not supported "
with test.support.SuppressCrashReport(): "in subinterpreters", msg)
rc, out, err = assert_python_failure("-c", script)
self.assertIn("Fatal Python error: Py_EndInterpreter: "
"not the last thread", err.decode())
class ThreadingExceptionTests(BaseTestCase): class ThreadingExceptionTests(BaseTestCase):
......
...@@ -34,6 +34,7 @@ _start_new_thread = _thread.start_new_thread ...@@ -34,6 +34,7 @@ _start_new_thread = _thread.start_new_thread
_allocate_lock = _thread.allocate_lock _allocate_lock = _thread.allocate_lock
_set_sentinel = _thread._set_sentinel _set_sentinel = _thread._set_sentinel
get_ident = _thread.get_ident get_ident = _thread.get_ident
_is_main_interpreter = _thread._is_main_interpreter
try: try:
get_native_id = _thread.get_native_id get_native_id = _thread.get_native_id
_HAVE_THREAD_NATIVE_ID = True _HAVE_THREAD_NATIVE_ID = True
...@@ -846,6 +847,11 @@ class Thread: ...@@ -846,6 +847,11 @@ class Thread:
if self._started.is_set(): if self._started.is_set():
raise RuntimeError("threads can only be started once") 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: with _active_limbo_lock:
_limbo[self] = self _limbo[self] = self
try: try:
......
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.
...@@ -8,6 +8,14 @@ ...@@ -8,6 +8,14 @@
#include "structmember.h" /* offsetof */ #include "structmember.h" /* offsetof */
#include "pythread.h" #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 *ThreadError;
static PyObject *str_dict; static PyObject *str_dict;
...@@ -1442,6 +1450,21 @@ PyDoc_STRVAR(excepthook_doc, ...@@ -1442,6 +1450,21 @@ PyDoc_STRVAR(excepthook_doc,
\n\ \n\
Handle uncaught Thread.run() exception."); 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[] = { static PyMethodDef thread_methods[] = {
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread, {"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_doc}, METH_VARARGS, start_new_doc},
...@@ -1471,6 +1494,7 @@ static PyMethodDef thread_methods[] = { ...@@ -1471,6 +1494,7 @@ static PyMethodDef thread_methods[] = {
METH_NOARGS, _set_sentinel_doc}, METH_NOARGS, _set_sentinel_doc},
{"_excepthook", thread_excepthook, {"_excepthook", thread_excepthook,
METH_O, excepthook_doc}, METH_O, excepthook_doc},
_THREAD__IS_MAIN_INTERPRETER_METHODDEF
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };
......
/*[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]*/
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment