-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
Description
Bug report
Bug description:
LeakSanitizer detects memory leaks in the test__interpchannels
test module. The leaks occur in _PyXIData_New()
function called from channel_send()
in Modules/_interpchannelsmodule.c
.
Affected versions:
- Python 3.13
- Python 3.14
- Python main branch
Steps to reproduce:
- Build Python with address sanitizer:
CC=clang CXX=clang++ ./configure --with-address-sanitizer --with-pydebug --with-undefined-behavior-sanitizer --disable-optimizations && make -j$(nproc)
- Run the test:
./python -X dev -X showrefcount -m test test__interpchannels -j$(nproc)
Expected behavior:
No memory leaks detected.
Actual behavior:
LeakSanitizer reports multiple direct memory leaks. The exact number of leaked bytes and allocations varies between Python versions.
Problematic code locations:
All leaks originate from _PyXIData_New()
:
Lines 306 to 314 in d86ad87
_PyXIData_t * | |
_PyXIData_New(void) | |
{ | |
_PyXIData_t *xid = PyMem_RawCalloc(1, sizeof(_PyXIData_t)); | |
if (xid == NULL) { | |
PyErr_NoMemory(); | |
} | |
return xid; | |
} |
Called through:
-
channel_send()
:
cpython/Modules/_interpchannelsmodule.c
Lines 1773 to 1819 in d86ad87
static int channel_send(_channels *channels, int64_t cid, PyObject *obj, _waiting_t *waiting, unboundop_t unboundop, xidata_fallback_t fallback) { PyThreadState *tstate = _PyThreadState_GET(); PyInterpreterState *interp = tstate->interp; int64_t interpid = PyInterpreterState_GetID(interp); // Look up the channel. PyThread_type_lock mutex = NULL; _channel_state *chan = NULL; int err = _channels_lookup(channels, cid, &mutex, &chan); if (err != 0) { return err; } assert(chan != NULL); // Past this point we are responsible for releasing the mutex. if (chan->closing != NULL) { PyThread_release_lock(mutex); return ERR_CHANNEL_CLOSED; } // Convert the object to cross-interpreter data. _PyXIData_t *data = _PyXIData_New(); if (data == NULL) { PyThread_release_lock(mutex); return -1; } if (_PyObject_GetXIData(tstate, obj, fallback, data) != 0) { PyThread_release_lock(mutex); GLOBAL_FREE(data); return -1; } // Add the data to the channel. int res = _channel_add(chan, interpid, data, waiting, unboundop); PyThread_release_lock(mutex); if (res != 0) { // We may chain an exception here: (void)_release_xid_data(data, 0); GLOBAL_FREE(data); return res; } return 0; } -
channel_send_wait()
:
cpython/Modules/_interpchannelsmodule.c
Lines 1843 to 1894 in d86ad87
// Like channel_send(), but strictly wait for the object to be received. static int channel_send_wait(_channels *channels, int64_t cid, PyObject *obj, unboundop_t unboundop, PY_TIMEOUT_T timeout, xidata_fallback_t fallback) { // We use a stack variable here, so we must ensure that &waiting // is not held by any channel item at the point this function exits. _waiting_t waiting; if (_waiting_init(&waiting) < 0) { assert(PyErr_Occurred()); return -1; } /* Queue up the object. */ int res = channel_send(channels, cid, obj, &waiting, unboundop, fallback); if (res < 0) { assert(waiting.status == WAITING_NO_STATUS); goto finally; } /* Wait until the object is received. */ if (wait_for_lock(waiting.mutex, timeout) < 0) { assert(PyErr_Occurred()); _waiting_finish_releasing(&waiting); /* The send() call is failing now, so make sure the item won't be received. */ channel_clear_sent(channels, cid, &waiting); assert(waiting.status == WAITING_RELEASED); if (!waiting.received) { res = -1; goto finally; } // XXX Emit a warning if not a TimeoutError? PyErr_Clear(); } else { _waiting_finish_releasing(&waiting); assert(waiting.status == WAITING_RELEASED); if (!waiting.received) { res = ERR_CHANNEL_CLOSED_WAITING; goto finally; } } /* success! */ res = 0; finally: _waiting_clear(&waiting); return res; } -
_sharednsitem_set_value()
:
Lines 2065 to 2084 in d86ad87
static int _sharednsitem_set_value(_PyXI_namespace_item *item, PyObject *value, xidata_fallback_t fallback) { assert(_sharednsitem_is_initialized(item)); assert(item->xidata == NULL); item->xidata = _PyXIData_New(); if (item->xidata == NULL) { return -1; } PyThreadState *tstate = PyThreadState_Get(); if (_PyObject_GetXIData(tstate, value, fallback, item->xidata) < 0) { PyMem_RawFree(item->xidata); item->xidata = NULL; // The caller may want to propagate PyExc_NotShareableError // if currently switched between interpreters. return -1; } return 0; } -
_copy_string_obj_raw()
via_PyXI_InitFailure()
:
Lines 1041 to 1065 in d86ad87
static const char * _copy_string_obj_raw(PyObject *strobj, Py_ssize_t *p_size) { Py_ssize_t size = -1; const char *str = PyUnicode_AsUTF8AndSize(strobj, &size); if (str == NULL) { return NULL; } if (size != (Py_ssize_t)strlen(str)) { PyErr_SetString(PyExc_ValueError, "found embedded NULL character"); return NULL; } char *copied = PyMem_RawMalloc(size+1); if (copied == NULL) { PyErr_NoMemory(); return NULL; } strcpy(copied, str); if (p_size != NULL) { *p_size = size; } return copied; }
Lines 1805 to 1825 in d86ad87
int _PyXI_InitFailure(_PyXI_failure *failure, _PyXI_errcode code, PyObject *obj) { PyObject *msgobj = PyObject_Str(obj); if (msgobj == NULL) { return -1; } // This will leak if not paired with clear_xi_failure(). // That happens automatically in _capture_current_exception(). const char *msg = _copy_string_obj_raw(msgobj, NULL); Py_DECREF(msgobj); if (PyErr_Occurred()) { return -1; } *failure = (_PyXI_failure){ .code = code, .msg = msg, .msg_owned = 1, }; return 0; }
Analysis:
The allocated memory in _PyXIData_New()
is not being properly freed. The function allocates memory using _PyMem_DebugRawAlloc()
but the corresponding cleanup appears to be missing in error paths or normal execution flow.
Full leak reports:
See attached log files for detailed stack traces on Python 3.13, 3.14, and main branch.
CPython versions tested on:
3.13, 3.14, CPython main branch
Operating systems tested on:
Linux