From c19a9d621c9bcd3c8b9a2c3d8c416f4138a6089e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 12 Jan 2026 10:45:10 +0200 Subject: [PATCH] [3.13] gh-142881: Fix concurrent and reentrant call of atexit.unregister() (GH-142901) (cherry picked from commit dbd10a6c29ba1cfc9348924a090b5dc514470002) Co-authored-by: Serhiy Storchaka --- Lib/test/_test_atexit.py | 34 +++++++++++++++++++ ...-12-17-20-18-17.gh-issue-142881.5IizIQ.rst | 1 + Modules/atexitmodule.c | 3 ++ 3 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-17-20-18-17.gh-issue-142881.5IizIQ.rst 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 93d4c4ede32513..23fcc1b9d35dc0 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -57,6 +57,9 @@ static void atexit_delete_cb(struct atexit_state *state, int i) { atexit_py_callback *cb = state->callbacks[i]; + if (cb == NULL) { + return; + } state->callbacks[i] = NULL; Py_DECREF(cb->func);