From 8c2eb8696f2f6b3ff3947d57fe6f2668adbdeb48 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Oct 2026 11:24:53 -0700 Subject: [PATCH] gh-159739: Hold the codec registry lock while unregistering (GH-160168) Concurrent calls to codecs.unregister() can shrink the search path between its length check and indexed lookup in a free-threaded build. The lookup then returns NULL and an unconditional decref crashes the interpreter; hold the registry mutex across the scan and removal. Also reported in gh-157078. Backport to 3.15 and 3.14. (cherry picked from commit 784b305df8821d57fb6fb6c52794ef67bda0a75d) Co-authored-by: Jelle Zijlstra --- ...-10-21-19-55-00.gh-issue-159739.codc01.rst | 2 ++ Python/codecs.c | 19 ++++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.codc01.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.codc01.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.codc01.rst new file mode 100644 index 00000000000000..985d4264befae2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-21-19-55-00.gh-issue-159739.codc01.rst @@ -0,0 +1,2 @@ +Fix a crash in free-threaded builds when :func:`codecs.unregister` is +called concurrently. diff --git a/Python/codecs.c b/Python/codecs.c index c881d82e30307a..b78d51858f64e5 100644 --- a/Python/codecs.c +++ b/Python/codecs.c @@ -63,24 +63,21 @@ PyCodec_Unregister(PyObject *search_function) PyObject *codec_search_path = interp->codecs.search_path; assert(PyList_CheckExact(codec_search_path)); + FT_MUTEX_LOCK(&interp->codecs.search_path_mutex); for (Py_ssize_t i = 0; i < PyList_GET_SIZE(codec_search_path); i++) { - FT_MUTEX_LOCK(&interp->codecs.search_path_mutex); - PyObject *item = PyList_GetItemRef(codec_search_path, i); - int ret = 1; - if (item == search_function) { - // We hold a reference to the item, so its destructor can't run - // while we hold search_path_mutex. - ret = PyList_SetSlice(codec_search_path, i, i+1, NULL); - } - FT_MUTEX_UNLOCK(&interp->codecs.search_path_mutex); - Py_DECREF(item); - if (ret != 1) { + if (PyList_GET_ITEM(codec_search_path, i) == search_function) { + // Keep the function alive until after releasing the mutex. + Py_INCREF(search_function); + int ret = PyList_SetSlice(codec_search_path, i, i+1, NULL); + FT_MUTEX_UNLOCK(&interp->codecs.search_path_mutex); + Py_DECREF(search_function); assert(interp->codecs.search_cache != NULL); assert(PyDict_CheckExact(interp->codecs.search_cache)); PyDict_Clear(interp->codecs.search_cache); return ret; } } + FT_MUTEX_UNLOCK(&interp->codecs.search_path_mutex); return 0; }