Skip to content

Commit 9b633f5

Browse files
gh-135893: Lazily initialize the libffi allocator in ctypes (GH-161844)
1 parent ba93577 commit 9b633f5

5 files changed

Lines changed: 46 additions & 12 deletions

File tree

‎Lib/test/test_ctypes/test_callbacks.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ctypes.util import find_library, struct, wrap_dll_function
1616
from test import support
1717
from test.support import import_helper
18+
from test.support import script_helper
1819
_ctypes_test = import_helper.import_module("_ctypes_test")
1920

2021

@@ -559,6 +560,19 @@ def cb():
559560

560561
self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1)
561562

563+
def test_import_does_not_open_fds(self):
564+
# gh-135893: Importing ctypes must not allocate a libffi closure,
565+
# which can leave a file descriptor open for the rest of the
566+
# process' lifetime.
567+
code = """if 1:
568+
from test.support import os_helper
569+
before = os_helper.fd_count()
570+
import ctypes
571+
after = os_helper.fd_count()
572+
assert before == after
573+
"""
574+
script_helper.assert_python_ok("-c", code)
575+
562576

563577
if __name__ == '__main__':
564578
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a regression where importing :mod:`ctypes` could leave an extra file
2+
descriptor open on some Linux systems, or hang in x86-64 macOS applications
3+
signed with the hardened runtime.

‎Modules/_ctypes/_ctypes.c‎

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6553,18 +6553,6 @@ _ctypes_add_version_constants(PyObject *mod)
65536553
static int
65546554
_ctypes_mod_exec(PyObject *mod)
65556555
{
6556-
// See https://github.com/python/cpython/issues/128485
6557-
// This allocates some memory and then frees it to ensure that the
6558-
// the dlmalloc allocator initializes itself to avoid data races
6559-
// in free-threading.
6560-
void *codeloc = NULL;
6561-
void *ptr = Py_ffi_closure_alloc(sizeof(void *), &codeloc);
6562-
if (ptr == NULL) {
6563-
PyErr_NoMemory();
6564-
return -1;
6565-
}
6566-
Py_ffi_closure_free(ptr);
6567-
65686556
_ctypes_init_fielddesc();
65696557

65706558
ctypes_state *st = get_module_state(mod);

‎Modules/_ctypes/callbacks.c‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#endif
1010

1111
#include "pycore_call.h" // _PyObject_CallNoArgs()
12+
#include "pycore_lock.h" // _PyOnceFlag_CallOnce()
1213
#include "pycore_runtime.h" // _Py_ID()
1314

1415
#include <ffi.h>
@@ -412,6 +413,27 @@ static CThunkObject* CThunkObject_new(ctypes_state *st, Py_ssize_t nargs)
412413
return p;
413414
}
414415

416+
static _PyOnceFlag closure_alloc_init_once = {0};
417+
418+
// This is done lazily with a once flag rather than in the module exec function,
419+
// because allocating a closure can have some side effects that affect the whole
420+
// process (gh-135893).
421+
static int
422+
closure_alloc_init(void *Py_UNUSED(arg))
423+
{
424+
// See https://github.com/python/cpython/issues/128485
425+
// This allocates some memory and then frees it to ensure that libffi's
426+
// dlmalloc allocator initializes itself exactly once, avoiding data races
427+
// between threads that don't share a GIL.
428+
void *codeloc = NULL;
429+
void *ptr = Py_ffi_closure_alloc(sizeof(void *), &codeloc);
430+
if (ptr == NULL) {
431+
return -1;
432+
}
433+
Py_ffi_closure_free(ptr);
434+
return 0;
435+
}
436+
415437
CThunkObject *_ctypes_alloc_callback(ctypes_state *st,
416438
PyObject *callable,
417439
PyObject *converters,
@@ -431,6 +453,12 @@ CThunkObject *_ctypes_alloc_callback(ctypes_state *st,
431453

432454
assert(CThunk_CheckExact(st, (PyObject *)p));
433455

456+
if (_PyOnceFlag_CallOnce(&closure_alloc_init_once,
457+
closure_alloc_init, NULL) < 0) {
458+
PyErr_NoMemory();
459+
goto error;
460+
}
461+
434462
p->pcl_write = Py_ffi_closure_alloc(sizeof(ffi_closure), &p->pcl_exec);
435463
if (p->pcl_write == NULL) {
436464
PyErr_NoMemory();

‎Tools/c-analyzer/cpython/ignored.tsv‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Modules/posixmodule.c os_pipe_impl pipe2_works_atomic -
2626
## guards around resource init
2727
Python/thread_pthread.h PyThread__init_thread lib_initialized -
2828
Modules/_struct.c - endian_tables_init_once -
29+
Modules/_ctypes/callbacks.c - closure_alloc_init_once -
2930

3031
##-----------------------
3132
## other values (not Python-specific)

0 commit comments

Comments
 (0)