From 56b7a07831fa670009c054a4ed26ed46f0eeb9fc Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 27 Jul 2026 22:48:40 +0300 Subject: [PATCH] gh-154786: Fix crashes on a screen without a terminal screen.use() makes its screen current for the callback, so a new_prescr() screen, which has no terminal, can be current without set_term(). Operations that need a terminal then crashed inside curses. Raise curses.error instead, checking that stdscr exists. --- Lib/test/test_curses.py | 24 +++++++++ Modules/_cursesmodule.c | 106 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 441eafd6e0d477..9f7f8535b6d9a8 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -3083,6 +3083,30 @@ def test_set_term_prescr_screen(self): # The current screen is unchanged, so refreshing it still works. screen.stdscr.refresh() + @unittest.skipUnless(hasattr(curses, 'new_prescr'), + 'requires curses.new_prescr()') + @unittest.skipUnless(hasattr(curses.screen, 'use'), + 'requires curses.screen.use()') + def test_use_prescr_screen(self): + # use() makes its screen current for the callback, so a new_prescr() + # screen is current there without having a terminal. Operations that + # need one used to crash inside curses. + s = self.make_pty() + screen = curses.newterm('xterm', s, s) + prescr = curses.new_prescr() + for func in [ + lambda scr: curses.doupdate(), + lambda scr: curses.newwin(3, 3), + lambda scr: screen.stdscr.refresh(), + lambda scr: screen.stdscr.getch(), + ]: + with self.assertRaises(curses.error): + prescr.use(func) + # Affecting the state before initscr() is what such a screen is for. + prescr.use(lambda scr: curses.use_env(False)) + # The current screen is unchanged. + screen.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 7314708cff7a81..01ea3c43cce2e6 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -356,6 +356,35 @@ _PyCursesStatefulCheckFunction(PyObject *module, return 0; } +/* + * Function to check that the current screen has a terminal, by testing + * stdscr, which a screen made by new_prescr() does not have. If an error + * occurs, a PyCursesError is set and this returns 0. Otherwise this + * returns 1. + */ +static int +_PyCursesStatefulCheckTerminal(PyObject *module) +{ + if (stdscr != NULL) { + return 1; + } + cursesmodule_state *state = get_cursesmodule_state(module); + PyErr_SetString(state->error, "the current screen has no terminal"); + return 0; +} + +/* Same as _PyCursesStatefulCheckTerminal() for a Window object. */ +static int +curses_window_check_terminal(PyCursesWindowObject *win) +{ + if (stdscr != NULL) { + return 1; + } + cursesmodule_state *state = get_cursesmodule_state_by_win(win); + PyErr_SetString(state->error, "the current screen has no terminal"); + return 0; +} + #define PyCursesStatefulSetupTermCalled(MODULE) \ do { \ if (!_PyCursesStatefulCheckFunction(MODULE, \ @@ -370,7 +399,8 @@ _PyCursesStatefulCheckFunction(PyObject *module, do { \ if (!_PyCursesStatefulCheckFunction(MODULE, \ curses_initscr_called, \ - "initscr")) \ + "initscr") \ + || !_PyCursesStatefulCheckTerminal(MODULE)) \ { \ return 0; \ } \ @@ -1793,6 +1823,42 @@ curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj, Py_RETURN_NONE; \ } +/* Same as Window_OneArgNoReturnVoidFunction() for a function that needs + a terminal. */ +#define Window_OneArgNoReturnVoidTerminalFunction(X, TYPE, PARSESTR) \ + static PyObject * PyCursesWindow_ ## X \ + (PyObject *op, PyObject *args) \ + { \ + TYPE arg1; \ + if (!PyArg_ParseTuple(args, PARSESTR, &arg1)) { \ + return NULL; \ + } \ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); \ + if (!curses_window_check_terminal(self)) { \ + return NULL; \ + } \ + X(self->win, arg1); \ + Py_RETURN_NONE; \ + } + +/* Same as Window_OneArgNoReturnFunction() for a function that needs + a terminal. */ +#define Window_OneArgNoReturnTerminalFunction(X, TYPE, PARSESTR) \ + static PyObject * PyCursesWindow_ ## X \ + (PyObject *op, PyObject *args) \ + { \ + TYPE arg1; \ + if (!PyArg_ParseTuple(args, PARSESTR, &arg1)) { \ + return NULL; \ + } \ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); \ + if (!curses_window_check_terminal(self)) { \ + return NULL; \ + } \ + int code = X(self->win, arg1); \ + return curses_window_check_err(self, code, # X, NULL); \ + } + #define Window_OneArgNoReturnFunction(X, TYPE, PARSESTR) \ static PyObject * PyCursesWindow_ ## X \ (PyObject *op, PyObject *args) \ @@ -1893,7 +1959,7 @@ Window_NoArgNoReturnVoidFunction(wclrtoeol) Window_NoArgNoReturnVoidFunction(wclrtobot) Window_NoArgNoReturnVoidFunction(wclear) -Window_OneArgNoReturnVoidFunction(idcok, int, "i;True(1) or False(0)") +Window_OneArgNoReturnVoidTerminalFunction(idcok, int, "i;True(1) or False(0)") #ifdef HAVE_CURSES_IMMEDOK Window_OneArgNoReturnVoidFunction(immedok, int, "i;True(1) or False(0)") #endif @@ -1905,8 +1971,8 @@ Window_NoArg2TupleReturnFunction(getmaxyx, int, "ii") Window_NoArg2TupleReturnFunction(getparyx, int, "ii") Window_OneArgNoReturnFunction(clearok, int, "i;True(1) or False(0)") -Window_OneArgNoReturnFunction(idlok, int, "i;True(1) or False(0)") -Window_OneArgNoReturnFunction(keypad, int, "i;True(1) or False(0)") +Window_OneArgNoReturnTerminalFunction(idlok, int, "i;True(1) or False(0)") +Window_OneArgNoReturnTerminalFunction(keypad, int, "i;True(1) or False(0)") Window_OneArgNoReturnFunction(leaveok, int, "i;True(1) or False(0)") Window_OneArgNoReturnFunction(nodelay, int, "i;True(1) or False(0)") Window_OneArgNoReturnFunction(notimeout, int, "i;True(1) or False(0)") @@ -2960,6 +3026,10 @@ _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch, int group_right_1, attr_t attr) /*[clinic end generated code: output=ab03afa580aa6a2a input=cd74c42aadcc7e30]*/ { + if (!curses_window_check_terminal(self)) { + return NULL; + } + chtype ch_; #ifdef HAVE_NCURSESW cchar_t wch; @@ -3206,6 +3276,10 @@ _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1, { int rtn; + if (!curses_window_check_terminal(self)) { + return NULL; + } + Py_BEGIN_ALLOW_THREADS if (!group_right_1) { rtn = wgetch(self->win); @@ -3254,6 +3328,10 @@ _curses_window_getkey_impl(PyCursesWindowObject *self, int group_right_1, { int rtn; + if (!curses_window_check_terminal(self)) { + return NULL; + } + Py_BEGIN_ALLOW_THREADS if (!group_right_1) { rtn = wgetch(self->win); @@ -3305,6 +3383,10 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1, int y, int x) /*[clinic end generated code: output=9f4f86e91fe50ef3 input=dd7e5367fb49dc48]*/ { + if (!curses_window_check_terminal(self)) { + return NULL; + } + #ifdef HAVE_NCURSESW int ct; wint_t rtn; @@ -3454,6 +3536,10 @@ static PyObject * PyCursesWindow_getstr(PyObject *op, PyObject *args) { PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + + if (!curses_window_check_terminal(self)) { + return NULL; + } return curses_window_getstr_bytes(self, args, "_curses.window.getstr"); } @@ -3755,6 +3841,10 @@ static PyObject * PyCursesWindow_get_wstr(PyObject *op, PyObject *args) { PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + + if (!curses_window_check_terminal(self)) { + return NULL; + } #ifdef HAVE_NCURSESW int rtn, use_xy = 0, y = 0, x = 0; unsigned int max_buf_size = 2048; @@ -4271,6 +4361,10 @@ _curses_window_noutrefresh_impl(PyCursesWindowObject *self) { int rtn; + if (!curses_window_check_terminal(self)) { + return NULL; + } + #ifdef py_is_pad if (py_is_pad(self->win)) { if (!group_right_1) { @@ -4501,6 +4595,10 @@ _curses_window_refresh_impl(PyCursesWindowObject *self, int group_right_1, { int rtn; + if (!curses_window_check_terminal(self)) { + return NULL; + } + #ifdef py_is_pad if (py_is_pad(self->win)) { if (!group_right_1) {