Skip to content

Commit 73d4445

Browse files
[3.14] gh-142881: Fix concurrent and reentrant call of atexit.unregister() (GH-142901) (GH-143721)
(cherry picked from commit dbd10a6) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 6447fa3 commit 73d4445

File tree

3 files changed

+56
-7
lines changed

3 files changed

+56
-7
lines changed

Lib/test/_test_atexit.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,40 @@ def __eq__(self, other):
148148
atexit.unregister(Evil())
149149
atexit._clear()
150150

151+
def test_eq_unregister(self):
152+
# Issue #112127: callback's __eq__ may call unregister
153+
def f1():
154+
log.append(1)
155+
def f2():
156+
log.append(2)
157+
def f3():
158+
log.append(3)
159+
160+
class Pred:
161+
def __eq__(self, other):
162+
nonlocal cnt
163+
cnt += 1
164+
if cnt == when:
165+
atexit.unregister(what)
166+
if other is f2:
167+
return True
168+
return False
169+
170+
for what, expected in (
171+
(f1, [3]),
172+
(f2, [3, 1]),
173+
(f3, [1]),
174+
):
175+
for when in range(1, 4):
176+
with self.subTest(what=what.__name__, when=when):
177+
cnt = 0
178+
log = []
179+
for f in (f1, f2, f3):
180+
atexit.register(f)
181+
atexit.unregister(Pred())
182+
atexit._run_exitfuncs()
183+
self.assertEqual(log, expected)
184+
151185

152186
if __name__ == "__main__":
153187
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix concurrent and reentrant call of :func:`atexit.unregister`.

Modules/atexitmodule.c

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,22 +255,36 @@ atexit_ncallbacks(PyObject *module, PyObject *Py_UNUSED(dummy))
255255
static int
256256
atexit_unregister_locked(PyObject *callbacks, PyObject *func)
257257
{
258-
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) {
258+
for (Py_ssize_t i = PyList_GET_SIZE(callbacks) - 1; i >= 0; --i) {
259259
PyObject *tuple = Py_NewRef(PyList_GET_ITEM(callbacks, i));
260260
assert(PyTuple_CheckExact(tuple));
261261
PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0);
262262
int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ);
263-
Py_DECREF(tuple);
264-
if (cmp < 0)
265-
{
263+
if (cmp < 0) {
264+
Py_DECREF(tuple);
266265
return -1;
267266
}
268267
if (cmp == 1) {
269268
// We found a callback!
270-
if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) {
271-
return -1;
269+
// But its index could have changed if it or other callbacks were
270+
// unregistered during the comparison.
271+
Py_ssize_t j = PyList_GET_SIZE(callbacks) - 1;
272+
j = Py_MIN(j, i);
273+
for (; j >= 0; --j) {
274+
if (PyList_GET_ITEM(callbacks, j) == tuple) {
275+
// We found the callback index! For real!
276+
if (PyList_SetSlice(callbacks, j, j + 1, NULL) < 0) {
277+
Py_DECREF(tuple);
278+
return -1;
279+
}
280+
i = j;
281+
break;
282+
}
272283
}
273-
--i;
284+
}
285+
Py_DECREF(tuple);
286+
if (i >= PyList_GET_SIZE(callbacks)) {
287+
i = PyList_GET_SIZE(callbacks);
274288
}
275289
}
276290

0 commit comments

Comments
 (0)