https://github.com/python/cpython/commit/f10166035d602da5052e8a48f9d5c216c57b401d
commit: f10166035d602da5052e8a48f9d5c216c57b401d
branch: main
author: Vyron Vasileiadis <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-16T10:45:05+03:00
summary:

gh-155864: Keep the module's current screen in step with use_screen() 
(GH-155865)

screen.use() makes its screen current for the callback, but the module
kept recording the previously current screen, so newwin(), newpad() and
getwin() tagged the new window with the wrong owner.  The window then
failed to keep its own screen alive: the screen could be freed while the
window was still in use, and the next call on it read freed memory.
initscr() inside use() returned the other screen's standard window for
the same reason.

files:
M Lib/test/test_curses.py
M Modules/_cursesmodule.c

diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 08afa587f041954..f582336fae17344 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -3056,6 +3056,33 @@ def test_window_keeps_screen_alive(self):
         win.addstr(0, 0, 'still alive')
         win.refresh()
 
+    @unittest.skipUnless(hasattr(curses.screen, 'use'),
+                         'requires curses.screen.use()')
+    def test_window_made_in_use_keeps_its_screen_alive(self):
+        # use() makes its screen current for the callback, so a window created
+        # there belongs to that screen and must keep it alive, not the screen
+        # that was current before.
+        s = self.make_pty()
+        s2 = self.make_pty()
+        a = curses.newterm('xterm', s, s)
+        b = curses.newterm('xterm', s2, s2)   # current screen is b
+        win = a.use(lambda scr: curses.newwin(3, 3))
+        del a
+        gc_collect()
+        win.addstr(0, 0, 'x')
+        b.stdscr.refresh()
+
+    @unittest.skipUnless(hasattr(curses.screen, 'use'),
+                         'requires curses.screen.use()')
+    def test_initscr_in_use_returns_its_screen(self):
+        # initscr() returns the standard window of the current screen, and
+        # inside use() that is the used screen.
+        s = self.make_pty()
+        s2 = self.make_pty()
+        a = curses.newterm('xterm', s, s)
+        b = curses.newterm('xterm', s2, s2)   # current screen is b
+        self.assertIs(a.use(lambda scr: curses.initscr()), a.stdscr)
+
     def test_screen_freed(self):
         # Dropping all references to a (non-current) screen and its windows
         # frees it without error.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 383de378670ea97..006e27d55d8925d 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -5303,8 +5303,8 @@ static PyObject *
 PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs)
 {
     PyCursesScreenObject *so = _PyCursesScreenObject_CAST(self);
+    cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
     if (so->screen == NULL) {
-        cursesmodule_state *state = 
get_cursesmodule_state_by_cls(Py_TYPE(self));
         PyErr_SetString(state->error, "the screen has been deleted");
         return NULL;
     }
@@ -5313,7 +5313,10 @@ PyCursesScreen_use(PyObject *self, PyObject *args, 
PyObject *kwargs)
         return NULL;
     }
     curses_use_data data = {self, func, extra, kwargs, NULL};
+    PyObject *prev = state->topscreen;
+    state->topscreen = Py_NewRef(self);
     use_screen(so->screen, curses_use_screen_cb, &data);
+    Py_SETREF(state->topscreen, prev);
     Py_DECREF(extra);
     return data.result;
 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to