From 504e741e37bf9d92a866cbf5a552dcf16e82a4c6 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 10:15:01 +0000 Subject: [PATCH 01/10] make instance creation thread safe --- .../internal/pycore_pyatomic_ft_wrappers.h | 4 ++ Modules/_ctypes/_ctypes.c | 37 +++++++++++++------ Modules/_ctypes/ctypes.h | 28 +++++++++++++- Modules/_ctypes/stgdict.c | 21 ++++++++--- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index d755d03a5fa190..92571b2b9edd9c 100644 --- a/Include/internal/pycore_pyatomic_ft_wrappers.h +++ b/Include/internal/pycore_pyatomic_ft_wrappers.h @@ -20,6 +20,8 @@ extern "C" { #endif #ifdef Py_GIL_DISABLED +#define FT_ATOMIC_LOAD_INT(value) _Py_atomic_load_int(&value) +#define FT_ATOMIC_STORE_INT(value, new_value) _Py_atomic_store_int(&value, new_value) #define FT_ATOMIC_LOAD_PTR(value) _Py_atomic_load_ptr(&value) #define FT_ATOMIC_STORE_PTR(value, new_value) _Py_atomic_store_ptr(&value, new_value) #define FT_ATOMIC_LOAD_SSIZE(value) _Py_atomic_load_ssize(&value) @@ -111,6 +113,8 @@ extern "C" { _Py_atomic_load_ullong_relaxed(&value) #else +#define FT_ATOMIC_LOAD_INT(value) value +#define FT_ATOMIC_STORE_INT(value, new_value) value = new_value #define FT_ATOMIC_LOAD_PTR(value) value #define FT_ATOMIC_STORE_PTR(value, new_value) value = new_value #define FT_ATOMIC_LOAD_SSIZE(value) value diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 6d817bdaecfa4e..c0ecb3ee02cd2e 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -109,6 +109,7 @@ bytes(cdata) #include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_ceval.h" // _Py_EnterRecursiveCall() #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString() +#include "pycore_pyatomic_ft_wrappers.h" #ifdef MS_WIN32 # include "pycore_modsupport.h" // _PyArg_NoKeywords() #endif @@ -710,13 +711,16 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc if (baseinfo == NULL) { return 0; } - + int ret = 0; + STGINFO_LOCK2(info, baseinfo); /* copy base info */ - if (PyCStgInfo_clone(info, baseinfo) < 0) { - return -1; + ret = PyCStgInfo_clone(info, baseinfo); + if (ret >= 0) { + stginfo_set_dict_final(info); + stginfo_set_dict_final(baseinfo); } - info->flags &= ~DICTFLAG_FINAL; /* clear the 'final' flag in the subclass info */ - baseinfo->flags |= DICTFLAG_FINAL; /* set the 'final' flag in the baseclass info */ + STGINFO_UNLOCK2(); + return ret; } return 0; } @@ -3178,11 +3182,11 @@ PyCData_FromBaseObj(ctypes_state *st, return NULL; } - info->flags |= DICTFLAG_FINAL; cmem = (CDataObject *)((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0); if (cmem == NULL) { return NULL; } + STGINFO_LOCK(info); assert(CDataObject_Check(st, cmem)); cmem->b_length = info->length; cmem->b_size = info->size; @@ -3194,12 +3198,15 @@ PyCData_FromBaseObj(ctypes_state *st, cmem->b_index = index; } else { /* copy contents of adr */ if (-1 == PyCData_MallocBuffer(cmem, info)) { - Py_DECREF(cmem); - return NULL; + Py_CLEAR(cmem); + goto exit; } memcpy(cmem->b_ptr, adr, info->size); cmem->b_index = index; } +exit:; + stginfo_set_dict_final(info); + STGINFO_UNLOCK(); return (PyObject *)cmem; } @@ -3226,8 +3233,11 @@ PyCData_AtAddress(ctypes_state *st, PyObject *type, void *buf) "abstract class"); return NULL; } - - info->flags |= DICTFLAG_FINAL; + if (stginfo_get_dict_final(info) != 1) { + STGINFO_LOCK(info); + stginfo_set_dict_final(info); + STGINFO_UNLOCK(); + } pd = (CDataObject *)((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0); if (!pd) { @@ -3461,8 +3471,11 @@ generic_pycdata_new(ctypes_state *st, "abstract class"); return NULL; } - - info->flags |= DICTFLAG_FINAL; + if (stginfo_get_dict_final(info) != 1) { + STGINFO_LOCK(info); + stginfo_set_dict_final(info); + STGINFO_UNLOCK(); + } obj = (CDataObject *)type->tp_alloc(type, 0); if (!obj) diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 2b8192059a0dc2..f88fa3604b0dcf 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -6,6 +6,8 @@ #include "pycore_moduleobject.h" // _PyModule_GetState() #include "pycore_typeobject.h" // _PyType_GetModuleState() +#include "pycore_critical_section.h" +#include "pycore_pyatomic_ft_wrappers.h" // Do we support C99 complex types in ffi? // For Apple's libffi, this must be determined at runtime (see gh-128156). @@ -373,6 +375,9 @@ typedef struct CFieldObject { *****************************************************************/ typedef struct { +#ifdef Py_GIL_DISABLED + PyMutex mutex; +#endif int initialized; Py_ssize_t size; /* number of bytes */ Py_ssize_t align; /* alignment requirements */ @@ -390,6 +395,7 @@ typedef struct { PyObject *checker; PyObject *module; int flags; /* calling convention and such */ + int dict_final; /* pep3118 fields, pointers need PyMem_Free */ char *format; @@ -399,6 +405,26 @@ typedef struct { /* Py_ssize_t *suboffsets; */ /* unused in ctypes */ } StgInfo; + +#define STGINFO_LOCK(stginfo) Py_BEGIN_CRITICAL_SECTION_MUT(&(stginfo)->mutex) +#define STGINFO_LOCK2(stginfo1, stginfo2) Py_BEGIN_CRITICAL_SECTION2_MUT(&(stginfo1)->mutex, &(stginfo2)->mutex) +#define STGINFO_UNLOCK() Py_END_CRITICAL_SECTION() +#define STGINFO_UNLOCK2() Py_END_CRITICAL_SECTION2() + +static inline void +stginfo_set_dict_final(StgInfo *info) +{ + _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&info->mutex); + FT_ATOMIC_STORE_INT(info->dict_final, 1); +} + +static inline int +stginfo_get_dict_final(StgInfo *info) +{ + return FT_ATOMIC_LOAD_INT(info->dict_final); +} + + extern int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info); extern void ctype_clear_stginfo(StgInfo *info); @@ -427,8 +453,6 @@ PyObject *_ctypes_callproc(ctypes_state *st, #define TYPEFLAG_ISPOINTER 0x100 #define TYPEFLAG_HASPOINTER 0x200 -#define DICTFLAG_FINAL 0x1000 - struct tagPyCArgObject { PyObject_HEAD ffi_type *pffi_type; diff --git a/Modules/_ctypes/stgdict.c b/Modules/_ctypes/stgdict.c index 6e4f8eb21d9632..fe87c8eeeb8f82 100644 --- a/Modules/_ctypes/stgdict.c +++ b/Modules/_ctypes/stgdict.c @@ -264,7 +264,14 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct /* If this structure/union is already marked final we cannot assign _fields_ anymore. */ - if (stginfo->flags & DICTFLAG_FINAL) {/* is final ? */ + if (stginfo_get_dict_final(stginfo) == 1) {/* is final ? */ + PyErr_SetString(PyExc_AttributeError, + "_fields_ is final"); + goto error; + } + STGINFO_LOCK(stginfo); + // check again after locking + if (stginfo_get_dict_final(stginfo) == 1) {/* is final ? */ PyErr_SetString(PyExc_AttributeError, "_fields_ is final"); goto error; @@ -422,12 +429,13 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct goto error; } assert(info); - + STGINFO_LOCK(info); stginfo->ffi_type_pointer.elements[ffi_ofs + i] = &info->ffi_type_pointer; if (info->flags & (TYPEFLAG_ISPOINTER | TYPEFLAG_HASPOINTER)) stginfo->flags |= TYPEFLAG_HASPOINTER; - info->flags |= DICTFLAG_FINAL; /* mark field type final */ + stginfo_set_dict_final(info); /* mark field type final */ + STGINFO_UNLOCK(); if (-1 == PyObject_SetAttr(type, prop->name, prop_obj)) { goto error; } @@ -461,15 +469,15 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct /* We did check that this flag was NOT set above, it must not have been set until now. */ - if (stginfo->flags & DICTFLAG_FINAL) { + if (stginfo_get_dict_final(stginfo) == 1) { PyErr_SetString(PyExc_AttributeError, "Structure or union cannot contain itself"); goto error; } - stginfo->flags |= DICTFLAG_FINAL; + stginfo_set_dict_final(stginfo); retval = MakeAnonFields(type); -error: +error:; Py_XDECREF(layout_func); Py_XDECREF(kwnames); Py_XDECREF(align_obj); @@ -478,6 +486,7 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct Py_XDECREF(layout_fields); Py_XDECREF(layout); Py_XDECREF(format_spec_obj); + STGINFO_UNLOCK(); return retval; } From a05195e64513b6a1ffe8ebeebe911b72a4ec7097 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 10:31:50 +0000 Subject: [PATCH 02/10] add tests --- Lib/test/test_ctypes/test_free_threading.py | 95 +++++++++++++++++++++ Modules/_ctypes/_ctypes.c | 53 ++++++------ Modules/_ctypes/stgdict.c | 10 ++- 3 files changed, 129 insertions(+), 29 deletions(-) create mode 100644 Lib/test/test_ctypes/test_free_threading.py diff --git a/Lib/test/test_ctypes/test_free_threading.py b/Lib/test/test_ctypes/test_free_threading.py new file mode 100644 index 00000000000000..78b3d6aadc16f0 --- /dev/null +++ b/Lib/test/test_ctypes/test_free_threading.py @@ -0,0 +1,95 @@ +from test.support import threading_helper +import unittest +import ctypes +import threading + +threading_helper.requires_working_threading(module=True) + + +class FreeThreadingTests(unittest.TestCase): + + def test_PyCFuncPtr_new_race(self): + # See https://github.com/python/cpython/issues/128567 + + def raw_func(x): + pass + + def race(): + barrier.wait() + def lookup(): + prototype = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + return prototype(raw_func) + + ctypes_args = () + func = lookup() + packed_args = (ctypes.c_void_p * len(ctypes_args))() + for argNum in range(len(ctypes_args)): + packed_args[argNum] = ctypes.cast(ctypes_args[argNum], ctypes.c_void_p) + func(packed_args) + + num_workers = 20 + + barrier = threading.Barrier(num_workers) + + threads = [] + for _ in range(num_workers): + t = threading.Thread(target=race) + threads.append(t) + + with threading_helper.start_threads(threads): + pass + + def test_Structure_creation_race(self): + class POINT(ctypes.Structure): + _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)] + + def race(): + barrier.wait() + return POINT(1, 2) + + num_workers = 20 + + barrier = threading.Barrier(num_workers) + + threads = [] + for _ in range(num_workers): + t = threading.Thread(target=race) + threads.append(t) + + with threading_helper.start_threads(threads): + pass + + def test_stginfo_update_race(self): + # See https://github.com/python/cpython/issues/128570 + def make_nd_memref_descriptor(rank, dtype): + class MemRefDescriptor(ctypes.Structure): + _fields_ = [ + ("aligned", ctypes.POINTER(dtype)), + ] + + return MemRefDescriptor + + + class F32(ctypes.Structure): + _fields_ = [("f32", ctypes.c_float)] + + + def race(): + barrier.wait() + ctp = F32 + ctypes.pointer( + ctypes.pointer(make_nd_memref_descriptor(1, ctp)()) + ) + + + num_workers = 20 + + barrier = threading.Barrier(num_workers) + + threads = [] + for _ in range(num_workers): + t = threading.Thread(target=race) + threads.append(t) + + with threading_helper.start_threads(threads): + pass diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index c0ecb3ee02cd2e..b30e1eb68e3cca 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -716,7 +716,7 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc /* copy base info */ ret = PyCStgInfo_clone(info, baseinfo); if (ret >= 0) { - stginfo_set_dict_final(info); + assert(stginfo_get_dict_final(info) == 0); stginfo_set_dict_final(baseinfo); } STGINFO_UNLOCK2(); @@ -2152,18 +2152,7 @@ static PyObject *CreateSwappedType(ctypes_state *st, PyTypeObject *type, if (!swapped_args) return NULL; - if (st->swapped_suffix == NULL) { -#ifdef WORDS_BIGENDIAN - st->swapped_suffix = PyUnicode_InternFromString("_le"); -#else - st->swapped_suffix = PyUnicode_InternFromString("_be"); -#endif - } - if (st->swapped_suffix == NULL) { - Py_DECREF(swapped_args); - return NULL; - } - + assert(st->swapped_suffix != NULL); newname = PyUnicode_Concat(name, st->swapped_suffix); if (newname == NULL) { Py_DECREF(swapped_args); @@ -3137,6 +3126,7 @@ PyCData_MallocBuffer(CDataObject *obj, StgInfo *info) * access. */ assert (Py_REFCNT(obj) == 1); + assert(stginfo_get_dict_final(info) == 1); if ((size_t)info->size <= sizeof(obj->b_value)) { /* No need to call malloc, can use the default buffer */ @@ -3186,7 +3176,13 @@ PyCData_FromBaseObj(ctypes_state *st, if (cmem == NULL) { return NULL; } - STGINFO_LOCK(info); + + if (stginfo_get_dict_final(info) != 1) { + STGINFO_LOCK(info); + stginfo_set_dict_final(info); + STGINFO_UNLOCK(); + } + assert(CDataObject_Check(st, cmem)); cmem->b_length = info->length; cmem->b_size = info->size; @@ -3198,15 +3194,12 @@ PyCData_FromBaseObj(ctypes_state *st, cmem->b_index = index; } else { /* copy contents of adr */ if (-1 == PyCData_MallocBuffer(cmem, info)) { - Py_CLEAR(cmem); - goto exit; + Py_DECREF(cmem); + return NULL; } memcpy(cmem->b_ptr, adr, info->size); cmem->b_index = index; } -exit:; - stginfo_set_dict_final(info); - STGINFO_UNLOCK(); return (PyObject *)cmem; } @@ -5126,12 +5119,8 @@ PyCArrayType_from_ctype(ctypes_state *st, PyObject *itemtype, Py_ssize_t length) char name[256]; PyObject *len; - if (st->array_cache == NULL) { - st->array_cache = PyDict_New(); - if (st->array_cache == NULL) { - return NULL; - } - } + assert(st->array_cache != NULL); + len = PyLong_FromSsize_t(length); if (len == NULL) return NULL; @@ -6112,6 +6101,20 @@ _ctypes_mod_exec(PyObject *mod) return -1; } + st->array_cache = PyDict_New(); + if (st->array_cache == NULL) { + return -1; + } + +#ifdef WORDS_BIGENDIAN + st->swapped_suffix = PyUnicode_InternFromString("_le"); +#else + st->swapped_suffix = PyUnicode_InternFromString("_be"); +#endif + if (st->swapped_suffix == NULL) { + return -1; + } + if (_ctypes_add_types(mod) < 0) { return -1; } diff --git a/Modules/_ctypes/stgdict.c b/Modules/_ctypes/stgdict.c index fe87c8eeeb8f82..57199736556b39 100644 --- a/Modules/_ctypes/stgdict.c +++ b/Modules/_ctypes/stgdict.c @@ -23,6 +23,8 @@ int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info) { + _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&src_info->mutex); + _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&dst_info->mutex); Py_ssize_t size; ctype_clear_stginfo(dst_info); @@ -248,17 +250,17 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct ctypes_state *st = get_module_state_by_def(Py_TYPE(type)); StgInfo *stginfo; if (PyStgInfo_FromType(st, type, &stginfo) < 0) { - goto error; + return -1; } if (!stginfo) { PyErr_SetString(PyExc_TypeError, "ctypes state is not initialized"); - goto error; + return -1; } PyObject *base = (PyObject *)((PyTypeObject *)type)->tp_base; StgInfo *baseinfo; if (PyStgInfo_FromType(st, base, &baseinfo) < 0) { - goto error; + return -1; } /* If this structure/union is already marked final we cannot assign @@ -267,7 +269,7 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct if (stginfo_get_dict_final(stginfo) == 1) {/* is final ? */ PyErr_SetString(PyExc_AttributeError, "_fields_ is final"); - goto error; + return -1; } STGINFO_LOCK(stginfo); // check again after locking From f3ef75ee30fc7474a48f4d58273ef950a1cc84bb Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 11:36:39 +0000 Subject: [PATCH 03/10] fmt --- Lib/test/test_ctypes/test_free_threading.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Lib/test/test_ctypes/test_free_threading.py b/Lib/test/test_ctypes/test_free_threading.py index 78b3d6aadc16f0..3bd519a4656c95 100644 --- a/Lib/test/test_ctypes/test_free_threading.py +++ b/Lib/test/test_ctypes/test_free_threading.py @@ -28,7 +28,6 @@ def lookup(): func(packed_args) num_workers = 20 - barrier = threading.Barrier(num_workers) threads = [] @@ -48,7 +47,6 @@ def race(): return POINT(1, 2) num_workers = 20 - barrier = threading.Barrier(num_workers) threads = [] @@ -69,11 +67,9 @@ class MemRefDescriptor(ctypes.Structure): return MemRefDescriptor - class F32(ctypes.Structure): _fields_ = [("f32", ctypes.c_float)] - def race(): barrier.wait() ctp = F32 @@ -81,9 +77,7 @@ def race(): ctypes.pointer(make_nd_memref_descriptor(1, ctp)()) ) - num_workers = 20 - barrier = threading.Barrier(num_workers) threads = [] From e3bec70814f1402e156afb9b9806529ab1f8f2c2 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 11:48:20 +0000 Subject: [PATCH 04/10] wip --- Modules/_ctypes/_ctypes.c | 22 ++++++---------------- Modules/_ctypes/ctypes.h | 21 ++++++++++++++++----- Modules/_ctypes/stgdict.c | 4 ++-- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 28b778ec6e70d0..45ae9da309ad3b 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -717,7 +717,7 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc ret = PyCStgInfo_clone(info, baseinfo); if (ret >= 0) { assert(stginfo_get_dict_final(info) == 0); - stginfo_set_dict_final(baseinfo); + stginfo_set_dict_final_lock_held(baseinfo); } STGINFO_UNLOCK2(); return ret; @@ -3177,11 +3177,7 @@ PyCData_FromBaseObj(ctypes_state *st, return NULL; } - if (stginfo_get_dict_final(info) != 1) { - STGINFO_LOCK(info); - stginfo_set_dict_final(info); - STGINFO_UNLOCK(); - } + stginfo_set_dict_final(info); assert(CDataObject_Check(st, cmem)); cmem->b_length = info->length; @@ -3226,11 +3222,8 @@ PyCData_AtAddress(ctypes_state *st, PyObject *type, void *buf) "abstract class"); return NULL; } - if (stginfo_get_dict_final(info) != 1) { - STGINFO_LOCK(info); - stginfo_set_dict_final(info); - STGINFO_UNLOCK(); - } + + stginfo_set_dict_final(info); pd = (CDataObject *)((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0); if (!pd) { @@ -3464,11 +3457,8 @@ generic_pycdata_new(ctypes_state *st, "abstract class"); return NULL; } - if (stginfo_get_dict_final(info) != 1) { - STGINFO_LOCK(info); - stginfo_set_dict_final(info); - STGINFO_UNLOCK(); - } + + stginfo_set_dict_final(info); obj = (CDataObject *)type->tp_alloc(type, 0); if (!obj) diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index f88fa3604b0dcf..edc282b72a040a 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -411,20 +411,31 @@ typedef struct { #define STGINFO_UNLOCK() Py_END_CRITICAL_SECTION() #define STGINFO_UNLOCK2() Py_END_CRITICAL_SECTION2() +static inline int +stginfo_get_dict_final(StgInfo *info) +{ + return FT_ATOMIC_LOAD_INT(info->dict_final); +} + static inline void -stginfo_set_dict_final(StgInfo *info) +stginfo_set_dict_final_lock_held(StgInfo *info) { _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&info->mutex); FT_ATOMIC_STORE_INT(info->dict_final, 1); } -static inline int -stginfo_get_dict_final(StgInfo *info) +static inline void +stginfo_set_dict_final(StgInfo *info) { - return FT_ATOMIC_LOAD_INT(info->dict_final); + // avoid acquiring the lock if final is already set + if (stginfo_get_dict_final(info) == 1) { + return; + } + STGINFO_LOCK(info); + stginfo_set_dict_final_lock_held(info); + STGINFO_UNLOCK(); } - extern int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info); extern void ctype_clear_stginfo(StgInfo *info); diff --git a/Modules/_ctypes/stgdict.c b/Modules/_ctypes/stgdict.c index 57199736556b39..4df4aa9b957054 100644 --- a/Modules/_ctypes/stgdict.c +++ b/Modules/_ctypes/stgdict.c @@ -436,7 +436,7 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct if (info->flags & (TYPEFLAG_ISPOINTER | TYPEFLAG_HASPOINTER)) stginfo->flags |= TYPEFLAG_HASPOINTER; - stginfo_set_dict_final(info); /* mark field type final */ + stginfo_set_dict_final_lock_held(info); /* mark field type final */ STGINFO_UNLOCK(); if (-1 == PyObject_SetAttr(type, prop->name, prop_obj)) { goto error; @@ -476,7 +476,7 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct "Structure or union cannot contain itself"); goto error; } - stginfo_set_dict_final(stginfo); + stginfo_set_dict_final_lock_held(stginfo); retval = MakeAnonFields(type); error:; From fd5ad205b884c2e3bf071a5f42787423203b8743 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 12:07:43 +0000 Subject: [PATCH 05/10] fix clearing of final bit --- Modules/_ctypes/_ctypes.c | 2 +- Modules/_ctypes/ctypes.h | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 45ae9da309ad3b..6b85c605b0b463 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -716,7 +716,7 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc /* copy base info */ ret = PyCStgInfo_clone(info, baseinfo); if (ret >= 0) { - assert(stginfo_get_dict_final(info) == 0); + stginfo_clear_dict_final_lock_held(info); stginfo_set_dict_final_lock_held(baseinfo); } STGINFO_UNLOCK2(); diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index edc282b72a040a..9f2b1b67cf82e5 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -436,6 +436,13 @@ stginfo_set_dict_final(StgInfo *info) STGINFO_UNLOCK(); } +static inline void +stginfo_clear_dict_final_lock_held(StgInfo *info) +{ + _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&info->mutex); + FT_ATOMIC_STORE_INT(info->dict_final, 0); +} + extern int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info); extern void ctype_clear_stginfo(StgInfo *info); From ee47298fdda266ef2549118ce8f2aa725951a7fb Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 12:11:18 +0000 Subject: [PATCH 06/10] move setting bit to earlier --- Modules/_ctypes/_ctypes.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 6b85c605b0b463..1cde1e460cbb37 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3172,13 +3172,11 @@ PyCData_FromBaseObj(ctypes_state *st, return NULL; } + stginfo_set_dict_final(info); cmem = (CDataObject *)((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0); if (cmem == NULL) { return NULL; } - - stginfo_set_dict_final(info); - assert(CDataObject_Check(st, cmem)); cmem->b_length = info->length; cmem->b_size = info->size; From 4db3e361a03db0a4584b7037988773dead68fa6d Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 12:31:06 +0000 Subject: [PATCH 07/10] add comment --- Modules/_ctypes/ctypes.h | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 9f2b1b67cf82e5..2f74967ce960e4 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -376,7 +376,7 @@ typedef struct CFieldObject { typedef struct { #ifdef Py_GIL_DISABLED - PyMutex mutex; + PyMutex mutex; /* critical section mutex */ #endif int initialized; Py_ssize_t size; /* number of bytes */ @@ -405,6 +405,23 @@ typedef struct { /* Py_ssize_t *suboffsets; */ /* unused in ctypes */ } StgInfo; +/* + In free-threading, concurrent mutations to StgInfo is not thread safe. + Therefore to make it thread safe, when modifying StgInfo, `STGINFO_LOCK` and + `STGINFO_UNLOCK` macros are used to acquire critical section of the StgInfo. + The critical section is write only and is acquired when modifying the + StgInfo fields and while setting the `dict_final` bit. Once the `dict_final` + is set, StgInfo is treated as read only and no further modifications are + allowed. This allows to avoid acquiring the critical section for most + read operations when `dict_final` is set (general case). + + It is important to set all the fields before setting the `dict_final` bit + in functions like `PyCStructUnionType_update_stginfo` because the store of + `dict_final` uses sequential consistency memory ordering. This ensures that + all the other fields are visible to other threads before the `dict_final` bit + is set thus allowing for lock free reads when `dict_final` is set. + +*/ #define STGINFO_LOCK(stginfo) Py_BEGIN_CRITICAL_SECTION_MUT(&(stginfo)->mutex) #define STGINFO_LOCK2(stginfo1, stginfo2) Py_BEGIN_CRITICAL_SECTION2_MUT(&(stginfo1)->mutex, &(stginfo2)->mutex) @@ -424,10 +441,12 @@ stginfo_set_dict_final_lock_held(StgInfo *info) FT_ATOMIC_STORE_INT(info->dict_final, 1); } + +// Set the `dict_final` bit in StgInfo. It checks if the bit is already set +// and in that avoids acquiring the critical section (general case). static inline void stginfo_set_dict_final(StgInfo *info) { - // avoid acquiring the lock if final is already set if (stginfo_get_dict_final(info) == 1) { return; } From b0e06a083539a280b9c20c61e445727fa6413a4f Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 25 Mar 2025 13:00:32 +0000 Subject: [PATCH 08/10] improve comments --- Modules/_ctypes/_ctypes.c | 10 ++++++---- Modules/_ctypes/ctypes.h | 9 --------- Modules/_ctypes/stgdict.c | 5 +++-- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 1cde1e460cbb37..0bc1709967f13f 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -712,14 +712,16 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc return 0; } int ret = 0; - STGINFO_LOCK2(info, baseinfo); + STGINFO_LOCK(baseinfo); /* copy base info */ ret = PyCStgInfo_clone(info, baseinfo); if (ret >= 0) { - stginfo_clear_dict_final_lock_held(info); - stginfo_set_dict_final_lock_held(baseinfo); + // clear the 'final' bit in the subclass info + // safe to modify without atomics as it is not exposed to other threads + info->dict_final = 0; + stginfo_set_dict_final_lock_held(baseinfo); /* set the 'final' flag in the baseclass info */ } - STGINFO_UNLOCK2(); + STGINFO_UNLOCK(); return ret; } return 0; diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 2f74967ce960e4..225b6a4df4f0dd 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -424,9 +424,7 @@ typedef struct { */ #define STGINFO_LOCK(stginfo) Py_BEGIN_CRITICAL_SECTION_MUT(&(stginfo)->mutex) -#define STGINFO_LOCK2(stginfo1, stginfo2) Py_BEGIN_CRITICAL_SECTION2_MUT(&(stginfo1)->mutex, &(stginfo2)->mutex) #define STGINFO_UNLOCK() Py_END_CRITICAL_SECTION() -#define STGINFO_UNLOCK2() Py_END_CRITICAL_SECTION2() static inline int stginfo_get_dict_final(StgInfo *info) @@ -455,13 +453,6 @@ stginfo_set_dict_final(StgInfo *info) STGINFO_UNLOCK(); } -static inline void -stginfo_clear_dict_final_lock_held(StgInfo *info) -{ - _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&info->mutex); - FT_ATOMIC_STORE_INT(info->dict_final, 0); -} - extern int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info); extern void ctype_clear_stginfo(StgInfo *info); diff --git a/Modules/_ctypes/stgdict.c b/Modules/_ctypes/stgdict.c index 4df4aa9b957054..1e49b7be156e8c 100644 --- a/Modules/_ctypes/stgdict.c +++ b/Modules/_ctypes/stgdict.c @@ -23,8 +23,6 @@ int PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info) { - _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&src_info->mutex); - _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&dst_info->mutex); Py_ssize_t size; ctype_clear_stginfo(dst_info); @@ -36,6 +34,9 @@ PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info) dst_info->ffi_type_pointer.elements = NULL; memcpy(dst_info, src_info, sizeof(StgInfo)); +#ifdef Py_GIL_DISABLED + dst_info->mutex = (PyMutex){0}; +#endif Py_XINCREF(dst_info->proto); Py_XINCREF(dst_info->argtypes); From ef9ba7a6f940644ef1dcff124df5c8f73a34bb5b Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 28 Mar 2025 20:10:31 +0530 Subject: [PATCH 09/10] code review --- Lib/test/test_ctypes/test_free_threading.py | 89 --------------------- Modules/_ctypes/_ctypes.c | 3 - Modules/_ctypes/ctypes.h | 26 +++--- Modules/_ctypes/stgdict.c | 10 +-- 4 files changed, 13 insertions(+), 115 deletions(-) delete mode 100644 Lib/test/test_ctypes/test_free_threading.py diff --git a/Lib/test/test_ctypes/test_free_threading.py b/Lib/test/test_ctypes/test_free_threading.py deleted file mode 100644 index 3bd519a4656c95..00000000000000 --- a/Lib/test/test_ctypes/test_free_threading.py +++ /dev/null @@ -1,89 +0,0 @@ -from test.support import threading_helper -import unittest -import ctypes -import threading - -threading_helper.requires_working_threading(module=True) - - -class FreeThreadingTests(unittest.TestCase): - - def test_PyCFuncPtr_new_race(self): - # See https://github.com/python/cpython/issues/128567 - - def raw_func(x): - pass - - def race(): - barrier.wait() - def lookup(): - prototype = ctypes.CFUNCTYPE(None, ctypes.c_void_p) - return prototype(raw_func) - - ctypes_args = () - func = lookup() - packed_args = (ctypes.c_void_p * len(ctypes_args))() - for argNum in range(len(ctypes_args)): - packed_args[argNum] = ctypes.cast(ctypes_args[argNum], ctypes.c_void_p) - func(packed_args) - - num_workers = 20 - barrier = threading.Barrier(num_workers) - - threads = [] - for _ in range(num_workers): - t = threading.Thread(target=race) - threads.append(t) - - with threading_helper.start_threads(threads): - pass - - def test_Structure_creation_race(self): - class POINT(ctypes.Structure): - _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)] - - def race(): - barrier.wait() - return POINT(1, 2) - - num_workers = 20 - barrier = threading.Barrier(num_workers) - - threads = [] - for _ in range(num_workers): - t = threading.Thread(target=race) - threads.append(t) - - with threading_helper.start_threads(threads): - pass - - def test_stginfo_update_race(self): - # See https://github.com/python/cpython/issues/128570 - def make_nd_memref_descriptor(rank, dtype): - class MemRefDescriptor(ctypes.Structure): - _fields_ = [ - ("aligned", ctypes.POINTER(dtype)), - ] - - return MemRefDescriptor - - class F32(ctypes.Structure): - _fields_ = [("f32", ctypes.c_float)] - - def race(): - barrier.wait() - ctp = F32 - ctypes.pointer( - ctypes.pointer(make_nd_memref_descriptor(1, ctp)()) - ) - - num_workers = 20 - barrier = threading.Barrier(num_workers) - - threads = [] - for _ in range(num_workers): - t = threading.Thread(target=race) - threads.append(t) - - with threading_helper.start_threads(threads): - pass diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 0bc1709967f13f..803d41630dd59a 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -716,9 +716,6 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc /* copy base info */ ret = PyCStgInfo_clone(info, baseinfo); if (ret >= 0) { - // clear the 'final' bit in the subclass info - // safe to modify without atomics as it is not exposed to other threads - info->dict_final = 0; stginfo_set_dict_final_lock_held(baseinfo); /* set the 'final' flag in the baseclass info */ } STGINFO_UNLOCK(); diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 225b6a4df4f0dd..5d12cdbf817580 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -406,21 +406,17 @@ typedef struct { } StgInfo; /* - In free-threading, concurrent mutations to StgInfo is not thread safe. - Therefore to make it thread safe, when modifying StgInfo, `STGINFO_LOCK` and - `STGINFO_UNLOCK` macros are used to acquire critical section of the StgInfo. - The critical section is write only and is acquired when modifying the - StgInfo fields and while setting the `dict_final` bit. Once the `dict_final` - is set, StgInfo is treated as read only and no further modifications are - allowed. This allows to avoid acquiring the critical section for most - read operations when `dict_final` is set (general case). - - It is important to set all the fields before setting the `dict_final` bit - in functions like `PyCStructUnionType_update_stginfo` because the store of - `dict_final` uses sequential consistency memory ordering. This ensures that - all the other fields are visible to other threads before the `dict_final` bit - is set thus allowing for lock free reads when `dict_final` is set. - + To ensure thread safety in the free threading build, the `STGINFO_LOCK` and + `STGINFO_UNLOCK` macros use critical sections to protect against concurrent + modifications to `StgInfo` and assignment of the `dict_final` field. Once + `dict_final` is set, `StgInfo` is treated as read-only, and no further + modifications are allowed. This approach allows most read operations to + proceed without acquiring the critical section lock. + + The `dict_final` field is written only after all other modifications to + `StgInfo` are complete. The reads and writes of `dict_final` use the + sequentially consistent memory ordering to ensure that all other fields are + visible to other threads before the `dict_final` bit is set. */ #define STGINFO_LOCK(stginfo) Py_BEGIN_CRITICAL_SECTION_MUT(&(stginfo)->mutex) diff --git a/Modules/_ctypes/stgdict.c b/Modules/_ctypes/stgdict.c index 1e49b7be156e8c..c4c4ec2cc5266a 100644 --- a/Modules/_ctypes/stgdict.c +++ b/Modules/_ctypes/stgdict.c @@ -37,6 +37,7 @@ PyCStgInfo_clone(StgInfo *dst_info, StgInfo *src_info) #ifdef Py_GIL_DISABLED dst_info->mutex = (PyMutex){0}; #endif + dst_info->dict_final = 0; Py_XINCREF(dst_info->proto); Py_XINCREF(dst_info->argtypes); @@ -264,16 +265,9 @@ PyCStructUnionType_update_stginfo(PyObject *type, PyObject *fields, int isStruct return -1; } + STGINFO_LOCK(stginfo); /* If this structure/union is already marked final we cannot assign _fields_ anymore. */ - - if (stginfo_get_dict_final(stginfo) == 1) {/* is final ? */ - PyErr_SetString(PyExc_AttributeError, - "_fields_ is final"); - return -1; - } - STGINFO_LOCK(stginfo); - // check again after locking if (stginfo_get_dict_final(stginfo) == 1) {/* is final ? */ PyErr_SetString(PyExc_AttributeError, "_fields_ is final"); From 2a05687453cbc6d3a75891053c83e631a00b8ccd Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 30 Mar 2025 14:57:26 +0530 Subject: [PATCH 10/10] use uint8_t --- .../internal/pycore_pyatomic_ft_wrappers.h | 4 ---- Modules/_ctypes/ctypes.h | 21 ++++++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index 92571b2b9edd9c..d755d03a5fa190 100644 --- a/Include/internal/pycore_pyatomic_ft_wrappers.h +++ b/Include/internal/pycore_pyatomic_ft_wrappers.h @@ -20,8 +20,6 @@ extern "C" { #endif #ifdef Py_GIL_DISABLED -#define FT_ATOMIC_LOAD_INT(value) _Py_atomic_load_int(&value) -#define FT_ATOMIC_STORE_INT(value, new_value) _Py_atomic_store_int(&value, new_value) #define FT_ATOMIC_LOAD_PTR(value) _Py_atomic_load_ptr(&value) #define FT_ATOMIC_STORE_PTR(value, new_value) _Py_atomic_store_ptr(&value, new_value) #define FT_ATOMIC_LOAD_SSIZE(value) _Py_atomic_load_ssize(&value) @@ -113,8 +111,6 @@ extern "C" { _Py_atomic_load_ullong_relaxed(&value) #else -#define FT_ATOMIC_LOAD_INT(value) value -#define FT_ATOMIC_STORE_INT(value, new_value) value = new_value #define FT_ATOMIC_LOAD_PTR(value) value #define FT_ATOMIC_STORE_PTR(value, new_value) value = new_value #define FT_ATOMIC_LOAD_SSIZE(value) value diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 5d12cdbf817580..7ffe83dc63d6db 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -375,12 +375,9 @@ typedef struct CFieldObject { *****************************************************************/ typedef struct { -#ifdef Py_GIL_DISABLED - PyMutex mutex; /* critical section mutex */ -#endif int initialized; Py_ssize_t size; /* number of bytes */ - Py_ssize_t align; /* alignment requirements */ + Py_ssize_t align; /* alignment reqwuirements */ Py_ssize_t length; /* number of fields */ ffi_type ffi_type_pointer; PyObject *proto; /* Only for Pointer/ArrayObject */ @@ -395,16 +392,20 @@ typedef struct { PyObject *checker; PyObject *module; int flags; /* calling convention and such */ - int dict_final; +#ifdef Py_GIL_DISABLED + PyMutex mutex; /* critical section mutex */ +#endif + uint8_t dict_final; /* pep3118 fields, pointers need PyMem_Free */ char *format; int ndim; Py_ssize_t *shape; -/* Py_ssize_t *strides; */ /* unused in ctypes */ -/* Py_ssize_t *suboffsets; */ /* unused in ctypes */ + /* Py_ssize_t *strides; */ /* unused in ctypes */ + /* Py_ssize_t *suboffsets; */ /* unused in ctypes */ } StgInfo; + /* To ensure thread safety in the free threading build, the `STGINFO_LOCK` and `STGINFO_UNLOCK` macros use critical sections to protect against concurrent @@ -422,17 +423,17 @@ typedef struct { #define STGINFO_LOCK(stginfo) Py_BEGIN_CRITICAL_SECTION_MUT(&(stginfo)->mutex) #define STGINFO_UNLOCK() Py_END_CRITICAL_SECTION() -static inline int +static inline uint8_t stginfo_get_dict_final(StgInfo *info) { - return FT_ATOMIC_LOAD_INT(info->dict_final); + return FT_ATOMIC_LOAD_UINT8(info->dict_final); } static inline void stginfo_set_dict_final_lock_held(StgInfo *info) { _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(&info->mutex); - FT_ATOMIC_STORE_INT(info->dict_final, 1); + FT_ATOMIC_STORE_UINT8(info->dict_final, 1); }