python / python/cpython

Type confusion in `_channelid_shared` via unvalidated `_id` attribute

Open
#143,376 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

extension-modules topic-subinterpreters type-crash
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

What happened?

_channelend_shared trusts an end's _id attribute and hands it to _channelid_shared, which blindly treats the object as a _ChannelID struct; a crafted end type can return an arbitrary tiny object, so the decoder misinterprets memory layout and walks past the allocation, producing a type confusion that ends in a heap buffer overflow.

Proof of Concept:

import _interpchannels as ch
import _interpreters

class Tiny:
    __slots__ = ()

tiny = Tiny()

class End:
    __slots__ = ()
    def __new__(cls, cid=None):
        return object.__new__(cls)
    @property
    def _id(self):
        return tiny  # bogus non-string id

class Send(End): pass
class Recv(End): pass

ch._register_end_types(Send, Recv)
chan = ch._channel_id(ch.create(), _resolve=True)

interp = _interpreters.create()
try:
    _interpreters.run_string(interp, "pass", {"chan": Send(chan)})
finally:
    _interpreters.destroy(interp)

Vulnerable Code Snippet:

Click to expand
/* Buggy Re-entrant Path */
static PyObject *
_interpreters_run_string_impl(PyObject *module, PyObject *id,
                              PyObject *script, PyObject *shared,
                              int restricted)
{
    /* ... */
    int res = _run_in_interpreter(tstate, interp, &xidata, NULL, shared, &runres);
    /* ... */
    Py_RETURN_NONE;
}

static int
_get_xidata(PyThreadState *tstate,
            PyObject *obj, xidata_fallback_t fallback, _PyXIData_t *xidata)
{
    /* ... */
    _PyXIData_getdata_t getdata = lookup_getdata(&ctx, obj);
    int res = getdata.basic != NULL
        ? getdata.basic(tstate, obj, xidata)
        : getdata.fallback(tstate, obj, fallback, xidata);
    /* ... */
    return res;
}

static int
_channelend_shared(PyThreadState *tstate, PyObject *obj, _PyXIData_t *data)
{
    PyObject *cidobj = PyObject_GetAttrString(obj, "_id");  /* Reentrant call site */
    if (cidobj == NULL) {
        return -1;
    }
    int res = _channelid_shared(tstate, cidobj, data);
    Py_DECREF(cidobj);
    if (res < 0) {
        return -1;
    }
    _PyXIData_SET_NEW_OBJECT(data, _channelend_from_xid);
    return 0;
}

static int
_channelid_shared(PyThreadState *tstate, PyObject *obj, _PyXIData_t *data)
{
    /* ... */
    struct _channelid_xid *xid = (struct _channelid_xid *)_PyXIData_DATA(data);
    channelid *cidobj = channelid_CAST(obj);  /* crashing pointer derived */
    xid->cid = cidobj->cid;                   /* Crash site */
    xid->end = cidobj->end;
    xid->resolve = cidobj->resolve;
    return 0;
}

/* Clobbering Path */
static int
_channelend_shared(PyThreadState *tstate, PyObject *obj, _PyXIData_t *data)
{
    PyObject *cidobj = PyObject_GetAttrString(obj, "_id");  /* state mutate site */
    /* no validation that cidobj is a _ChannelID */
    /* ... */
    return 0;
}

Sanitizer Output:

Click to expand
=================================================================
==2202758==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50300001af70 at pc 0x7ddaf5b70bab bp 0x7ffddb3f5520 sp 0x7ffddb3f5510
READ of size 8 at 0x50300001af70 thread T0
    #0 0x7ddaf5b70baa in _channelid_shared Modules/_interpchannelsmodule.c:2643
    #1 0x7ddaf5b70baa in _channelend_shared Modules/_interpchannelsmodule.c:2799
    #2 0x56723fa7ae6a in _get_xidata Python/crossinterp.c:502
    #3 0x56723fa7f779 in _sharednsitem_set_value Python/crossinterp.c:2076
    #4 0x56723fa7f779 in _sharednsitem_copy_from_ns Python/crossinterp.c:2121
    #5 0x56723fa7f779 in _fill_sharedns Python/crossinterp.c:2385
    #6 0x56723fa854a3 in _PyXI_Enter Python/crossinterp.c:2639
    #7 0x7ddaf5b58caf in _run_in_interpreter Modules/_interpretersmodule.c:674
    #8 0x7ddaf5b5a528 in _interpreters_run_string_impl Modules/_interpretersmodule.c:1202
    #9 0x7ddaf5b5a528 in _interpreters_run_string Modules/clinic/_interpretersmodule.c.h:531
    #10 0x56723f6a93e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #11 0x56723f6a93e7 in PyObject_Vectorcall Objects/call.c:327
    #12 0x56723f55d5a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #13 0x56723fa27ad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #14 0x56723fa27ad6 in _PyEval_Vector Python/ceval.c:2001
    #15 0x56723fa27ad6 in PyEval_EvalCode Python/ceval.c:884
    #16 0x56723fb6d16e in run_eval_code_obj Python/pythonrun.c:1365
    #17 0x56723fb6d16e in run_mod Python/pythonrun.c:1459
    #18 0x56723fb71e17 in pyrun_file Python/pythonrun.c:1293
    #19 0x56723fb71e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #20 0x56723fb7293c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #21 0x56723fbe5e3c in pymain_run_file_obj Modules/main.c:410
    #22 0x56723fbe5e3c in pymain_run_file Modules/main.c:429
    #23 0x56723fbe5e3c in pymain_run_python Modules/main.c:691
    #24 0x56723fbe771e in Py_RunMain Modules/main.c:772
    #25 0x56723fbe771e in pymain_main Modules/main.c:802
    #26 0x56723fbe771e in Py_BytesMain Modules/main.c:826
    #27 0x7ddaf602a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #28 0x7ddaf602a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #29 0x56723f581634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)

0x50300001af70 is located 0 bytes after 32-byte region [0x50300001af50,0x50300001af70)
allocated by thread T0 here:
    #0 0x7ddaf64fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x56723f84e88e in _PyObject_MallocWithType Include/internal/pycore_object_alloc.h:46
    #2 0x56723f84e88e in _PyType_AllocNoTrack Objects/typeobject.c:2504
    #3 0x56723f84eaf4 in PyType_GenericAlloc Objects/typeobject.c:2535
    #4 0x56723f846118 in type_call Objects/typeobject.c:2448
    #5 0x56723f6a79cd in _PyObject_MakeTpCall Objects/call.c:242
    #6 0x56723f565f33 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #7 0x56723fa27ad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #8 0x56723fa27ad6 in _PyEval_Vector Python/ceval.c:2001
    #9 0x56723fa27ad6 in PyEval_EvalCode Python/ceval.c:884
    #10 0x56723fb6d16e in run_eval_code_obj Python/pythonrun.c:1365
    #11 0x56723fb6d16e in run_mod Python/pythonrun.c:1459
    #12 0x56723fb71e17 in pyrun_file Python/pythonrun.c:1293
    #13 0x56723fb71e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #14 0x56723fb7293c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #15 0x56723fbe5e3c in pymain_run_file_obj Modules/main.c:410
    #16 0x56723fbe5e3c in pymain_run_file Modules/main.c:429
    #17 0x56723fbe5e3c in pymain_run_python Modules/main.c:691
    #18 0x56723fbe771e in Py_RunMain Modules/main.c:772
    #19 0x56723fbe771e in pymain_main Modules/main.c:802
    #20 0x56723fbe771e in Py_BytesMain Modules/main.c:826
    #21 0x7ddaf602a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #22 0x7ddaf602a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #23 0x56723f581634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)

SUMMARY: AddressSanitizer: heap-buffer-overflow Modules/_interpchannelsmodule.c:2643 in _channelid_shared
Shadow bytes around the buggy address:
  0x50300001ac80: fd fd fd fd fa fa fd fd fd fd fa fa fd fd fd fd
  0x50300001ad00: fa fa fd fd fd fd fa fa fd fd fd fd fa fa fd fd
  0x50300001ad80: fd fd fa fa fd fd fd fd fa fa fd fd fd fd fa fa
  0x50300001ae00: fd fd fd fd fa fa fd fd fd fd fa fa fd fd fd fd
  0x50300001ae80: fa fa fd fd fd fd fa fa 00 00 00 00 fa fa fd fd
=>0x50300001af00: fd fd fa fa 00 00 00 00 fa fa 00 00 00 00[fa]fa
  0x50300001af80: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00
  0x50300001b000: fa fa 00 00 00 fa fa fa 00 00 00 00 fa fa 00 00
  0x50300001b080: 00 00 fa fa fd fd fd fa fa fa fd fd fd fd fa fa
  0x50300001b100: fd fd fd fa fa fa fd fd fd fa fa fa fd fd fd fd
  0x50300001b180: fa fa fd fd fd fd fa fa 00 00 00 00 fa fa 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==2202758==ABORTING
CPython versions tested on:
Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) Exception 1
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] Exception 1
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 16:56:16) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0] ASAN 1
Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0]

Linked PRs
  • gh-146574

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in Modules/_interpchannelsmodule.c at _channelend_shared and _channelid_shared, then trace the _get_xidata path in Python/crossinterp.c. Reproduce the supplied proof of concept under ASAN and verify that an invalid _id is rejected without the heap-buffer-overflow; add regression coverage for this case.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.