Skip to content

Commit 762745a

Browse files
GH-100892: Fix race in clearing threading.local (#100922)
1 parent 847d770 commit 762745a

File tree

4 files changed

+70
-15
lines changed

4 files changed

+70
-15
lines changed

Lib/test/test_threading_local.py

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from doctest import DocTestSuite
44
from test import support
55
from test.support import threading_helper
6+
from test.support.import_helper import import_module
67
import weakref
78

89
# Modules under test
@@ -196,6 +197,18 @@ class X:
196197
self.assertIsNone(wr())
197198

198199

200+
def test_threading_local_clear_race(self):
201+
# See https://github.com/python/cpython/issues/100892
202+
203+
_testcapi = import_module('_testcapi')
204+
_testcapi.call_in_temporary_c_thread(lambda: None, False)
205+
206+
for _ in range(1000):
207+
_ = threading.local()
208+
209+
_testcapi.join_temporary_c_thread()
210+
211+
199212
class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
200213
_local = _thread._local
201214

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix race while iterating over thread states in clearing :class:`threading.local`. Patch by Kumar Aditya.

Modules/_testcapimodule.c

+36-5
Original file line numberDiff line numberDiff line change
@@ -1879,12 +1879,19 @@ temporary_c_thread(void *data)
18791879
PyThread_release_lock(test_c_thread->exit_event);
18801880
}
18811881

1882+
static test_c_thread_t test_c_thread;
1883+
18821884
static PyObject *
1883-
call_in_temporary_c_thread(PyObject *self, PyObject *callback)
1885+
call_in_temporary_c_thread(PyObject *self, PyObject *args)
18841886
{
18851887
PyObject *res = NULL;
1886-
test_c_thread_t test_c_thread;
1888+
PyObject *callback = NULL;
18871889
long thread;
1890+
int wait = 1;
1891+
if (!PyArg_ParseTuple(args, "O|i", &callback, &wait))
1892+
{
1893+
return NULL;
1894+
}
18881895

18891896
test_c_thread.start_event = PyThread_allocate_lock();
18901897
test_c_thread.exit_event = PyThread_allocate_lock();
@@ -1910,6 +1917,10 @@ call_in_temporary_c_thread(PyObject *self, PyObject *callback)
19101917
PyThread_acquire_lock(test_c_thread.start_event, 1);
19111918
PyThread_release_lock(test_c_thread.start_event);
19121919

1920+
if (!wait) {
1921+
Py_RETURN_NONE;
1922+
}
1923+
19131924
Py_BEGIN_ALLOW_THREADS
19141925
PyThread_acquire_lock(test_c_thread.exit_event, 1);
19151926
PyThread_release_lock(test_c_thread.exit_event);
@@ -1919,13 +1930,32 @@ call_in_temporary_c_thread(PyObject *self, PyObject *callback)
19191930

19201931
exit:
19211932
Py_CLEAR(test_c_thread.callback);
1922-
if (test_c_thread.start_event)
1933+
if (test_c_thread.start_event) {
19231934
PyThread_free_lock(test_c_thread.start_event);
1924-
if (test_c_thread.exit_event)
1935+
test_c_thread.start_event = NULL;
1936+
}
1937+
if (test_c_thread.exit_event) {
19251938
PyThread_free_lock(test_c_thread.exit_event);
1939+
test_c_thread.exit_event = NULL;
1940+
}
19261941
return res;
19271942
}
19281943

1944+
static PyObject *
1945+
join_temporary_c_thread(PyObject *self, PyObject *Py_UNUSED(ignored))
1946+
{
1947+
Py_BEGIN_ALLOW_THREADS
1948+
PyThread_acquire_lock(test_c_thread.exit_event, 1);
1949+
PyThread_release_lock(test_c_thread.exit_event);
1950+
Py_END_ALLOW_THREADS
1951+
Py_CLEAR(test_c_thread.callback);
1952+
PyThread_free_lock(test_c_thread.start_event);
1953+
test_c_thread.start_event = NULL;
1954+
PyThread_free_lock(test_c_thread.exit_event);
1955+
test_c_thread.exit_event = NULL;
1956+
Py_RETURN_NONE;
1957+
}
1958+
19291959
/* marshal */
19301960

19311961
static PyObject*
@@ -3275,8 +3305,9 @@ static PyMethodDef TestMethods[] = {
32753305
METH_VARARGS | METH_KEYWORDS},
32763306
{"with_tp_del", with_tp_del, METH_VARARGS},
32773307
{"create_cfunction", create_cfunction, METH_NOARGS},
3278-
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_O,
3308+
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_VARARGS,
32793309
PyDoc_STR("set_error_class(error_class) -> None")},
3310+
{"join_temporary_c_thread", join_temporary_c_thread, METH_NOARGS},
32803311
{"pymarshal_write_long_to_file",
32813312
pymarshal_write_long_to_file, METH_VARARGS},
32823313
{"pymarshal_write_object_to_file",

Modules/_threadmodule.c

+20-10
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,11 @@ local_traverse(localobject *self, visitproc visit, void *arg)
839839
return 0;
840840
}
841841

842+
#define HEAD_LOCK(runtime) \
843+
PyThread_acquire_lock((runtime)->interpreters.mutex, WAIT_LOCK)
844+
#define HEAD_UNLOCK(runtime) \
845+
PyThread_release_lock((runtime)->interpreters.mutex)
846+
842847
static int
843848
local_clear(localobject *self)
844849
{
@@ -849,18 +854,23 @@ local_clear(localobject *self)
849854
/* Remove all strong references to dummies from the thread states */
850855
if (self->key) {
851856
PyInterpreterState *interp = _PyInterpreterState_GET();
857+
_PyRuntimeState *runtime = &_PyRuntime;
858+
HEAD_LOCK(runtime);
852859
PyThreadState *tstate = PyInterpreterState_ThreadHead(interp);
853-
for(; tstate; tstate = PyThreadState_Next(tstate)) {
854-
if (tstate->dict == NULL) {
855-
continue;
856-
}
857-
PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
858-
if (v != NULL) {
859-
Py_DECREF(v);
860-
}
861-
else {
862-
PyErr_Clear();
860+
HEAD_UNLOCK(runtime);
861+
while (tstate) {
862+
if (tstate->dict) {
863+
PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
864+
if (v != NULL) {
865+
Py_DECREF(v);
866+
}
867+
else {
868+
PyErr_Clear();
869+
}
863870
}
871+
HEAD_LOCK(runtime);
872+
tstate = PyThreadState_Next(tstate);
873+
HEAD_UNLOCK(runtime);
864874
}
865875
}
866876
return 0;

0 commit comments

Comments
 (0)