From 23f58192c3b718677aa80af7714391058f75e687 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 27 Jul 2026 00:12:38 +0300 Subject: [PATCH 1/3] gh-154751: Keep the screen alive in curses.initscr() after newterm() When initscr() was called while a screen from newterm() was current, it returned a window created with no screen. The window therefore did not hold a reference to the screen it belonged to, so the screen could be collected while the window was still in use, and the next method call on the window read freed memory. It now passes the current screen, like newwin(), newpad() and getwin() already do. That is NULL for the screen created by initscr(), which has no screen object, so that case is unchanged. --- Lib/test/test_curses.py | 16 ++++++++++++++++ ...026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst | 3 +++ Modules/_cursesmodule.c | 6 +++++- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index ad5893e6754f68c..e285c4bddd0b342 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3072,6 +3072,22 @@ def test_new_prescr(self): del screen gc_collect() + def test_initscr_after_newterm_keeps_screen_alive(self): + # initscr() called while a newterm() screen is current returns a window + # for that screen, and the window must keep the screen alive. It used + # to be created without a screen, and using the window after the + # screen was collected read freed memory. + s1 = self.make_pty() + s2 = self.make_pty() + screen1 = curses.newterm('xterm', s1, s1) + screen2 = curses.newterm('xterm', s2, s2) + curses.set_term(screen1) + win = curses.initscr() + curses.set_term(screen2) + del screen1 + gc_collect() + win.addstr(0, 0, 'x') + @cpython_only def test_disallow_instantiation(self): # The screen type cannot be instantiated directly (bpo-43916). diff --git a/Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst b/Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst new file mode 100644 index 000000000000000..354447ca05d2bbe --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst @@ -0,0 +1,3 @@ +Fix a use-after-free in :mod:`curses`. :func:`curses.initscr` called while a +screen from :func:`curses.newterm` is current now returns a window that keeps +that screen alive, like :func:`curses.newwin` and :func:`curses.newpad` do. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index b2d745332317a36..aa06666a8201bbb 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -6573,7 +6573,11 @@ _curses_initscr_impl(PyObject *module) _curses_set_null_error(state, "wrefresh", "initscr"); return NULL; } - PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL, NULL); + /* Attach the current screen, like newwin(), newpad() and getwin() do, + so that the window keeps its screen alive. It is NULL for the + screen created by initscr(), which has no screen object. */ + PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL, + state->topscreen); if (winobj == NULL) { return NULL; } From 0cebe76b9f375b5172c7439ae1c3122b7a2c2854 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 27 Jul 2026 15:34:34 +0300 Subject: [PATCH 2/3] Apply suggestion from @serhiy-storchaka Co-authored-by: Serhiy Storchaka --- Modules/_cursesmodule.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index aa06666a8201bbb..2b580c3475e6d93 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -6573,6 +6573,18 @@ _curses_initscr_impl(PyObject *module) _curses_set_null_error(state, "wrefresh", "initscr"); return NULL; } + if (state->topscreen != NULL) { + /* The current screen is one made by newterm(); return its own + standard window instead of a second wrapper over the same + WINDOW, which would delwin() it on its own. */ + PyCursesScreenObject *so = (PyCursesScreenObject *)state->topscreen; + if (so->stdscr_win != NULL) { + if (curses_update_screen_encoding(so->stdscr_win) < 0) { + return NULL; + } + return Py_NewRef(so->stdscr_win); + } + } /* Attach the current screen, like newwin(), newpad() and getwin() do, so that the window keeps its screen alive. It is NULL for the screen created by initscr(), which has no screen object. */ From ed144a565623394c9170ee7a77256245228c824b Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 27 Jul 2026 20:00:00 +0300 Subject: [PATCH 3/3] Assert initscr() returns the screen's own window, drop the NEWS entry The test now checks the identity the review asked for. The NEWS entry goes away because set_term(), newterm() and this code path are all new in 3.16 and unreleased, so no user could observe the crash. --- Lib/test/test_curses.py | 10 ++++++---- .../2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst | 3 --- 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index e285c4bddd0b342..31b7371abd32300 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3073,16 +3073,18 @@ def test_new_prescr(self): gc_collect() def test_initscr_after_newterm_keeps_screen_alive(self): - # initscr() called while a newterm() screen is current returns a window - # for that screen, and the window must keep the screen alive. It used - # to be created without a screen, and using the window after the - # screen was collected read freed memory. + # initscr() called while a newterm() screen is current returns that + # screen's own standard window, so the window keeps the screen alive. + # It used to be a second wrapper created without a screen: using it + # after the screen was collected read freed memory, and both wrappers + # could delwin() the same window. s1 = self.make_pty() s2 = self.make_pty() screen1 = curses.newterm('xterm', s1, s1) screen2 = curses.newterm('xterm', s2, s2) curses.set_term(screen1) win = curses.initscr() + self.assertIs(win, screen1.stdscr) curses.set_term(screen2) del screen1 gc_collect() diff --git a/Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst b/Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst deleted file mode 100644 index 354447ca05d2bbe..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-07-26-21-11-58.gh-issue-154751.q-YeEM.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix a use-after-free in :mod:`curses`. :func:`curses.initscr` called while a -screen from :func:`curses.newterm` is current now returns a window that keeps -that screen alive, like :func:`curses.newwin` and :func:`curses.newpad` do.