Skip to content

tracemalloc raw-domain allocator hook violates PYMEM_DOMAIN_RAW contract #155725

Description

@chirizxc

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions