Skip to content

Commit

Permalink
pythonGH-100892: Fix race in clearing threading.local (python#100922)
Browse files Browse the repository at this point in the history
  • Loading branch information
kumaraditya303 committed Jan 11, 2023
1 parent 847d770 commit 762745a
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 15 deletions.
13 changes: 13 additions & 0 deletions Lib/test/test_threading_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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
Expand Down Expand Up @@ -196,6 +197,18 @@ class X:
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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix race while iterating over thread states in clearing :class:`threading.local`. Patch by Kumar Aditya.
41 changes: 36 additions & 5 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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*
Expand Down Expand Up @@ -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",
Expand Down
30 changes: 20 additions & 10 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down

0 comments on commit 762745a

Please sign in to comment.