You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Race in stgdict PyCStructUnionType_update_stginfo under free-threading #128570
$ ./python ../../free/check2.py
[('i24', <class '__main__.c_int_Array_24'>)]
[('i98', <class '__main__.c_int_Array_98'>)]
[('i97', <class '__main__.c_int_Array_97'>)]
[('i97', <class '__main__.c_int_Array_97'>)]
[('i98', <class '__main__.c_int_Array_98'>)]
mimalloc: error: double free detected of block 0x20000091050 with size 48
Segmentation fault (core dumped)
$ ./python ../../free/check2.py
[('i99', <class '__main__.c_int_Array_99'>)]
[('i78', <class '__main__.c_int_Array_78'>)]
[('i99', <class '__main__.c_int_Array_99'>)]
Debug memory block at address p=0x20000090b50: API '�'
16102724419182469211 bytes originally requested
The 7 pad bytes at p-7 are not all FORBIDDENBYTE (0xfd):
at p-7: 0xdd *** OUCH
at p-6: 0xdd *** OUCH
at p-5: 0xdd *** OUCH
at p-4: 0xdd *** OUCH
at p-3: 0xdd *** OUCH
at p-2: 0xdd *** OUCH
at p-1: 0xdd *** OUCH
Because memory is corrupted at the start, the count of bytes requested
may be bogus, and checking the trailing pad bytes may segfault.
The 8 pad bytes at tail=0xdf78608b196a2bab are Segmentation fault (core dumped)
Also in some other functions like generic_pycdata_new() because that may wind up attempting allocation as the type is being finalized with an assignment to _fields_. This also applies to #128567.
I haven't fully decided what to do about stgdict's yet. I'm leaning towards just slapping big critical sections on them, but I need to look through what fields are written so we can go for locks or atomics as needed.
I'm leaning towards just slapping big critical sections on them, but I need to look through what fields are written so we can go for locks or atomics as needed.
A mutex fixed the problem for me. Global instead of per-type because of possibility of A._fields_ = [("b", B)] in one thread simultaneously with B._fields_ = [("a", A)] in another:
$ git diff main
diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c
index ede95bdf98..f6479df571 100644
--- a/Modules/_ctypes/_ctypes.c+++ b/Modules/_ctypes/_ctypes.c@@ -699,11 +699,14 @@ StructUnionType_init(PyObject *self, PyObject *args, PyObject *kwds, int isStruc
}
/* copy base info */
+ PyMutex_Lock(&st->mutex);
if (PyCStgInfo_clone(info, baseinfo) < 0) {
+ PyMutex_Unlock(&st->mutex);
return -1;
}
info->flags &= ~DICTFLAG_FINAL; /* clear the 'final' flag in the subclass info */
baseinfo->flags |= DICTFLAG_FINAL; /* set the 'final' flag in the baseclass info */
+ PyMutex_Unlock(&st->mutex);
}
return 0;
}
@@ -3138,7 +3141,9 @@ PyCData_FromBaseObj(ctypes_state *st,
return NULL;
}
+ PyMutex_Lock(&st->mutex);
info->flags |= DICTFLAG_FINAL;
+ PyMutex_Unlock(&st->mutex);
cmem = (CDataObject *)((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0);
if (cmem == NULL) {
return NULL;
@@ -3187,7 +3192,9 @@ PyCData_AtAddress(ctypes_state *st, PyObject *type, void *buf)
return NULL;
}
+ PyMutex_Lock(&st->mutex);
info->flags |= DICTFLAG_FINAL;
+ PyMutex_Unlock(&st->mutex);
pd = (CDataObject *)((PyTypeObject *)type)->tp_alloc((PyTypeObject *)type, 0);
if (!pd) {
@@ -3401,7 +3408,9 @@ generic_pycdata_new(ctypes_state *st,
return NULL;
}
+ PyMutex_Lock(&st->mutex);
info->flags |= DICTFLAG_FINAL;
+ PyMutex_Unlock(&st->mutex);
obj = (CDataObject *)type->tp_alloc(type, 0);
if (!obj)
diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h
index 45e00a538f..82688f0084 100644
--- a/Modules/_ctypes/ctypes.h+++ b/Modules/_ctypes/ctypes.h@@ -75,6 +75,7 @@ typedef struct {
PyObject *error_object_name; // callproc.c
PyObject *PyExc_ArgError;
PyObject *swapped_suffix;
+ PyMutex mutex;
} ctypes_state;
Not sure is in all needed places though or fits desired convention so I leave it there if u are messing with it.
That's not safe, we need to use critical sections (Py_BEGIN_CRITICAL_SECTION/Py_END_CRITICAL_SECTION) because PyCStgInfo_clone is prone to re-entrancy deadlocks via an object's finalizer. For info->flags, we probably want _Py_atomic_or_uint8 (I think it's a uint8?), and then add some critical section wrapper functions around the stginfo functions.
Activity
tom-pytel commentedon Jan 13, 2025
PyCStructUnionType_update_stginfo()definitely needs a lock:Results:
Also in some other functions like
generic_pycdata_new()because that may wind up attempting allocation as the type is being finalized with an assignment to_fields_. This also applies to #128567.ZeroIntensity commentedon Jan 13, 2025
I haven't fully decided what to do about stgdict's yet. I'm leaning towards just slapping big critical sections on them, but I need to look through what fields are written so we can go for locks or atomics as needed.
tom-pytel commentedon Jan 14, 2025
A mutex fixed the problem for me. Global instead of per-type because of possibility of
A._fields_ = [("b", B)]in one thread simultaneously withB._fields_ = [("a", A)]in another:Not sure is in all needed places though or fits desired convention so I leave it there if u are messing with it.
ZeroIntensity commentedon Jan 14, 2025
That's not safe, we need to use critical sections (
Py_BEGIN_CRITICAL_SECTION/Py_END_CRITICAL_SECTION) becausePyCStgInfo_cloneis prone to re-entrancy deadlocks via an object's finalizer. Forinfo->flags, we probably want_Py_atomic_or_uint8(I think it's a uint8?), and then add some critical section wrapper functions around the stginfo functions.