Skip to content

Commit d79b714

Browse files
gh-142881: Fix concurrent and reentrant call of atexit.unregister()
1 parent 568a819 commit d79b714

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
@@ -256,22 +256,36 @@ atexit_ncallbacks(PyObject *module, PyObject *Py_UNUSED(dummy))
256256
static int
257257
atexit_unregister_locked(PyObject *callbacks, PyObject *func)
258258
{
259-
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) {
259+
for (Py_ssize_t i = PyList_GET_SIZE(callbacks) - 1; i >= 0; --i) {
260260
PyObject *tuple = Py_NewRef(PyList_GET_ITEM(callbacks, i));
261261
assert(PyTuple_CheckExact(tuple));
262262
PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0);
263263
int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ);
264-
Py_DECREF(tuple);
265-
if (cmp < 0)
266-
{
264+
if (cmp < 0) {
265+
Py_DECREF(tuple);
267266
return -1;
268267
}
269268
if (cmp == 1) {
270269
// We found a callback!
271-
if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) {
272-
return -1;
270+
// But it's index could be changed if it or other callbacks were
271+
// unregistered during the comparison.
272+
Py_ssize_t j = PyList_GET_SIZE(callbacks) - 1;
273+
j = Py_MIN(j, i);
274+
for (; j >= 0; --j) {
275+
if (PyList_GET_ITEM(callbacks, j) == tuple) {
276+
// We found the callback index! For real!
277+
if (PyList_SetSlice(callbacks, j, j + 1, NULL) < 0) {
278+
Py_DECREF(tuple);
279+
return -1;
280+
}
281+
i = j;
282+
break;
283+
}
273284
}
274-
--i;
285+
}
286+
Py_DECREF(tuple);
287+
if (i >= PyList_GET_SIZE(callbacks)) {
288+
i = PyList_GET_SIZE(callbacks);
275289
}
276290
}
277291

0 commit comments

Comments
 (0)