Related:
Bug description:
tracemalloc's raw-domain allocator hook requires an attached thread
state (PyGILState_Ensure(): tracemalloc_alloc / tracemalloc_realloc) , which violates the documented PYMEM_DOMAIN_RAW contract:
MRE (Minimal Reproducible Example):
Pure Python MRE
import ctypes
import sys
import threading
import tracemalloc
if not sys.platform.startswith("linux"):
msg = "This MRE requires Linux"
raise RuntimeError(msg)
pythonapi = ctypes.pythonapi
pythonapi.PyMem_RawMalloc.restype = ctypes.c_void_p
pythonapi.PyMem_RawMalloc.argtypes = [ctypes.c_size_t]
pythonapi.PyMem_RawFree.argtypes = [ctypes.c_void_p]
# this is only needed to creat a native thread
# without a `PyThreadState` attached to it
libc_cdll = ctypes.CDLL(None)
libc_cdll.pthread_create.restype = ctypes.c_int
libc_cdll.usleep.argtypes = [ctypes.c_uint]
# unlike CDLL, PyDLL does NOT release the GIL around the call
# this is intentional, so that the main thread holds the GIL for
# the full 5 seconds creating conditions under which the raw-allocator
# hook cannot acquire the GIL, even if it were to request it
blocking_libc = ctypes.PyDLL(None)
blocking_libc.sleep.argtypes = [ctypes.c_uint]
THREAD_FN = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
done = threading.Event()
@THREAD_FN
def native_thread_entry(_arg: ctypes.c_void_p) -> ctypes.c_void_p:
# this thread was created using `pthread_create()`, not
# `threading.Thread` it does NOT have an associated `PyThreadState`
libc_cdll.usleep(500_000)
# according to the `PyMem_SetAllocator` contract for `PYMEM_DOMAIN_RAW`
# ("a thread state is not attached when the allocator is called"),
# the call below MUST execute without blocking on the GIL
ptr = pythonapi.PyMem_RawMalloc(64)
pythonapi.PyMem_RawFree(ptr)
# if the allocator internally calls `PyGILState_Ensure()`,
# then this code will never reach this point while the GIL is busy
done.set()
def main() -> None:
# tracemalloc.start() sets a hook on `PYMEM_DOMAIN_RAW`
tracemalloc.start()
thread_id = ctypes.c_ulong()
libc_cdll.pthread_create(ctypes.byref(thread_id), None, native_thread_entry, None)
# The main thread holds the GIL continuously for 5 seconds (`PyDLL.sleep` does not
# release the GIL). If the raw-allocator hook requires the GIL, a deadlock occurs
blocking_libc.sleep(5)
# deadlock
print("DEADLOCK" if not done.is_set() else "OK")
main()
mre.c:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <unistd.h>
static atomic_bool done = false;
static void *native_thread_entry(void *arg)
{
(void)arg;
usleep(500000);
void *ptr = PyMem_RawMalloc(64);
PyMem_RawFree(ptr);
atomic_store(&done, true);
return NULL;
}
static PyObject *mre_run(PyObject *self, PyObject *args)
{
(void)self;
(void)args;
atomic_store(&done, false);
pthread_t thread;
int err = pthread_create(&thread, NULL, native_thread_entry, NULL);
if (err != 0) {
return PyErr_SetFromErrno(PyExc_OSError);
}
sleep(5);
printf("%s\n", atomic_load(&done) ? "OK" : "DEADLOCK");
pthread_detach(thread);
Py_RETURN_NONE;
}
static PyMethodDef methods[] = {
{
"run",
mre_run,
METH_NOARGS,
"Run the raw allocator MRE.",
},
{NULL, NULL, 0, NULL},
};
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"mre",
NULL,
-1,
methods,
};
PyMODINIT_FUNC PyInit_mre(void)
{
return PyModule_Create(&module);
}
setup.py:
from setuptools import Extension, setup
setup(
name="mre",
ext_modules=[
Extension(
"mre",
sources=["mre.c"],
extra_compile_args=["-std=c11"],
),
],
)
test.py:
import tracemalloc
import mre
tracemalloc.start()
mre.run()
CPython versions tested on:
3.10, 3.11, 3.12, 3.13, 3.14, 3.15
Operating systems tested on:
Linux
Related:
GlobalAlloc that uses PyMem_Raw* methods PyO3/pyo3#6268
GlobalAlloc that uses PyMem_Raw* methods PyO3/pyo3#6268 (comment)
add
PyMemRawAllocatorPyO3/pyo3#6279Bug description:
tracemalloc's raw-domain allocator hook requires an attached thread
state (
PyGILState_Ensure():tracemalloc_alloc/tracemalloc_realloc) , which violates the documentedPYMEM_DOMAIN_RAWcontract:https://docs.python.org/3.15/c-api/memory.html#allocator-domains
https://docs.python.org/3.15/c-api/memory.html#raw-memory-interface
https://docs.python.org/3.15/c-api/memory.html#raw-memory-interface:~:text=PyMem%5FSetAllocator%28%29%20does%20have%20the%20following%20contract
MRE (Minimal Reproducible Example):
Pure Python MRE
mre.c:setup.py:test.py:CPython versions tested on:
3.10, 3.11, 3.12, 3.13, 3.14, 3.15
Operating systems tested on:
Linux