]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-80937: Fix memory leak in tkinter createcommand (GH-152294) (GH-152329)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 26 Jun 2026 19:15:14 +0000 (21:15 +0200)
committerGitHub <noreply@github.com>
Fri, 26 Jun 2026 19:15:14 +0000 (19:15 +0000)
A command created with createcommand() held a strong reference to the
interpreter, forming an uncollectable cycle (interpreter -> command ->
interpreter) that kept the interpreter and the callback alive until the
command was removed with deletecommand() or destroy().  The command now
borrows the reference; it cannot outlive the interpreter, which deletes its
commands when finalized.
(cherry picked from commit bbf7786a904e558a15d01475356167e29b2e3708)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Lib/test/test_tkinter/test_misc.py
Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst [new file with mode: 0644]
Modules/_tkinter.c

index c2d64d63d04a1b23e5c8112237a81ca45b176fed..b37aa4d1606ccc717c171f7b59445d561dbc760f 100644 (file)
@@ -1,5 +1,6 @@
 import functools
 import unittest
+import weakref
 import tkinter
 from tkinter import TclError
 import enum
@@ -333,6 +334,17 @@ class MiscTest(AbstractTkTest, unittest.TestCase):
         self.root.deletecommand(name)
         self.assertRaises(TclError, self.root.tk.call, name)
 
+    def test_createcommand_no_leak(self):
+        # gh-80937: dropping the interpreter must release a command's callback,
+        # even without an explicit deletecommand().
+        interp = tkinter.Tcl()
+        callback = lambda: ''
+        ref = weakref.ref(callback)
+        interp.tk.createcommand('cb', callback)
+        del callback, interp
+        support.gc_collect()
+        self.assertIsNone(ref())
+
     def test_option(self):
         self.addCleanup(self.root.option_clear)
         self.root.option_add('*Button.background', 'red')
diff --git a/Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst b/Misc/NEWS.d/next/Library/2026-06-26-13-10-00.gh-issue-80937.Hq3mNp.rst
new file mode 100644 (file)
index 0000000..4ea0179
--- /dev/null
@@ -0,0 +1,4 @@
+Fix a memory leak in :mod:`tkinter` when a Tcl command created with
+``createcommand`` was not explicitly removed before the interpreter was
+deleted.  The command no longer keeps the interpreter alive through a
+reference cycle.
index 718b378a63b0fa03063fb641cadda67f69a9af44..38b0b101c9b935ceec17dd3c3e0273d6336798ba 100644 (file)
@@ -2406,7 +2406,7 @@ PythonCmdDelete(ClientData clientData)
     PythonCmd_ClientData *data = (PythonCmd_ClientData *)clientData;
 
     ENTER_PYTHON
-    Py_XDECREF(data->self);
+    /* data->self is borrowed. */
     Py_XDECREF(data->func);
     PyMem_Free(data);
     LEAVE_PYTHON
@@ -2475,7 +2475,9 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
     data = PyMem_NEW(PythonCmd_ClientData, 1);
     if (!data)
         return PyErr_NoMemory();
-    Py_INCREF(self);
+    /* Borrow the interpreter: a strong reference would form an uncollectable
+       cycle (interp -> command -> data->self -> interp) and leak the command
+       (gh-80937).  The command cannot outlive the interpreter. */
     data->self = self;
     data->func = Py_NewRef(func);
     if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
@@ -2506,6 +2508,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
     }
     if (err) {
         PyErr_SetString(Tkinter_TclError, "can't create Tcl command");
+        Py_DECREF(data->func);
         PyMem_Free(data);
         return NULL;
     }