From 031dd0d2908192b02a4c8f322bea4bc92e7cb644 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Nov 2026 20:50:53 -0500 Subject: [PATCH 1/4] Lazily load the libffi allocator in ctypes --- Lib/test/test_ctypes/test_callbacks.py | 16 ++++++++++ ...-11-11-12-00-00.gh-issue-135893.cTyp3s.rst | 2 ++ Modules/_ctypes/_ctypes.c | 12 -------- Modules/_ctypes/callbacks.c | 29 +++++++++++++++++++ Tools/c-analyzer/cpython/ignored.tsv | 1 + 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index c7f287d9fb6293d..f4fa9a15f9df943 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -15,6 +15,7 @@ from ctypes.util import find_library, struct, wrap_dll_function from test import support from test.support import import_helper +from test.support import script_helper _ctypes_test = import_helper.import_module("_ctypes_test") @@ -560,5 +561,20 @@ def cb(): self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) +class ImportSideEffectsTestCase(unittest.TestCase): + def test_import_does_not_open_fds(self): + # gh-135893: Importing ctypes must not allocate a libffi closure, + # which can leave a file descriptor open for the rest of the + # process' lifetime. + code = """if 1: + from test.support import os_helper + before = os_helper.fd_count() + import ctypes + after = os_helper.fd_count() + assert before == after + """ + script_helper.assert_python_ok("-c", code) + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst b/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst new file mode 100644 index 000000000000000..c9dffc65f4aee3e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst @@ -0,0 +1,2 @@ +Fix a regression in where importing :mod:`ctypes` could leave an extra file +descriptor open on some Linux systems. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 56d39e29f140dd0..217b5cfedf2adb0 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -6553,18 +6553,6 @@ _ctypes_add_version_constants(PyObject *mod) static int _ctypes_mod_exec(PyObject *mod) { - // See https://github.com/python/cpython/issues/128485 - // This allocates some memory and then frees it to ensure that the - // the dlmalloc allocator initializes itself to avoid data races - // in free-threading. - void *codeloc = NULL; - void *ptr = Py_ffi_closure_alloc(sizeof(void *), &codeloc); - if (ptr == NULL) { - PyErr_NoMemory(); - return -1; - } - Py_ffi_closure_free(ptr); - _ctypes_init_fielddesc(); ctypes_state *st = get_module_state(mod); diff --git a/Modules/_ctypes/callbacks.c b/Modules/_ctypes/callbacks.c index 8395e9eaf3505ec..5c5365121881b2f 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -9,6 +9,7 @@ #endif #include "pycore_call.h" // _PyObject_CallNoArgs() +#include "pycore_lock.h" // _PyOnceFlag_CallOnce() #include "pycore_runtime.h" // _Py_ID() #include @@ -412,6 +413,28 @@ static CThunkObject* CThunkObject_new(ctypes_state *st, Py_ssize_t nargs) return p; } +static _PyOnceFlag closure_alloc_init_once = {0}; + +// See https://github.com/python/cpython/issues/128485 +// This allocates some memory and then frees it to ensure that libffi's +// dlmalloc allocator initializes itself exactly once, avoiding data races +// between threads that don't share a GIL. +// +// This is done lazily with a once flag rather than in the module exec function, +// because allocating a closure can have some side effects that affect the whole +// process (gh-135893). +static int +closure_alloc_init(void *Py_UNUSED(arg)) +{ + void *codeloc = NULL; + void *ptr = Py_ffi_closure_alloc(sizeof(void *), &codeloc); + if (ptr == NULL) { + return -1; + } + Py_ffi_closure_free(ptr); + return 0; +} + CThunkObject *_ctypes_alloc_callback(ctypes_state *st, PyObject *callable, PyObject *converters, @@ -431,6 +454,12 @@ CThunkObject *_ctypes_alloc_callback(ctypes_state *st, assert(CThunk_CheckExact(st, (PyObject *)p)); + if (_PyOnceFlag_CallOnce(&closure_alloc_init_once, + closure_alloc_init, NULL) < 0) { + PyErr_NoMemory(); + goto error; + } + p->pcl_write = Py_ffi_closure_alloc(sizeof(ffi_closure), &p->pcl_exec); if (p->pcl_write == NULL) { PyErr_NoMemory(); diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 3863872fd542624..bda6ce14e8bcd30 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -26,6 +26,7 @@ Modules/posixmodule.c os_pipe_impl pipe2_works_atomic - ## guards around resource init Python/thread_pthread.h PyThread__init_thread lib_initialized - Modules/_struct.c - endian_tables_init_once - +Modules/_ctypes/callbacks.c - closure_alloc_init_once - ##----------------------- ## other values (not Python-specific) From aaa7b4b94903afa5d931fdd649f2c625092bf100 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Nov 2026 20:54:20 -0500 Subject: [PATCH 2/4] Move comment. --- Modules/_ctypes/callbacks.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Modules/_ctypes/callbacks.c b/Modules/_ctypes/callbacks.c index 5c5365121881b2f..e54111b8a4fab44 100644 --- a/Modules/_ctypes/callbacks.c +++ b/Modules/_ctypes/callbacks.c @@ -415,17 +415,16 @@ static CThunkObject* CThunkObject_new(ctypes_state *st, Py_ssize_t nargs) static _PyOnceFlag closure_alloc_init_once = {0}; -// See https://github.com/python/cpython/issues/128485 -// This allocates some memory and then frees it to ensure that libffi's -// dlmalloc allocator initializes itself exactly once, avoiding data races -// between threads that don't share a GIL. -// // This is done lazily with a once flag rather than in the module exec function, // because allocating a closure can have some side effects that affect the whole // process (gh-135893). static int closure_alloc_init(void *Py_UNUSED(arg)) { + // See https://github.com/python/cpython/issues/128485 + // This allocates some memory and then frees it to ensure that libffi's + // dlmalloc allocator initializes itself exactly once, avoiding data races + // between threads that don't share a GIL. void *codeloc = NULL; void *ptr = Py_ffi_closure_alloc(sizeof(void *), &codeloc); if (ptr == NULL) { From f5d86a43135f1b667d06afbe72d597395278f205 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Nov 2026 20:57:07 -0500 Subject: [PATCH 3/4] Don't make a new test class. --- Lib/test/test_ctypes/test_callbacks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index f4fa9a15f9df943..7d746749130db25 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -560,8 +560,6 @@ def cb(): self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) - -class ImportSideEffectsTestCase(unittest.TestCase): def test_import_does_not_open_fds(self): # gh-135893: Importing ctypes must not allocate a libffi closure, # which can leave a file descriptor open for the rest of the From 878a8a5f323c9a8c01354e2dab56c99335477314 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Nov 2026 21:08:59 -0500 Subject: [PATCH 4/4] Adjust blurb entry. --- .../Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst b/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst index c9dffc65f4aee3e..bddd3d817ab1cbf 100644 --- a/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst +++ b/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst @@ -1,2 +1,3 @@ -Fix a regression in where importing :mod:`ctypes` could leave an extra file -descriptor open on some Linux systems. +Fix a regression where importing :mod:`ctypes` could leave an extra file +descriptor open on some Linux systems, or hang in x86-64 macOS applications +signed with the hardened runtime.