From fdb095320351c85233ec61b171190cf2018c2008 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 15 Aug 2026 22:07:30 +0300 Subject: [PATCH] gh-155864: Keep the module's current screen in step with use_screen() 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. --- Lib/test/test_curses.py | 27 +++++++++++++++++++++++++++ Modules/_cursesmodule.c | 5 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index d87374a298fc33..bf0b5c4b885a27 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 383de378670ea9..006e27d55d8925 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; }