mirror of
https://github.com/python/cpython.git
synced 2026-07-26 20:03:16 -04:00
gh-83274: Don't crash when a Tcl interpreter is deallocated in the wrong thread (GH-152323)
Deallocating the interpreter from a thread other than the one it was created
in ran Tcl_DeleteInterp() there, which makes Tcl abort the process
("Tcl_AsyncDelete: async handler deleted by the wrong thread").
Tkapp_Dealloc() now leaks the interpreter in that case and reports a
RuntimeWarning instead.
Co-Authored-By: E. Paine <63801254+E-Paine@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import collections.abc
|
||||
import functools
|
||||
import platform
|
||||
import sys
|
||||
import textwrap
|
||||
import unittest
|
||||
import weakref
|
||||
import tkinter
|
||||
@@ -9,6 +10,7 @@ from tkinter import TclError
|
||||
import enum
|
||||
from test import support
|
||||
from test.support import os_helper
|
||||
from test.support.script_helper import assert_python_ok
|
||||
from test.test_tkinter.support import setUpModule # noqa: F401
|
||||
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
|
||||
requires_tk, get_tk_patchlevel,
|
||||
@@ -53,6 +55,33 @@ class MiscTest(AbstractTkTest, unittest.TestCase):
|
||||
b4 = Button2(f2)
|
||||
self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4)
|
||||
|
||||
def test_dealloc_in_wrong_thread(self):
|
||||
# gh-83274: deallocating the interpreter in the wrong thread must not
|
||||
# crash.
|
||||
script = textwrap.dedent("""
|
||||
import threading
|
||||
import tkinter
|
||||
root = tkinter.Tk()
|
||||
root.destroy()
|
||||
# Let another thread drop the last reference.
|
||||
ready = threading.Event()
|
||||
t = threading.Thread(target=lambda obj: ready.wait(), args=(root,))
|
||||
t.start()
|
||||
del root
|
||||
ready.set()
|
||||
t.join()
|
||||
print('ok')
|
||||
""")
|
||||
rc, out, err = assert_python_ok('-c', script)
|
||||
self.assertEqual(out.strip(), b'ok')
|
||||
if not support.Py_GIL_DISABLED:
|
||||
# On the free-threaded build the interpreter may instead be
|
||||
# deallocated in its own thread (deferred reference counting), so
|
||||
# the warning is not necessarily emitted. The crucial guarantee --
|
||||
# no crash -- is already checked by assert_python_ok() above.
|
||||
self.assertIn(b'RuntimeWarning', err)
|
||||
self.assertIn(b'gh-83274', err)
|
||||
|
||||
@requires_tk(8, 6, 6)
|
||||
def test_tk_busy(self):
|
||||
root = self.root
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Deallocating a :mod:`tkinter` application from a thread other than the one it
|
||||
was created in no longer crashes the interpreter. The underlying Tcl
|
||||
interpreter is leaked instead, and a :exc:`RuntimeWarning` is reported.
|
||||
+18
-4
@@ -3132,10 +3132,24 @@ Tkapp_Dealloc(PyObject *op)
|
||||
{
|
||||
TkappObject *self = TkappObject_CAST(op);
|
||||
PyTypeObject *tp = Py_TYPE(self);
|
||||
/*CHECK_TCL_APPARTMENT;*/
|
||||
ENTER_TCL
|
||||
Tcl_DeleteInterp(Tkapp_Interp(self));
|
||||
LEAVE_TCL
|
||||
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
|
||||
/* Deleting the interpreter from another thread aborts the process
|
||||
("Tcl_AsyncDelete: async handler deleted by the wrong thread").
|
||||
Leak it instead (gh-83274). */
|
||||
if (PyErr_WarnEx(PyExc_RuntimeWarning,
|
||||
"the Tcl interpreter is leaked because it was "
|
||||
"deallocated in a thread other than the one it was "
|
||||
"created in (see gh-83274)", 1) < 0)
|
||||
{
|
||||
PyErr_FormatUnraisable("Exception ignored while finalizing "
|
||||
"a Tcl interpreter");
|
||||
}
|
||||
}
|
||||
else {
|
||||
ENTER_TCL
|
||||
Tcl_DeleteInterp(Tkapp_Interp(self));
|
||||
LEAVE_TCL
|
||||
}
|
||||
Py_XDECREF(self->trace);
|
||||
PyObject_Free(self);
|
||||
Py_DECREF(tp);
|
||||
|
||||
Reference in New Issue
Block a user