python / python/cpython

Null pointer dereference in `array_repeat` via re-entrant `__index__` during array multiplication

Open
#142,883 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

[!IMPORTANT]
This issue is not for newcomers.

What happened?

A custom __index__ can swap the MRO of an array.array subclass mid multiplication, so array_repeat calls find_array_state_by_type on a type without the array module and receives a NULL module state. The subsequent dereference of state->ArrayType then hits freed memory and triggers a null pointer read.

Proof of Concept:

import array

class Good(array.array):
    pass

class Hide(type):
    def mro(cls):
        return (cls, object)

class Bad(Good, metaclass=Hide):
    pass

arr = Good('b', b'x')

class Count:
    def __index__(self):
        arr.__class__ = Bad     # Drop array.array from the MRO mid-op
        return 2

arr * Count()

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 27 2025, 21:34:13) OK 0
Python 3.10.19+ (heads/3.10:014261980b1, Oct 27 2025, 21:19:00) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 27 2025, 21:20:35) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 27 2025, 21:27:07) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 27 2025, 21:28:49) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.14.0+ (heads/3.14:2e216728038, Oct 27 2025, 21:30:55) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 27 2025, 21:32:37) [Clang 18.1.3 (1ubuntu1)] ASAN 1

Related Code Snippet

#define find_array_state_by_type(tp) \
      (get_array_state(PyType_GetModuleByDef(tp, &arraymodule)))   // requires array.array in the MRO

static array_state *
get_array_state(PyObject *module)
{
    return (array_state *)_PyModule_GetState(module);            // assumes module is non-NULL
}

static PyObject *
array_repeat(PyObject *op, Py_ssize_t n)
{
    arrayobject *a = arrayobject_CAST(op);
    array_state *state = find_array_state_by_type(Py_TYPE(a));

    if (n < 0)
        n = 0;
    const Py_ssize_t array_length = Py_SIZE(a);
    if ((array_length != 0) && (n > PY_SSIZE_T_MAX / array_length)) {
        return PyErr_NoMemory();
    }
    Py_ssize_t size = array_length * n;
    arrayobject* np = (arrayobject *) newarrayobject(state->ArrayType, size, a->ob_descr);
    if (np == NULL)
        return NULL;
    if (size == 0)
        return (PyObject *)np;

    const Py_ssize_t oldbytes = array_length * a->ob_descr->itemsize;
    const Py_ssize_t newbytes = oldbytes * n;
    _PyBytes_Repeat(np->ob_item, newbytes, a->ob_item, oldbytes);

    return (PyObject *)np;
}

Sanitizer Report

=================================================================
==105120==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x77b5659592d8 bp 0x7ffe28c17cf0 sp 0x7ffe28c17cf0 T0)
==105120==The signal is caused by a READ memory access.
==105120==Hint: address points to the zero page.
    #0 0x77b5659592d8 in _Py_TYPE Include/object.h:277
    #1 0x77b5659592d8 in Py_IS_TYPE Include/object.h:302
    #2 0x77b5659592d8 in PyObject_TypeCheck Include/object.h:432
    #3 0x77b565959317 in _PyModule_GetState Include/internal/pycore_moduleobject.h:38
    #4 0x77b56595936b in get_array_state Modules/arraymodule.c:75
    #5 0x77b56595f09d in array_repeat Modules/arraymodule.c:970
    #6 0x584ac280512b in wrap_indexargfunc Objects/typeobject.c:9651
    #7 0x584ac270d606 in wrapperdescr_raw_call Objects/descrobject.c:532
    #8 0x584ac270df48 in wrapperdescr_call Objects/descrobject.c:570
    #9 0x584ac26eec71 in _PyObject_MakeTpCall Objects/call.c:242
    #10 0x584ac2801605 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #11 0x584ac2801680 in vectorcall_unbound Objects/typeobject.c:3033
    #12 0x584ac281def4 in vectorcall_maybe Objects/typeobject.c:3130
    #13 0x584ac2821819 in slot_nb_multiply Objects/typeobject.c:10397
    #14 0x584ac26b8613 in binary_op1 Objects/abstract.c:966
    #15 0x584ac26bcc64 in PyNumber_Multiply Objects/abstract.c:1170
    #16 0x584ac29638f8 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:62
    #17 0x584ac29b0e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #18 0x584ac29b1148 in _PyEval_Vector Python/ceval.c:2001
    #19 0x584ac29b13f8 in PyEval_EvalCode Python/ceval.c:884
    #20 0x584ac2aa8507 in run_eval_code_obj Python/pythonrun.c:1365
    #21 0x584ac2aa8723 in run_mod Python/pythonrun.c:1459
    #22 0x584ac2aa957a in pyrun_file Python/pythonrun.c:1293
    #23 0x584ac2aac220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #24 0x584ac2aac4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #25 0x584ac2afd74d in pymain_run_file_obj Modules/main.c:410
    #26 0x584ac2afd9b4 in pymain_run_file Modules/main.c:429
    #27 0x584ac2aff1b2 in pymain_run_python Modules/main.c:691
    #28 0x584ac2aff842 in Py_RunMain Modules/main.c:772
    #29 0x584ac2affa2e in pymain_main Modules/main.c:802
    #30 0x584ac2affdb3 in Py_BytesMain Modules/main.c:826
    #31 0x584ac2583645 in main Programs/python.c:15
    #32 0x77b56622a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #33 0x77b56622a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #34 0x584ac2583574 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: 202d5dbb945f6d5f5a66ad50e2688d56affd6ecb)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV Include/object.h:277 in _Py_TYPE
==105120==ABORTING
Linked PRs
  • gh-143776

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 with Modules/arraymodule.c, especially array_repeat and the find_array_state_by_type path, then run the supplied re-entrant index reproducer under the sanitizer configuration. Review linked PR gh-143776 for the current direction; done means the reproducer no longer triggers the reported sanitizer failure and relevant tests pass.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.