Skip to content

Legacy-mode subinterpreters in Python 3.12: import _tkinter leads to shutdown crash  #115649

Closed
@car-bianco

Description

@car-bianco

Bug report

Bug description:

Hello everyone,

I am working on an application embedding multiple Python subinterpreters - for which the Python version should be upgraded from Python 3.10 to Python 3.12.1. For the time being, the "legacy version" of subinterpreters (i.e., using a global, shared GIL) should be used, since all Python extensions (including _tkinter and all extensions with single-phase initialization) should be supported.

If I understand the docs correctly, using the legacy Py_NewInterpreter() method should preserve the existing behavior. Still, the following application crashes at shutdown:

#include <Python.h>

class PythonInterpreter {
 public:
    PythonInterpreter(PyThreadState* parent, int id)
    : mParent(parent)
    , mId(id) {
        PyEval_RestoreThread(mParent);
        mThread = Py_NewInterpreter();
        PyObject* globals = PyModule_GetDict(PyImport_AddModule("__main__"));
        PyRun_String("import _tkinter", Py_single_input, globals, globals);
        PyEval_SaveThread();
        std::cout << "Subinterpreter " << id << " done" << std::endl;
    }

    virtual ~PythonInterpreter() {
        std::cout << "destructor " << mId << std::endl;
        PyThreadState_Swap(mThread);
        Py_EndInterpreter(mThread);
    }

 private:
    PyThreadState* mParent;
    PyThreadState* mThread;
    int mId;
};

int main(int /* argc */, char** /* argv[] */) {
    PyConfig config;
    PyConfig_InitPythonConfig(&config);
    PyStatus status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        PyConfig_Clear(&config);
        Py_ExitStatusException(status);
    } else {
        PyConfig_Clear(&config);
    }
    PyThreadState* s0 = PyThreadState_Get();
    PyEval_SaveThread();
    PythonInterpreter* i0 = new PythonInterpreter(s0, 0);
    PythonInterpreter* i1 = new PythonInterpreter(s0, 1);
    delete i0;
    delete i1;
    PyEval_RestoreThread(s0);
    Py_Finalize();
}

When compiling and running this program with a debug build of Python 3.12.1 (or later) on Linux, I get this output:

Subinterpreter 0 done
Subinterpreter 1 done
destructor 0
destructor 1
main: Objects/dictobject.c:283: unicode_get_hash: Assertion `Py_IS_TYPE(((PyObject*)(((o)))), (&PyUnicode_Type))' failed.

With a non-debug build, the program exits with a segmentation fault.

The gdb backtrace looks as follows:

#0  0x00007ffff6311387 in raise () from /lib64/libc.so.6
#1  0x00007ffff6312a78 in abort () from /lib64/libc.so.6
#2  0x00007ffff630a1a6 in __assert_fail_base () from /lib64/libc.so.6
#3  0x00007ffff630a252 in __assert_fail () from /lib64/libc.so.6
#4  0x00007ffff6a9baf5 in unicode_get_hash (o=<optimized out>) at Objects/dictobject.c:2143
#5  _PyDict_Next (op=op@entry=0x7fffecfd2270, ppos=ppos@entry=0x7fffffffd2a8, pkey=pkey@entry=0x7fffffffd2a0, pvalue=pvalue@entry=0x7fffffffd298, 
    phash=phash@entry=0x0) at Objects/dictobject.c:2142
#6  0x00007ffff6a9c0d8 in PyDict_Next (op=op@entry=0x7fffecfd2270, ppos=ppos@entry=0x7fffffffd2a8, pkey=pkey@entry=0x7fffffffd2a0, 
    pvalue=pvalue@entry=0x7fffffffd298) at Objects/dictobject.c:2189
#7  0x00007ffff6ab3751 in _PyModule_ClearDict (d=0x7fffecfd2270) at Objects/moduleobject.c:624
#8  0x00007ffff6ab40dd in _PyModule_Clear (m=m@entry=0x7fffed02ca70) at Objects/moduleobject.c:604
#9  0x00007ffff6c11bd4 in finalize_modules_clear_weaklist (interp=interp@entry=0x7fffed03c020, weaklist=weaklist@entry=0x7fffef912da0, verbose=verbose@entry=0)
    at Python/pylifecycle.c:1526
#10 0x00007ffff6c125ef in finalize_modules (tstate=tstate@entry=0x7fffed099950) at Python/pylifecycle.c:1609
#11 0x00007ffff6c202da in Py_EndInterpreter (tstate=0x7fffed099950) at Python/pylifecycle.c:2220
#12 0x00000000004014b5 in PythonInterpreter::~PythonInterpreter (this=0x5092f0, __in_chrg=<optimized out>) at main.cc:25
#13 PythonInterpreter::~PythonInterpreter (this=0x5092f0, __in_chrg=<optimized out>) at main.cc:26
#14 0x00000000004012e2 in main () at main.cc:60

Digging further into the backtrace, it looks like the Python garbage collector is trying to decrease the reference counter to the _tkinter module twice, despite it having been increased only once. Oddly enough, the program runs just fine when destroying the interpreters in reverse order:

    PythonInterpreter* i0 = new PythonInterpreter(s0, 0);
    PythonInterpreter* i1 = new PythonInterpreter(s0, 1);
    delete i1;
    delete i0;

Can anyone help me shed some light into this issue? Is there anything I am overlooking?

CPython versions tested on:

3.12.1, 3.12.2, 3.13.0a4

Operating systems tested on:

Linux, Windows

### Tasks

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions