https://github.com/python/cpython/commit/c8eb79d5e89196449992fe50978d6b8845dc6681
commit: c8eb79d5e89196449992fe50978d6b8845dc6681
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-28T08:00:39+03:00
summary:
gh-154786: Fix crashes on a screen without a terminal (GH-154787)
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.
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 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) {
_______________________________________________
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]