Repository navigation
ctypes array cache can return distinct types when types are created concurrently #160978
Description
Bug report
On the free-threaded build, this fails with TypeError: expected c_int_Array_1 instance instead of c_int_Array_1:
from concurrent.futures import ThreadPoolExecutor
from ctypes import c_int
from threading import Barrier
barrier = Barrier(8)
def make_type(n):
barrier.wait()
return c_int * n
with ThreadPoolExecutor(8) as pool:
for n in range(1, 1001):
types = list(pool.map(make_type, [n] * 8))
for t in types:
types[0].from_param(t())It passes on the GIL-enabled build because there isn't an opportunity for a GIL thread switch.
If I instead create a type that has a Python hook, I can deterministically trigger the same race on the GIL-enabled build as well:
import ctypes
from concurrent.futures import ThreadPoolExecutor
from threading import Barrier
barrier = Barrier(2)
class Meta(type(ctypes.c_int)):
def __set_name__(cls, owner, name):
barrier.wait(timeout=5)
class C(ctypes.c_int, metaclass=Meta):
pass
with ThreadPoolExecutor(2) as pool:
a, b = pool.map(lambda _: C * 100, range(2))
print(a is b)
a.from_param(b())This second script prints False and then TypeError: expected C_Array_100 instance instead of C_Array_100 on both builds, because defining __set_name__ makes python code run after the cache miss on the array type cache during creation of the array class.
I think the fix is to rewrite how PyCArrayType_from_ctype uses the array_cache:
cpython/Modules/_ctypes/_ctypes.c
Lines 5363 to 5417 in 4a9b981
| PyObject * | |
| PyCArrayType_from_ctype(ctypes_state *st, PyObject *itemtype, Py_ssize_t length) | |
| { | |
| PyObject *key; | |
| char name[256]; | |
| PyObject *len; | |
| assert(st->array_cache != NULL); | |
| len = PyLong_FromSsize_t(length); | |
| if (len == NULL) | |
| return NULL; | |
| key = _PyTuple_FromPairSteal(Py_NewRef(itemtype), len); | |
| if (!key) | |
| return NULL; | |
| PyObject *result; | |
| if (_PyDict_GetItemProxy(st->array_cache, key, &result) != 0) { | |
| // found or error | |
| Py_DECREF(key); | |
| return result; | |
| } | |
| // not found | |
| if (!PyType_Check(itemtype)) { | |
| PyErr_SetString(PyExc_TypeError, | |
| "Expected a type object"); | |
| Py_DECREF(key); | |
| return NULL; | |
| } | |
| #ifdef MS_WIN64 | |
| sprintf(name, "%.200s_Array_%Id", | |
| ((PyTypeObject *)itemtype)->tp_name, length); | |
| #else | |
| sprintf(name, "%.200s_Array_%ld", | |
| ((PyTypeObject *)itemtype)->tp_name, (long)length); | |
| #endif | |
| result = PyObject_CallFunction((PyObject *)st->PyCArrayType_Type, | |
| "s(O){s:n,s:O}", | |
| name, | |
| st->PyCArray_Type, | |
| "_length_", | |
| length, | |
| "_type_", | |
| itemtype | |
| ); | |
| if (result == NULL) { | |
| Py_DECREF(key); | |
| return NULL; | |
| } | |
| if (PyDict_SetItemProxy(st, st->array_cache, key, result) < 0) { | |
| Py_DECREF(key); | |
| Py_DECREF(result); | |
| return NULL; | |
| } | |
| Py_DECREF(key); | |
| return result; |
The current implementation has a check-then-insert TOCTOU bug.
I found this looking at the root cause of numpy/numpy#29943. See also #127945.
CPython versions tested on:
3.14, 3.15, 3.16, main
Operating systems tested on:
macOS (M1)
Activity