Skip to content

Commit 2a3189c

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.15] gh-80937: Fix memory leak in tkinter createcommand (GH-152294) (GH-152327)
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 bbf7786) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 073b658 commit 2a3189c

3 files changed

Lines changed: 21 additions & 2 deletions

File tree

Lib/test/test_tkinter/test_misc.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import collections.abc
22
import functools
33
import unittest
4+
import weakref
45
import tkinter
56
from tkinter import TclError
67
import enum
@@ -348,6 +349,17 @@ def callback():
348349
self.root.deletecommand(name)
349350
self.assertRaises(TclError, self.root.tk.call, name)
350351

352+
def test_createcommand_no_leak(self):
353+
# gh-80937: dropping the interpreter must release a command's callback,
354+
# even without an explicit deletecommand().
355+
interp = tkinter.Tcl()
356+
callback = lambda: ''
357+
ref = weakref.ref(callback)
358+
interp.tk.createcommand('cb', callback)
359+
del callback, interp
360+
support.gc_collect()
361+
self.assertIsNone(ref())
362+
351363
def test_option(self):
352364
self.addCleanup(self.root.option_clear)
353365
self.root.option_add('*Button.background', 'red')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a memory leak in :mod:`tkinter` when a Tcl command created with
2+
``createcommand`` was not explicitly removed before the interpreter was
3+
deleted. The command no longer keeps the interpreter alive through a
4+
reference cycle.

Modules/_tkinter.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2464,7 +2464,7 @@ PythonCmdDelete(ClientData clientData)
24642464
PythonCmd_ClientData *data = (PythonCmd_ClientData *)clientData;
24652465

24662466
ENTER_PYTHON
2467-
Py_XDECREF(data->self);
2467+
/* data->self is borrowed. */
24682468
Py_XDECREF(data->func);
24692469
PyMem_Free(data);
24702470
LEAVE_PYTHON
@@ -2533,7 +2533,9 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25332533
data = PyMem_NEW(PythonCmd_ClientData, 1);
25342534
if (!data)
25352535
return PyErr_NoMemory();
2536-
Py_INCREF(self);
2536+
/* Borrow the interpreter: a strong reference would form an uncollectable
2537+
cycle (interp -> command -> data->self -> interp) and leak the command
2538+
(gh-80937). The command cannot outlive the interpreter. */
25372539
data->self = self;
25382540
data->func = Py_NewRef(func);
25392541
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
@@ -2566,6 +2568,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
25662568
}
25672569
if (err) {
25682570
PyErr_SetString(Tkinter_TclError, "can't create Tcl command");
2571+
Py_DECREF(data->func);
25692572
PyMem_Free(data);
25702573
return NULL;
25712574
}

0 commit comments

Comments
 (0)