Skip to content

Null pointer dereference in _tkinter AsObj via re-entrant __str__ #143310

Closed

Description

@jackfromeast

What happened?

Passing a list that contains an object whose __str__ clears the list lets _tkinter’s AsObj recurse through PyObject_Str, which mutates the list while the outer loop still walks it. The subsequent PySequence_Fast_GET_ITEM hits a NULL slot and crashes the interpreter.

Proof of Concept:

import tkinter

interp = tkinter.Tcl()

victim = []

class Evil:
    def __str__(self):
        victim.clear()
        return "boom"

victim[:] = [Evil(), "pad"]
interp.call('set', 'var', victim)

Affected Versions

Details
Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) ASAN 1
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] ASAN 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, 16:57:16) [Clang 18.1.3 (1ubuntu1)] ASAN 1

Vulnerable Code

Details
/* Buggy Re-entrant Path */
static PyObject *
Tkapp_Call(PyObject *selfptr, PyObject *args)
{
    Tcl_Obj *objStore[ARGSZ];
    Tcl_Obj **objv = NULL;
    Tcl_Size objc;

    /* ... */

    objv = Tkapp_CallArgs(args, objStore, &objc);
    if (!objv) {
        return NULL;
    }

    /* ... */
}

static Tcl_Obj**
Tkapp_CallArgs(PyObject *args, Tcl_Obj** objStore, Tcl_Size *pobjc)
{
    Tcl_Obj **objv = objStore;
    Py_ssize_t objc = 0, i;

    /* ... */

    for (i = 0; i < objc; i++) {
        PyObject *v = PySequence_Fast_GET_ITEM(args, i);
        objv[i] = AsObj(v);
        /* ... */
    }
    /* ... */
}

static Tcl_Obj*
AsObj(PyObject *value)
{
    Tcl_Obj *result;

    /* ... */

    if (PyTuple_Check(value) || PyList_Check(value)) {
        Tcl_Obj **argv;
        Py_ssize_t size, i;

        size = PySequence_Fast_GET_SIZE(value);
        /* crashing pointer derived: ((PyListObject *)value)->ob_item */

        argv = (Tcl_Obj **) PyMem_Malloc(((size_t)size) * sizeof(Tcl_Obj *));
        if (!argv) {
            PyErr_NoMemory();
            return NULL;
        }
        for (i = 0; i < size; i++) {
            argv[i] = AsObj(PySequence_Fast_GET_ITEM(value, i));  /* Crash site */
        }
        result = Tcl_NewListObj((int)size, argv);
        PyMem_Free(argv);
        return result;
    }

    /* ... */

    {
        PyObject *v = PyObject_Str(value);  /* Reentrant call site */
        if (!v)
            return 0;
        result = AsObj(v);
        Py_DECREF(v);
        return result;
    }
}

/* Clobbering Path */
static void
list_clear_impl(PyListObject *a, bool is_resize)
{
    PyObject **items = a->ob_item;
    if (items == NULL) {
        return;
    }

    /* ... */

    Py_ssize_t i = Py_SIZE(a);
    Py_SET_SIZE(a, 0);
    FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item, NULL);  /* state mutate site */
    a->allocated = 0;
    while (--i >= 0) {
        Py_XDECREF(items[i]);
    }
    /* ... */
}

static PyObject *
py_list_clear_impl(PyListObject *self)
{
    list_clear(self);
    Py_RETURN_NONE;
}

Sanitizer Output

Details
=================================================================
==1649088==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x7bdb62a40dc6 bp 0x7fffc6b37870 sp 0x7fffc6b377a0 T0)
==1649088==The signal is caused by a READ memory access.
==1649088==Hint: address points to the zero page.
    #0 0x7bdb62a40dc6 in AsObj /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/./Modules/_tkinter.c:1011:27
    #1 0x7bdb62a3f9a8 in Tkapp_CallArgs /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/./Modules/_tkinter.c:1325:23
    #2 0x7bdb62a3237d in Tkapp_Call /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/./Modules/_tkinter.c:1509:16
    #3 0x5bcab7d8cfdb in cfunction_call /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Objects/methodobject.c:575:18
    #4 0x5bcab7bc4b81 in _PyObject_MakeTpCall /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Objects/call.c:242:18
    #5 0x5bcab81c4c62 in _PyEval_EvalFrameDefault /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/generated_cases.c.h:1620:35
    #6 0x5bcab8193bf4 in _PyEval_Vector /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/ceval.c:2005:12
    #7 0x5bcab8193bf4 in PyEval_EvalCode /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/ceval.c:888:21
    #8 0x5bcab84584d4 in run_mod /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/pythonrun.c:1459:19
    #9 0x5bcab845202d in pyrun_file /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/pythonrun.c:1293:15
    #10 0x5bcab844f2d3 in _PyRun_SimpleFileObject /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/pythonrun.c:521:13
    #11 0x5bcab844e89e in _PyRun_AnyFileObject /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Python/pythonrun.c:81:15
    #12 0x5bcab8513b13 in pymain_run_file_obj /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Modules/main.c:410:15
    #13 0x5bcab8513b13 in pymain_run_file /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Modules/main.c:429:15
    #14 0x5bcab8510bcb in pymain_run_python /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Modules/main.c:691:21
    #15 0x5bcab8510bcb in Py_RunMain /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Modules/main.c:772:5
    #16 0x5bcab85127fb in pymain_main /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Modules/main.c:802:12
    #17 0x5bcab8512aa2 in Py_BytesMain /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/Modules/main.c:826:12
    #18 0x7bdb6622a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #19 0x7bdb6622a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #20 0x5bcab78cf114 in _start (/home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/python+0x6b0114) (BuildId: 0aee20a59f1c25de22733bd0e5f8259ab04406c4)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/./Modules/_tkinter.c:1011:27 in AsObj
==1649088==ABORTING

Linked PRs

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Dec 31, 2025
  2. picnixz commented on Dec 31, 2025

    @picnixz
    Member

    @jackfromeast Could you please use the crash template when you open your issues instead of the blank one please?

  3. self-assigned this
    on Dec 31, 2025
  4. picnixz commented on Dec 31, 2025

    @picnixz
    Member

    Hum. The question I ask myself is: should we continue making it grow or not? we are actually building an array of Tcl_Obj, but we pre-compute the list's size so we have some solutions:

    • Copy the content of the list in an internal tuple and use the tuple from there. I can optimize this path for exact lists or tuples that are known to be safe, but otherwise I'll need to call tuple(object) first and work on that tuple.
    • Resize the allocated array when needed. It's a bit more flexible and only requires C resizes and size checks.
    • Only use whatever I could copy, e.g., if we call clear() in the middle, then just copy the initial objects. Or, if the list object changes its size during iteration and we get an index out of bound, the just raise that IndexError. If the list grows, don't copy past what we already knew. And if the list is mutated, use the "old" item value that was inserted.

    The easiest for me would be to copy the content of the list into a temporary tuple and work with it.

    @serhiy-storchaka what are your thoughts on this one?

  5. terryjreedy commented on Dec 31, 2025

    @terryjreedy
    Member

    Without tkinter involved:

    victim = []
    
    class Evil:
        def __str__(self):
            victim.clear()
            return "boom"
    
    victim[:] = ['before', Evil(), 'after']
    for s in victim: print(s)
    

    prints

    before
    boom
    

    Replacing the last line with

    result = [s for s in victim]
    print(result)
    

    seems more line the OP's code. It changes the result to ['before', <__main__.Evil object at 0x000001D43FCD9A90>, 'after']. I am not sure why the difference, but I expect a tuple copy would result in something equivalent.

  6. picnixz commented on Dec 31, 2025

    @picnixz
    Member

    I think, I'll go for a tuple copy but only if we don't provide an exact list or tuple. Most of the time, people will just use plain lists or tuples and not their custom sequences.

  7. serhiy-storchaka commented on Jan 1, 2026

    @serhiy-storchaka
    Member

    I knew about this bug as soon as I committed list support. It just looked too insignificant and was too low on my priority list. But if it has been discovered by third party, there is no reason to not fix it.

    It does not matter how you fix it. You convert all list elements to Tcl objects, so making an intermediate tuple copy (which will just create new references) will not make a difference.

  8. added
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Jan 1, 2026
  9. added 3 commits that reference this issue on Jan 1, 2026
  10. added 2 commits that reference this issue on Jan 2, 2026
  11. picnixz commented on Jan 2, 2026

    @picnixz
    Member

    Thank you for the report!

  12. added 2 commits that reference this issue on Jan 2, 2026
  13. added a commit that references this issue on Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.13only security fixes3.14bugs and security fixes3.15bugs and security fixesextension-modulesC modules in the Modules dirtopic-tkintertype-crashA hard crash of the interpreter, possibly with a core dump

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions