]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-100892: Fix race in clearing `threading.local` (#100922)
authorKumar Aditya <59607654+kumaraditya303@users.noreply.github.com>
Wed, 11 Jan 2023 10:33:31 +0000 (16:03 +0530)
committerGitHub <noreply@github.com>
Wed, 11 Jan 2023 10:33:31 +0000 (16:03 +0530)
Lib/test/test_threading_local.py
Misc/NEWS.d/next/Core and Builtins/2023-01-10-14-11-17.gh-issue-100892.qfBVYI.rst [new file with mode: 0644]
Modules/_testcapimodule.c
Modules/_threadmodule.c

index 48e302930ff7e98e8c17680ec677901252577c62..f0b829a978feb5c16450448f3fa2a769c3260603 100644 (file)
@@ -3,6 +3,7 @@ import unittest
 from doctest import DocTestSuite
 from test import support
 from test.support import threading_helper
+from test.support.import_helper import import_module
 import weakref
 
 # Modules under test
@@ -196,6 +197,18 @@ class BaseLocalTest:
         self.assertIsNone(wr())
 
 
+    def test_threading_local_clear_race(self):
+        # See https://github.com/python/cpython/issues/100892
+
+        _testcapi = import_module('_testcapi')
+        _testcapi.call_in_temporary_c_thread(lambda: None, False)
+
+        for _ in range(1000):
+            _ = threading.local()
+
+        _testcapi.join_temporary_c_thread()
+
+
 class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
     _local = _thread._local
 
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-01-10-14-11-17.gh-issue-100892.qfBVYI.rst b/Misc/NEWS.d/next/Core and Builtins/2023-01-10-14-11-17.gh-issue-100892.qfBVYI.rst
new file mode 100644 (file)
index 0000000..f2576be
--- /dev/null
@@ -0,0 +1 @@
+Fix race while iterating over thread states in clearing :class:`threading.local`. Patch by Kumar Aditya.
index c777c3efc4923bf23d67da60cbc3581f7732b097..486988b4e34cdd665795accefbe91355488446b8 100644 (file)
@@ -1879,12 +1879,19 @@ temporary_c_thread(void *data)
     PyThread_release_lock(test_c_thread->exit_event);
 }
 
+static test_c_thread_t test_c_thread;
+
 static PyObject *
-call_in_temporary_c_thread(PyObject *self, PyObject *callback)
+call_in_temporary_c_thread(PyObject *self, PyObject *args)
 {
     PyObject *res = NULL;
-    test_c_thread_t test_c_thread;
+    PyObject *callback = NULL;
     long thread;
+    int wait = 1;
+    if (!PyArg_ParseTuple(args, "O|i", &callback, &wait))
+    {
+        return NULL;
+    }
 
     test_c_thread.start_event = PyThread_allocate_lock();
     test_c_thread.exit_event = PyThread_allocate_lock();
@@ -1910,6 +1917,10 @@ call_in_temporary_c_thread(PyObject *self, PyObject *callback)
     PyThread_acquire_lock(test_c_thread.start_event, 1);
     PyThread_release_lock(test_c_thread.start_event);
 
+    if (!wait) {
+        Py_RETURN_NONE;
+    }
+
     Py_BEGIN_ALLOW_THREADS
         PyThread_acquire_lock(test_c_thread.exit_event, 1);
         PyThread_release_lock(test_c_thread.exit_event);
@@ -1919,13 +1930,32 @@ call_in_temporary_c_thread(PyObject *self, PyObject *callback)
 
 exit:
     Py_CLEAR(test_c_thread.callback);
-    if (test_c_thread.start_event)
+    if (test_c_thread.start_event) {
         PyThread_free_lock(test_c_thread.start_event);
-    if (test_c_thread.exit_event)
+        test_c_thread.start_event = NULL;
+    }
+    if (test_c_thread.exit_event) {
         PyThread_free_lock(test_c_thread.exit_event);
+        test_c_thread.exit_event = NULL;
+    }
     return res;
 }
 
+static PyObject *
+join_temporary_c_thread(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+    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_CLEAR(test_c_thread.callback);
+    PyThread_free_lock(test_c_thread.start_event);
+    test_c_thread.start_event = NULL;
+    PyThread_free_lock(test_c_thread.exit_event);
+    test_c_thread.exit_event = NULL;
+    Py_RETURN_NONE;
+}
+
 /* marshal */
 
 static PyObject*
@@ -3275,8 +3305,9 @@ static PyMethodDef TestMethods[] = {
      METH_VARARGS | METH_KEYWORDS},
     {"with_tp_del",             with_tp_del,                     METH_VARARGS},
     {"create_cfunction",        create_cfunction,                METH_NOARGS},
-    {"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O,
+    {"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_VARARGS,
      PyDoc_STR("set_error_class(error_class) -> None")},
+    {"join_temporary_c_thread", join_temporary_c_thread, METH_NOARGS},
     {"pymarshal_write_long_to_file",
         pymarshal_write_long_to_file, METH_VARARGS},
     {"pymarshal_write_object_to_file",
index d323ca83dcffa74754d322b869118155f77e99f1..4fbf0e551b078e5d6fa29ae3875ee21eed23cde2 100644 (file)
@@ -839,6 +839,11 @@ local_traverse(localobject *self, visitproc visit, void *arg)
     return 0;
 }
 
+#define HEAD_LOCK(runtime) \
+    PyThread_acquire_lock((runtime)->interpreters.mutex, WAIT_LOCK)
+#define HEAD_UNLOCK(runtime) \
+    PyThread_release_lock((runtime)->interpreters.mutex)
+
 static int
 local_clear(localobject *self)
 {
@@ -849,18 +854,23 @@ local_clear(localobject *self)
     /* Remove all strong references to dummies from the thread states */
     if (self->key) {
         PyInterpreterState *interp = _PyInterpreterState_GET();
+        _PyRuntimeState *runtime = &_PyRuntime;
+        HEAD_LOCK(runtime);
         PyThreadState *tstate = PyInterpreterState_ThreadHead(interp);
-        for(; tstate; tstate = PyThreadState_Next(tstate)) {
-            if (tstate->dict == NULL) {
-                continue;
-            }
-            PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
-            if (v != NULL) {
-                Py_DECREF(v);
-            }
-            else {
-                PyErr_Clear();
+        HEAD_UNLOCK(runtime);
+        while (tstate) {
+            if (tstate->dict) {
+                PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
+                if (v != NULL) {
+                    Py_DECREF(v);
+                }
+                else {
+                    PyErr_Clear();
+                }
             }
+            HEAD_LOCK(runtime);
+            tstate = PyThreadState_Next(tstate);
+            HEAD_UNLOCK(runtime);
         }
     }
     return 0;