summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Lib/test/test_threading.py43
-rw-r--r--Misc/NEWS6
-rw-r--r--Modules/_testcapimodule.c94
-rw-r--r--Objects/genobject.c3
4 files changed, 146 insertions, 0 deletions
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index 71f84f6d4c..41be68ed0f 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -14,6 +14,10 @@ import unittest
import weakref
import os
import subprocess
+try:
+ import _testcapi
+except ImportError:
+ _testcapi = None
from test import lock_tests
@@ -720,6 +724,45 @@ class ThreadJoinOnShutdown(BaseTestCase):
for t in threads:
t.join()
+ @unittest.skipIf(_testcapi is None, "need _testcapi module")
+ def test_frame_tstate_tracing(self):
+ # Issue #14432: Crash when a generator is created in a C thread that is
+ # destroyed while the generator is still used. The issue was that a
+ # generator contains a frame, and the frame kept a reference to the
+ # Python state of the destroyed C thread. The crash occurs when a trace
+ # function is setup.
+
+ def noop_trace(frame, event, arg):
+ # no operation
+ return noop_trace
+
+ def generator():
+ while 1:
+ yield "genereator"
+
+ def callback():
+ if callback.gen is None:
+ callback.gen = generator()
+ return next(callback.gen)
+ callback.gen = None
+
+ old_trace = sys.gettrace()
+ sys.settrace(noop_trace)
+ try:
+ # Install a trace function
+ threading.settrace(noop_trace)
+
+ # Create a generator in a C thread which exits after the call
+ _testcapi.call_in_temporary_c_thread(callback)
+
+ # Call the generator in a different Python thread, check that the
+ # generator didn't keep a reference to the destroyed thread state
+ for test in range(3):
+ # The trace function is still called here
+ callback()
+ finally:
+ sys.settrace(old_trace)
+
class ThreadingExceptionTests(BaseTestCase):
# A RuntimeError should be raised if Thread.start() is called
diff --git a/Misc/NEWS b/Misc/NEWS
index cdcdda3f02..008e612b7d 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -9,6 +9,12 @@ What's New in Python 2.7.7?
Core and Builtins
-----------------
+- Issue #14432: Generator now clears the borrowed reference to the thread
+ state. Fix a crash when a generator is created in a C thread that is
+ destroyed while the generator is still used. The issue was that a generator
+ contains a frame, and the frame kept a reference to the Python state of the
+ destroyed C thread. The crash occurs when a trace function is setup.
+
- Issue #19932: Fix typo in import.h, missing whitespaces in function prototypes.
- Issue #19638: Fix possible crash / undefined behaviour from huge (more than 2
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index 4e7d47d859..e4885d187f 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -1687,6 +1687,96 @@ sequence_delitem(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}
+#ifdef WITH_THREAD
+typedef struct {
+ PyThread_type_lock start_event;
+ PyThread_type_lock exit_event;
+ PyObject *callback;
+} test_c_thread_t;
+
+static void
+temporary_c_thread(void *data)
+{
+ test_c_thread_t *test_c_thread = data;
+ PyGILState_STATE state;
+ PyObject *res;
+
+ PyThread_release_lock(test_c_thread->start_event);
+
+ /* Allocate a Python thread state for this thread */
+ state = PyGILState_Ensure();
+
+ res = PyObject_CallFunction(test_c_thread->callback, "", NULL);
+ Py_CLEAR(test_c_thread->callback);
+
+ if (res == NULL) {
+ PyErr_Print();
+ }
+ else {
+ Py_DECREF(res);
+ }
+
+ /* Destroy the Python thread state for this thread */
+ PyGILState_Release(state);
+
+ PyThread_release_lock(test_c_thread->exit_event);
+
+ PyThread_exit_thread();
+}
+
+static PyObject *
+call_in_temporary_c_thread(PyObject *self, PyObject *callback)
+{
+ PyObject *res = NULL;
+ test_c_thread_t test_c_thread;
+ long thread;
+
+ PyEval_InitThreads();
+
+ test_c_thread.start_event = PyThread_allocate_lock();
+ test_c_thread.exit_event = PyThread_allocate_lock();
+ test_c_thread.callback = NULL;
+ if (!test_c_thread.start_event || !test_c_thread.exit_event) {
+ PyErr_SetString(PyExc_RuntimeError, "could not allocate lock");
+ goto exit;
+ }
+
+ Py_INCREF(callback);
+ test_c_thread.callback = callback;
+
+ PyThread_acquire_lock(test_c_thread.start_event, 1);
+ PyThread_acquire_lock(test_c_thread.exit_event, 1);
+
+ thread = PyThread_start_new_thread(temporary_c_thread, &test_c_thread);
+ if (thread == -1) {
+ PyErr_SetString(PyExc_RuntimeError, "unable to start the thread");
+ PyThread_release_lock(test_c_thread.start_event);
+ PyThread_release_lock(test_c_thread.exit_event);
+ goto exit;
+ }
+
+ PyThread_acquire_lock(test_c_thread.start_event, 1);
+ PyThread_release_lock(test_c_thread.start_event);
+
+ Py_BEGIN_ALLOW_THREADS
+ PyThread_acquire_lock(test_c_thread.exit_event, 1);
+ PyThread_release_lock(test_c_thread.exit_event);
+ Py_END_ALLOW_THREADS
+
+ Py_INCREF(Py_None);
+ res = Py_None;
+
+exit:
+ Py_CLEAR(test_c_thread.callback);
+ if (test_c_thread.start_event)
+ PyThread_free_lock(test_c_thread.start_event);
+ if (test_c_thread.exit_event)
+ PyThread_free_lock(test_c_thread.exit_event);
+ return res;
+}
+#endif /* WITH_THREAD */
+
+
static PyMethodDef TestMethods[] = {
{"raise_exception", raise_exception, METH_VARARGS},
{"test_config", (PyCFunction)test_config, METH_NOARGS},
@@ -1745,6 +1835,10 @@ static PyMethodDef TestMethods[] = {
{"make_exception_with_doc", (PyCFunction)make_exception_with_doc,
METH_VARARGS | METH_KEYWORDS},
{"sequence_delitem", (PyCFunction)sequence_delitem, METH_VARARGS},
+#ifdef WITH_THREAD
+ {"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O,
+ PyDoc_STR("set_error_class(error_class) -> None")},
+#endif
{NULL, NULL} /* sentinel */
};
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 341f6605fb..082e03c48e 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -76,6 +76,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
/* Generators always return to their most recent caller, not
* necessarily their creator. */
+ f->f_tstate = tstate;
Py_XINCREF(tstate->frame);
assert(f->f_back == NULL);
f->f_back = tstate->frame;
@@ -89,6 +90,8 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
* cycle. */
assert(f->f_back == tstate->frame);
Py_CLEAR(f->f_back);
+ /* Clear the borrowed reference to the thread state */
+ f->f_tstate = NULL;
/* If the generator just returned (as opposed to yielding), signal
* that the generator is exhausted. */