From 1106e38c5959f86a0e2b5840de7de545c3ee1318 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Sun, 16 Aug 2026 21:54:40 +0000 Subject: [PATCH 1/2] gh-155875: Fix new_prescr screen lifetime --- Lib/test/test_curses.py | 25 +++++++++++++++++++++++++ Modules/_cursesmodule.c | 29 +++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index f582336fae1734..2c08a3566f9091 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3166,6 +3166,31 @@ def test_use_prescr_screen(self): # The current screen is unchanged. screen.stdscr.refresh() + @unittest.skipUnless(hasattr(curses, 'new_prescr'), + 'requires curses.new_prescr()') + def test_newterm_after_new_prescr_keeps_screen_alive(self): + # newterm() adopts the SCREEN created by new_prescr(). Dropping the + # pre-screen wrapper must not delete the live screen. + s = self.make_pty() + pre = curses.new_prescr() + screen = curses.newterm('xterm', s, s) + del pre + gc_collect() + screen.stdscr.addstr(0, 0, 'x') + screen.stdscr.refresh() + + @unittest.skipUnless(hasattr(curses, 'new_prescr'), + 'requires curses.new_prescr()') + def test_initscr_after_new_prescr_keeps_screen_alive(self): + # initscr() adopts the SCREEN created by new_prescr(). Dropping the + # pre-screen wrapper must not delete the live screen. + pre = curses.new_prescr() + stdscr = curses.initscr() + del pre + gc_collect() + stdscr.addstr(0, 0, 'x') + stdscr.refresh() + def test_initscr_after_newterm_keeps_screen_alive(self): # initscr() called while a newterm() screen is current returns that # screen's own standard window, so the window keeps the screen alive. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 006e27d55d8925..6a6a0232d3d51e 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -159,6 +159,8 @@ typedef struct { PyTypeObject *complexstr_type; // _curses.complexstr PyObject *topscreen; // owned ref to the current screen object, // or NULL for the initscr() screen + PyObject *prescreen; // owned ref to the pending new_prescr() screen, + // or NULL if there is no pending pre-screen } cursesmodule_state; static inline cursesmodule_state * @@ -6619,13 +6621,21 @@ _curses_initscr_impl(PyObject *module) return NULL; } + cursesmodule_state *state = get_cursesmodule_state(module); + if (state->prescreen != NULL) { + PyCursesScreenObject *prescreen = + _PyCursesScreenObject_CAST(state->prescreen); + assert(prescreen->screen != NULL); + prescreen->screen = NULL; + Py_CLEAR(state->prescreen); + } + curses_initscr_called = curses_setupterm_called = TRUE; if (curses_init_dict(module) < 0) { return NULL; } - cursesmodule_state *state = get_cursesmodule_state(module); PyObject *winobj = PyCursesWindow_New(state, win, NULL, NULL, NULL); if (winobj == NULL) { return NULL; @@ -6801,6 +6811,13 @@ _curses_newterm_impl(PyObject *module, const char *type, PyObject *fd, cursesmodule_state *state = get_cursesmodule_state(module); /* The screen object owns the SCREEN and the streams; deleting it (when it is no longer referenced) calls delscreen() and closes the streams. */ + if (state->prescreen != NULL) { + PyCursesScreenObject *prescreen = + _PyCursesScreenObject_CAST(state->prescreen); + assert(prescreen->screen == screen); + prescreen->screen = NULL; + Py_CLEAR(state->prescreen); + } PyObject *screenobj = PyCursesScreen_New(state, screen, outfp, infp, NULL); if (screenobj == NULL) { delscreen(screen); @@ -6898,7 +6915,13 @@ _curses_new_prescr_impl(PyObject *module) return NULL; } cursesmodule_state *state = get_cursesmodule_state(module); - return PyCursesScreen_New(state, screen, NULL, NULL, NULL); + PyObject *screenobj = PyCursesScreen_New(state, screen, NULL, NULL, NULL); + if (screenobj == NULL) { + delscreen(screen); + return NULL; + } + Py_XSETREF(state->prescreen, Py_NewRef(screenobj)); + return screenobj; } #endif /* HAVE_CURSES_NEW_PRESCR */ @@ -8894,6 +8917,7 @@ cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg) Py_VISIT(state->complexchar_type); Py_VISIT(state->complexstr_type); Py_VISIT(state->topscreen); + Py_VISIT(state->prescreen); return 0; } @@ -8907,6 +8931,7 @@ cursesmodule_clear(PyObject *mod) Py_CLEAR(state->complexchar_type); Py_CLEAR(state->complexstr_type); Py_CLEAR(state->topscreen); + Py_CLEAR(state->prescreen); return 0; } From be346ad8cc5e8ff642d00a57924a55cbe8abba4b Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Sun, 16 Aug 2026 22:00:39 +0000 Subject: [PATCH 2/2] gh-155875: Add NEWS entry --- .../next/Library/2026-08-16-22-00-25.gh-issue-155875.6TI-oX.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-16-22-00-25.gh-issue-155875.6TI-oX.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-16-22-00-25.gh-issue-155875.6TI-oX.rst b/Misc/NEWS.d/next/Library/2026-08-16-22-00-25.gh-issue-155875.6TI-oX.rst new file mode 100644 index 00000000000000..13685368623900 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-16-22-00-25.gh-issue-155875.6TI-oX.rst @@ -0,0 +1,2 @@ +Fix a use-after-free in :mod:`curses` when :func:`curses.initscr` or +:func:`curses.newterm` follows :func:`curses.new_prescr`.