python / python/cpython

CPython UaF during index callbacks

Open
#144,128 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

3.13 3.14 3.15 extension-modules pending type-crash
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

Bug report

Bug description:

The _PyNumber_Index, PyFloat_AsDouble and PyNumber_Long functions use a borrowed reference for error formatting after calling Python slots (index/float), but the slot's frame cleanup can free the object before the error is formatted.

A use-after-free vulnerability exists in CPython's type conversion functions (_PyNumber_Index, PyFloat_AsDouble and PyNumber_Long) where a borrowed reference is used for error message formatting after a Python callback that can free the referenced object.
For example, the vulnerable code pattern could be observed in _PyNumber_Index implementation (Objects/abstract.c):

  PyObject *
  _PyNumber_Index(PyObject *item)    // item is a BORROWED reference
  {
      if (item == NULL) {
          return null_error();
      }

      if (PyLong_Check(item)) {
          return Py_NewRef(item);
      }
      if (!_PyIndex_Check(item)) {
          PyErr_Format(PyExc_TypeError,
                       "'%.200s' object cannot be interpreted "
                       "as an integer", Py_TYPE(item)->tp_name);
          return NULL;
      }

      // === PYTHON CODE EXECUTES HERE ===
      PyObject *result = Py_TYPE(item)->tp_as_number->nb_index(item);
      // === item MAY NOW BE FREED ===

      if (!result || PyLong_CheckExact(result)) {
          return result;
      }

      if (!PyLong_Check(result)) {
          PyErr_Format(PyExc_TypeError,
                       "%T.__index__() must return an int, not %T",
                       item, result);  // <== UAF: item may be freed
          Py_DECREF(result);
          return NULL;
      }
      // ... similar pattern continues
  }

Similar patterns can be found in the PyFloat_AsDouble and PyNumber_Long functions.

The Python array.fromlist() method provides an ideal attack vector because it:

Iterates over a list using PyList_GET_ITEM() (returns borrowed reference)
Passes the borrowed reference directly to type-specific setitem functions
These functions call _PyNumber_Index() or PyFloat_AsDouble() for conversion.

Running with a python interpreter compiled with ASAN (./configure --with-address-sanitizer):

import array

class Evil:
    def __init__(self, lst):
        self.lst = lst

    def __index__(self):
        self.lst.clear()
        return "not an int"

lst = []
e = Evil(lst)
lst.append(e)
del e

arr = array.array('I')
arr.fromlist(lst)

You'll get the following:

==73525==ERROR: AddressSanitizer: heap-use-after-free on address 0x6130000300b8 at pc 0x0001013458a4 bp 0x00016ee396f0 sp 0x00016ee396e8
READ of size 8 at 0x6130000300b8 thread T0
    #0 0x0001013458a0 in unicode_from_format unicodeobject.c:3075
    #1 0x0001013436a8 in PyUnicode_FromFormatV unicodeobject.c:3109
    #2 0x00010154d5c8 in PyErr_Format errors.c:1243
    #3 0x0001010fe4e8 in _PyNumber_Index abstract.c:1433
    #4 0x000109338334 in II_setitem arraymodule.c:411
    #5 0x0001093427d4 in array_array_fromlist arraymodule.c.h:495
    #6 0x00010114aeb0 in _PyObject_VectorcallTstate pycore_call.h:136
    #7 0x0001014828ac in _Py_VectorCallInstrumentation_StackRefSteal ceval.c:1094
    #8 0x0001014b4718 in _PyEval_EvalFrameDefault generated_cases.c.h:1785
    #9 0x00010148180c in _PyEval_Vector ceval.c:2516
    #10 0x00010148105c in PyEval_EvalCode ceval.c:1005
    #11 0x000101646118 in run_mod pythonrun.c:1469
    #12 0x00010163e6a0 in _PyRun_SimpleFileObject pythonrun.c:518
    #13 0x00010163da30 in _PyRun_AnyFileObject pythonrun.c:81
    #14 0x0001016ac948 in pymain_run_file main.c:429
    #15 0x0001016aabcc in Py_RunMain main.c:772
    #16 0x0001016ab9fc in pymain_main main.c:802
    #17 0x0001016abba8 in Py_BytesMain main.c:826
    #18 0x00019e4edd50  (<unknown module>)
CPython versions tested on:

3.15

Operating systems tested on:

macOS

Linked PRs
  • gh-144138

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 Objects/abstract.c at _PyNumber_Index, PyFloat_AsDouble, and PyNumber_Long, then inspect the arraymodule.c call path and reproduce with the ASAN example. Compare the related handling and tests in the current tree; done means the reported use-after-free is addressed across the named conversion functions and the ASAN reproducer no longer fails.

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.