From 23f58192c3b718677aa80af7714391058f75e687 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 27 Jul 2026 00:12:38 +0300 Subject: [PATCH] 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; }