https://github.com/python/cpython/commit/0fc5ebc7a1b7475e746e34204dbc9ad386a3b030
commit: 0fc5ebc7a1b7475e746e34204dbc9ad386a3b030
branch: main
author: Bhuvansh <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-15T11:41:44Z
summary:
gh-155875: Fix use-after-free in curses after new_prescr() (GH-155914)
initscr() and newterm() adopt the screen created by new_prescr(), so the
screen object returned by new_prescr() no longer owns it after that, and
new_prescr() returns the same object while the screen is pending.
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 d779955e236228..9cc6eee266bdd8 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -3481,6 +3481,44 @@ 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_new_prescr_returns_existing_screen(self):
+ pre1 = curses.new_prescr()
+ pre2 = curses.new_prescr()
+ self.assertIs(pre1, pre2)
+
+ @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.
+ s = self.make_pty()
+ saved = os.dup(1)
+ self.addCleanup(os.close, saved)
+ self.addCleanup(os.dup2, saved, 1)
+ os.dup2(s, 1)
+
+ 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 24cfbcdd503cee..fd65cbb0bf67a8 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 *
@@ -6979,13 +6981,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;
@@ -7161,6 +7171,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);
@@ -7252,13 +7269,25 @@ static PyObject *
_curses_new_prescr_impl(PyObject *module)
/*[clinic end generated code: output=e7de5031da7511e2 input=1a3a89d630b641c3]*/
{
+ cursesmodule_state *state = get_cursesmodule_state(module);
+ if (state->prescreen != NULL) {
+ return Py_NewRef(state->prescreen);
+ }
+
SCREEN *screen = new_prescr();
if (screen == NULL) {
curses_set_null_error(module, "new_prescr", NULL);
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;
+ }
+
+ state->prescreen = Py_NewRef(screenobj);
+ return screenobj;
}
#endif /* HAVE_CURSES_NEW_PRESCR */
@@ -9262,6 +9291,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;
}
@@ -9275,6 +9305,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;
}
_______________________________________________
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]