gh-142881: Fix concurrent and reentrant call of atexit.unregister() (GH-142901)

This commit is contained in:
Serhiy Storchaka 2026-01-12 10:45:10 +02:00 committed by GitHub
parent 5f28aa2f37
commit dbd10a6c29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 56 additions and 7 deletions

View File

@ -148,6 +148,40 @@ class GeneralTest(unittest.TestCase):
atexit.unregister(Evil())
atexit._clear()
def test_eq_unregister(self):
# Issue #112127: callback's __eq__ may call unregister
def f1():
log.append(1)
def f2():
log.append(2)
def f3():
log.append(3)
class Pred:
def __eq__(self, other):
nonlocal cnt
cnt += 1
if cnt == when:
atexit.unregister(what)
if other is f2:
return True
return False
for what, expected in (
(f1, [3]),
(f2, [3, 1]),
(f3, [1]),
):
for when in range(1, 4):
with self.subTest(what=what.__name__, when=when):
cnt = 0
log = []
for f in (f1, f2, f3):
atexit.register(f)
atexit.unregister(Pred())
atexit._run_exitfuncs()
self.assertEqual(log, expected)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1 @@
Fix concurrent and reentrant call of :func:`atexit.unregister`.

View File

@ -256,22 +256,36 @@ atexit_ncallbacks(PyObject *module, PyObject *Py_UNUSED(dummy))
static int
atexit_unregister_locked(PyObject *callbacks, PyObject *func)
{
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) {
for (Py_ssize_t i = PyList_GET_SIZE(callbacks) - 1; i >= 0; --i) {
PyObject *tuple = Py_NewRef(PyList_GET_ITEM(callbacks, i));
assert(PyTuple_CheckExact(tuple));
PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0);
int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ);
Py_DECREF(tuple);
if (cmp < 0)
{
if (cmp < 0) {
Py_DECREF(tuple);
return -1;
}
if (cmp == 1) {
// We found a callback!
if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) {
return -1;
// But its index could have changed if it or other callbacks were
// unregistered during the comparison.
Py_ssize_t j = PyList_GET_SIZE(callbacks) - 1;
j = Py_MIN(j, i);
for (; j >= 0; --j) {
if (PyList_GET_ITEM(callbacks, j) == tuple) {
// We found the callback index! For real!
if (PyList_SetSlice(callbacks, j, j + 1, NULL) < 0) {
Py_DECREF(tuple);
return -1;
}
i = j;
break;
}
}
--i;
}
Py_DECREF(tuple);
if (i >= PyList_GET_SIZE(callbacks)) {
i = PyList_GET_SIZE(callbacks);
}
}