diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index c7f287d9fb6293..7d746749130db2 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") @@ -559,6 +560,19 @@ def cb(): self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) + 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 00000000000000..bddd3d817ab1cb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-11-12-00-00.gh-issue-135893.cTyp3s.rst @@ -0,0 +1,3 @@ +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. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 56d39e29f140dd..217b5cfedf2adb 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 8395e9eaf3505e..e54111b8a4fab4 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,27 @@ static CThunkObject* CThunkObject_new(ctypes_state *st, Py_ssize_t nargs) return p; } +static _PyOnceFlag closure_alloc_init_once = {0}; + +// 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) { + return -1; + } + Py_ffi_closure_free(ptr); + return 0; +} + CThunkObject *_ctypes_alloc_callback(ctypes_state *st, PyObject *callable, PyObject *converters, @@ -431,6 +453,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 3863872fd54262..bda6ce14e8bcd3 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)