https://github.com/python/cpython/commit/dbd10a6c29ba1cfc9348924a090b5dc514470002
commit: dbd10a6c29ba1cfc9348924a090b5dc514470002
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-01-12T10:45:10+02:00
summary:
gh-142881: Fix concurrent and reentrant call of atexit.unregister() (GH-142901)
files:
A Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst
M Lib/test/_test_atexit.py
M Modules/atexitmodule.c
diff --git a/Lib/test/_test_atexit.py b/Lib/test/_test_atexit.py
index 490b0686a0c179..2e961d6a4854a0 100644
--- a/Lib/test/_test_atexit.py
+++ b/Lib/test/_test_atexit.py
@@ -148,6 +148,40 @@ def __eq__(self, other):
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()
diff --git
a/Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst
b/Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst
new file mode 100644
index 00000000000000..02f22d367bd831
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst
@@ -0,0 +1 @@
+Fix concurrent and reentrant call of :func:`atexit.unregister`.
diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c
index f81f0b5724799b..1c901d9124d9ca 100644
--- a/Modules/atexitmodule.c
+++ b/Modules/atexitmodule.c
@@ -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);
}
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]