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():
|
_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():
|
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():
|
// 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():
|
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():
|
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; |
|
} |
|
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.
313.log
314.log
main.log
CPython versions tested on:
3.13, 3.14, CPython main branch
Operating systems tested on:
Linux
Linked PRs
Bug report
Bug description:
LeakSanitizer detects memory leaks in the
test__interpchannelstest module. The leaks occur in_PyXIData_New()function called fromchannel_send()inModules/_interpchannelsmodule.c.Affected versions:
Steps to reproduce:
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():cpython/Python/crossinterp.c
Lines 306 to 314 in d86ad87
Called through:
channel_send():cpython/Modules/_interpchannelsmodule.c
Lines 1773 to 1819 in d86ad87
channel_send_wait():cpython/Modules/_interpchannelsmodule.c
Lines 1843 to 1894 in d86ad87
_sharednsitem_set_value():cpython/Python/crossinterp.c
Lines 2065 to 2084 in d86ad87
_copy_string_obj_raw()via_PyXI_InitFailure():cpython/Python/crossinterp.c
Lines 1041 to 1065 in d86ad87
cpython/Python/crossinterp.c
Lines 1805 to 1825 in d86ad87
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.
313.log
314.log
main.log
CPython versions tested on:
3.13, 3.14, CPython main branch
Operating systems tested on:
Linux
Linked PRs