Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Memory leak in test__interpchannels: _PyXIData_New not freed in channel_send #140306

Closed
Labels
3.13bugs and security fixes3.14bugs and security fixes3.15new features, bugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error
@ashm-dev

Description

@ashm-dev

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:

  1. Build Python with address sanitizer:
CC=clang CXX=clang++ ./configure --with-address-sanitizer --with-pydebug --with-undefined-behavior-sanitizer --disable-optimizations&& make -j$(nproc)
  1. 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():

_PyXIData_t*
_PyXIData_New(void)
{
_PyXIData_t*xid=PyMem_RawCalloc(1,sizeof(_PyXIData_t));
if (xid==NULL) {
PyErr_NoMemory();
}
returnxid;
}

Called through:

  1. channel_send():

    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;
    }

  2. channel_send_wait():

    // 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;
    }

  3. _sharednsitem_set_value():

    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;
    }

  4. _copy_string_obj_raw() via_PyXI_InitFailure():

    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;
    }

    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.

313.log
314.log
main.log

CPython versions tested on:

3.13, 3.14, CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13bugs and security fixes3.14bugs and security fixes3.15new features, bugs and security fixesinterpreter-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


      [8]ページ先頭

      ©2009-2026 Movatter.jp