From: Serhiy Storchaka Date: Tue, 30 Jun 2026 09:19:43 +0000 (+0300) Subject: gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626) X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=6f103fab178c07cbb5f701b8ad97e275b6eb6c4c;p=thirdparty%2FPython%2Fcpython.git gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626) TurtleScreenBase._update() redraws with cv.update(), which also reprocesses input events, so a handler that moves the turtle (such as screen.ondrag(turtle.goto)) reenters _update() for every queued event until the interpreter crashes. A reentrant _update() now only flushes drawing with update_idletasks(). Co-authored-by: Claude Opus 4.8 --- diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 12d2eed87414..c49ce9cdb6f6 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -577,6 +577,28 @@ class TestTurtleScreen(unittest.TestCase): s.update.assert_not_called() s.update.assert_called_once() + def test_update_is_not_reentrant(self): + # ondrag(goto) reenters _update() while cv.update() processes events; + # without a guard this recurses without bound (gh-50966). + s = turtle.TurtleScreen(cv=unittest.mock.MagicMock()) + depth = max_depth = 0 + + def reenter(): + nonlocal depth, max_depth + depth += 1 + max_depth = max(max_depth, depth) + if depth < 50: + s._update() # as an event handler would + depth -= 1 + + s.cv.update.reset_mock() # ignore calls made during construction + s.cv.update.side_effect = reenter + s._update() + # cv.update() runs once; reentrant calls only flush idle tasks. + self.assertEqual(s.cv.update.call_count, 1) + self.assertEqual(max_depth, 1) + self.assertTrue(s.cv.update_idletasks.called) + class TestTurtle(unittest.TestCase): def setUp(self): diff --git a/Lib/turtle.py b/Lib/turtle.py index ae5be9fe7039..af2c7448a0af 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -486,6 +486,7 @@ class TurtleScreenBase(object): self.canvwidth = w self.canvheight = h self.xscale = self.yscale = 1.0 + self._updating = False def _createpoly(self): """Create an invisible polygon item on canvas self.cv) @@ -555,7 +556,16 @@ class TurtleScreenBase(object): def _update(self): """Redraw graphics items on canvas """ - self.cv.update() + if self._updating: + # Reentrant call (e.g. a drag handler moving the turtle, + # gh-50966): flush drawing without reprocessing input. + self.cv.update_idletasks() + return + self._updating = True + try: + self.cv.update() + finally: + self._updating = False def _delay(self, delay): """Delay subsequent canvas actions for delay ms.""" diff --git a/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst new file mode 100644 index 000000000000..96a0938c02d2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst @@ -0,0 +1,3 @@ +Fix unbounded recursion in :mod:`turtle` when a mouse event handler that moves +the turtle is reentered while the screen is being redrawn, for example with +``screen.ondrag(turtle.goto)``. This could previously crash the interpreter.