]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626) (GH...
authorSerhiy Storchaka <storchaka@gmail.com>
Thu, 2 Jul 2026 19:26:34 +0000 (22:26 +0300)
committerGitHub <noreply@github.com>
Thu, 2 Jul 2026 19:26:34 +0000 (19:26 +0000)
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().
(cherry picked from commit 6f103fab178c07cbb5f701b8ad97e275b6eb6c4c)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Lib/test/test_turtle.py
Lib/turtle.py
Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst [new file with mode: 0644]

index a572ff4b9e91ccc4762ae45bcd48389dd7ebd3d9..a93557195fb8e424c2bde624f8e935c560eb040b 100644 (file)
@@ -490,6 +490,30 @@ class TestTPen(unittest.TestCase):
             self.assertTrue(tpen.isdown())
 
 
+class TestTurtleScreen(unittest.TestCase):
+    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):
         with patch_screen():
index a94ec10ea7460d06642d7ff399c3bc2f97647d96..173cfc50b579daf7bd09e4f8cb56f14252180583 100644 (file)
@@ -483,6 +483,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)
@@ -552,7 +553,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 (file)
index 0000000..96a0938
--- /dev/null
@@ -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.