Skip to content
Open
2 changes: 1 addition & 1 deletion Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ struct _ts {
PyObject *threading_local_key;

/* Used by `threading.local`s to be remove keys/values for dying threads.
The PyThreadObject must hold the only reference to this value.
The PyThreadState must hold the only reference to this value.
*/
PyObject *threading_local_sentinel;
_PyRemoteDebuggerSupport remote_debugger_support;
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_threading_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ def __eq__(self, other):
with self.assertRaisesRegex(AttributeError, 'Loop.*read-only'):
loop.__setattr__(NameCompareTrue(), 2)

def test_finalizer_leak(self):
# See https://github.com/python/cpython/issues/140798

local = self._local()

class ClassWithDel:
def __del__(_):
local.b = 42
self.assertEqual(local.__dict__, {'b': 42})

def thread_func():
local.a = ClassWithDel()

t = threading.Thread(target=thread_func)
t.start()
t.join()

self.assertEqual(local.__dict__, {})


class ThreadLocalTest(unittest.TestCase, BaseLocalTest):
_local = _thread._local
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix possible memory leak with instance of :class:`!_thread._local` when
some finalizer uses this object after ``thread_local_key`` was cleared.
Patch by Mikhail Efimov.
30 changes: 29 additions & 1 deletion Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1452,8 +1452,10 @@ typedef struct {
PyObject *weakreflist; /* List of weak references to self */
/* A {localdummy -> localdict} dict */
PyObject *localdicts;
/* A set of weakrefs to thread sentinels localdummies*/
/* A set of weakrefs to thread sentinels localdummies */
PyObject *thread_watchdogs;
/* A set of thread sentinels that were created during finalization */
PyObject *abandoned_sentinels;
} localobject;

#define localobject_CAST(op) ((localobject *)(op))
Expand Down Expand Up @@ -1544,6 +1546,11 @@ local_new(PyTypeObject *type, PyObject *args, PyObject *kw)
goto err;
}

self->abandoned_sentinels = PySet_New(NULL);
if (self->abandoned_sentinels == NULL) {
goto err;
}

PyObject *localsdict = NULL;
PyObject *sentinel_wr = NULL;
if (create_localsdict(self, state, &localsdict, &sentinel_wr) < 0) {
Expand All @@ -1568,6 +1575,7 @@ local_traverse(PyObject *op, visitproc visit, void *arg)
Py_VISIT(self->kw);
Py_VISIT(self->localdicts);
Py_VISIT(self->thread_watchdogs);
Py_VISIT(self->abandoned_sentinels);
return 0;
}

Expand All @@ -1579,6 +1587,7 @@ local_clear(PyObject *op)
Py_CLEAR(self->kw);
Py_CLEAR(self->localdicts);
Py_CLEAR(self->thread_watchdogs);
Py_CLEAR(self->abandoned_sentinels);
return 0;
}

Expand Down Expand Up @@ -1862,6 +1871,25 @@ clear_locals(PyObject *locals_and_key, PyObject *dummyweakref)
"thread local %R", (PyObject *)self);
}
}

PyThreadState *tstate = PyThreadState_Get();
if (tstate->threading_local_key != NULL) {
// gh-140798: It's possible that some finalizer recreates
// thread key and sentinel localdummies during clearing localsdict.
// In this case we save thread sentinel to local set and
// clear key and sentinel in thread state one more time.
if (PySet_Add(self->abandoned_sentinels, tstate->threading_local_sentinel) < 0) {
PyErr_FormatUnraisable("Exception ignored while adding "
"thread abandoned sentinel of %R", self);
}

Py_CLEAR(tstate->threading_local_key);
Py_CLEAR(tstate->threading_local_sentinel);
}

assert(tstate->threading_local_key == NULL);
assert(tstate->threading_local_sentinel == NULL);

if (self->thread_watchdogs != NULL) {
if (PySet_Discard(self->thread_watchdogs, dummyweakref) < 0) {
PyErr_FormatUnraisable("Exception ignored while clearing "
Expand Down
Loading