Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork34k
Description
Bug report
Bug description:
LeakSanitizer detects memory leaks in thetest__interpchannels test module. The leaks occur in_PyXIData_New() function called fromchannel_send() inModules/_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 -mtest 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 ind86ad87
| _PyXIData_t* | |
| _PyXIData_New(void) | |
| { | |
| _PyXIData_t*xid=PyMem_RawCalloc(1,sizeof(_PyXIData_t)); | |
| if (xid==NULL) { | |
| PyErr_NoMemory(); | |
| } | |
| returnxid; | |
| } |
Called through:
channel_send():cpython/Modules/_interpchannelsmodule.c
Lines 1773 to 1819 ind86ad87
staticint channel_send(_channels*channels,int64_tcid,PyObject*obj, _waiting_t*waiting,unboundop_tunboundop,xidata_fallback_tfallback) { PyThreadState*tstate=_PyThreadState_GET(); PyInterpreterState*interp=tstate->interp; int64_tinterpid=PyInterpreterState_GetID(interp); // Look up the channel. PyThread_type_lockmutex=NULL; _channel_state*chan=NULL; interr=_channels_lookup(channels,cid,&mutex,&chan); if (err!=0) { returnerr; } assert(chan!=NULL); // Past this point we are responsible for releasing the mutex. if (chan->closing!=NULL) { PyThread_release_lock(mutex); returnERR_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. intres=_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); returnres; } return0; } channel_send_wait():cpython/Modules/_interpchannelsmodule.c
Lines 1843 to 1894 ind86ad87
// Like channel_send(), but strictly wait for the object to be received. staticint channel_send_wait(_channels*channels,int64_tcid,PyObject*obj, unboundop_tunboundop,PY_TIMEOUT_Ttimeout, xidata_fallback_tfallback) { // 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_twaiting; if (_waiting_init(&waiting)<0) { assert(PyErr_Occurred()); return-1; } /* Queue up the object. */ intres=channel_send(channels,cid,obj,&waiting,unboundop,fallback); if (res<0) { assert(waiting.status==WAITING_NO_STATUS); gotofinally; } /* 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; gotofinally; } // 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; gotofinally; } } /* success! */ res=0; finally: _waiting_clear(&waiting); returnres; } _sharednsitem_set_value():Lines 2065 to 2084 ind86ad87
staticint _sharednsitem_set_value(_PyXI_namespace_item*item,PyObject*value, xidata_fallback_tfallback) { 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; } return0; } _copy_string_obj_raw()via_PyXI_InitFailure():Lines 1041 to 1065 ind86ad87
staticconstchar* _copy_string_obj_raw(PyObject*strobj,Py_ssize_t*p_size) { Py_ssize_tsize=-1; constchar*str=PyUnicode_AsUTF8AndSize(strobj,&size); if (str==NULL) { returnNULL; } if (size!= (Py_ssize_t)strlen(str)) { PyErr_SetString(PyExc_ValueError,"found embedded NULL character"); returnNULL; } char*copied=PyMem_RawMalloc(size+1); if (copied==NULL) { PyErr_NoMemory(); returnNULL; } strcpy(copied,str); if (p_size!=NULL) { *p_size=size; } returncopied; } Lines 1805 to 1825 ind86ad87
int _PyXI_InitFailure(_PyXI_failure*failure,_PyXI_errcodecode,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(). constchar*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, }; return0; }
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
Linked PRs
- gh-140306: Fix memory leaks in cross-interpreter data handling #140307
- [3.14] gh-140306: Fix memory leaks in cross-interpreter data handling (GH-140307) #140338
- [3.13] gh-140306: Fix memory leaks in cross-interpreter data handling (GH-140307) #140357
- [3.13] gh-140306: Fix memory leaks in cross-interpreter data handling #140397