Skip to content

Commit c8eb79d

Browse files
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.
1 parent 22a123f commit c8eb79d

2 files changed

Lines changed: 126 additions & 4 deletions

File tree

Lib/test/test_curses.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3083,6 +3083,30 @@ def test_set_term_prescr_screen(self):
30833083
# The current screen is unchanged, so refreshing it still works.
30843084
screen.stdscr.refresh()
30853085

3086+
@unittest.skipUnless(hasattr(curses, 'new_prescr'),
3087+
'requires curses.new_prescr()')
3088+
@unittest.skipUnless(hasattr(curses.screen, 'use'),
3089+
'requires curses.screen.use()')
3090+
def test_use_prescr_screen(self):
3091+
# use() makes its screen current for the callback, so a new_prescr()
3092+
# screen is current there without having a terminal. Operations that
3093+
# need one used to crash inside curses.
3094+
s = self.make_pty()
3095+
screen = curses.newterm('xterm', s, s)
3096+
prescr = curses.new_prescr()
3097+
for func in [
3098+
lambda scr: curses.doupdate(),
3099+
lambda scr: curses.newwin(3, 3),
3100+
lambda scr: screen.stdscr.refresh(),
3101+
lambda scr: screen.stdscr.getch(),
3102+
]:
3103+
with self.assertRaises(curses.error):
3104+
prescr.use(func)
3105+
# Affecting the state before initscr() is what such a screen is for.
3106+
prescr.use(lambda scr: curses.use_env(False))
3107+
# The current screen is unchanged.
3108+
screen.stdscr.refresh()
3109+
30863110
def test_initscr_after_newterm_keeps_screen_alive(self):
30873111
# initscr() called while a newterm() screen is current returns that
30883112
# screen's own standard window, so the window keeps the screen alive.

Modules/_cursesmodule.c

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,35 @@ _PyCursesStatefulCheckFunction(PyObject *module,
356356
return 0;
357357
}
358358

359+
/*
360+
* Function to check that the current screen has a terminal, by testing
361+
* stdscr, which a screen made by new_prescr() does not have. If an error
362+
* occurs, a PyCursesError is set and this returns 0. Otherwise this
363+
* returns 1.
364+
*/
365+
static int
366+
_PyCursesStatefulCheckTerminal(PyObject *module)
367+
{
368+
if (stdscr != NULL) {
369+
return 1;
370+
}
371+
cursesmodule_state *state = get_cursesmodule_state(module);
372+
PyErr_SetString(state->error, "the current screen has no terminal");
373+
return 0;
374+
}
375+
376+
/* Same as _PyCursesStatefulCheckTerminal() for a Window object. */
377+
static int
378+
curses_window_check_terminal(PyCursesWindowObject *win)
379+
{
380+
if (stdscr != NULL) {
381+
return 1;
382+
}
383+
cursesmodule_state *state = get_cursesmodule_state_by_win(win);
384+
PyErr_SetString(state->error, "the current screen has no terminal");
385+
return 0;
386+
}
387+
359388
#define PyCursesStatefulSetupTermCalled(MODULE) \
360389
do { \
361390
if (!_PyCursesStatefulCheckFunction(MODULE, \
@@ -370,7 +399,8 @@ _PyCursesStatefulCheckFunction(PyObject *module,
370399
do { \
371400
if (!_PyCursesStatefulCheckFunction(MODULE, \
372401
curses_initscr_called, \
373-
"initscr")) \
402+
"initscr") \
403+
|| !_PyCursesStatefulCheckTerminal(MODULE)) \
374404
{ \
375405
return 0; \
376406
} \
@@ -1793,6 +1823,42 @@ curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj,
17931823
Py_RETURN_NONE; \
17941824
}
17951825

1826+
/* Same as Window_OneArgNoReturnVoidFunction() for a function that needs
1827+
a terminal. */
1828+
#define Window_OneArgNoReturnVoidTerminalFunction(X, TYPE, PARSESTR) \
1829+
static PyObject * PyCursesWindow_ ## X \
1830+
(PyObject *op, PyObject *args) \
1831+
{ \
1832+
TYPE arg1; \
1833+
if (!PyArg_ParseTuple(args, PARSESTR, &arg1)) { \
1834+
return NULL; \
1835+
} \
1836+
PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); \
1837+
if (!curses_window_check_terminal(self)) { \
1838+
return NULL; \
1839+
} \
1840+
X(self->win, arg1); \
1841+
Py_RETURN_NONE; \
1842+
}
1843+
1844+
/* Same as Window_OneArgNoReturnFunction() for a function that needs
1845+
a terminal. */
1846+
#define Window_OneArgNoReturnTerminalFunction(X, TYPE, PARSESTR) \
1847+
static PyObject * PyCursesWindow_ ## X \
1848+
(PyObject *op, PyObject *args) \
1849+
{ \
1850+
TYPE arg1; \
1851+
if (!PyArg_ParseTuple(args, PARSESTR, &arg1)) { \
1852+
return NULL; \
1853+
} \
1854+
PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); \
1855+
if (!curses_window_check_terminal(self)) { \
1856+
return NULL; \
1857+
} \
1858+
int code = X(self->win, arg1); \
1859+
return curses_window_check_err(self, code, # X, NULL); \
1860+
}
1861+
17961862
#define Window_OneArgNoReturnFunction(X, TYPE, PARSESTR) \
17971863
static PyObject * PyCursesWindow_ ## X \
17981864
(PyObject *op, PyObject *args) \
@@ -1893,7 +1959,7 @@ Window_NoArgNoReturnVoidFunction(wclrtoeol)
18931959
Window_NoArgNoReturnVoidFunction(wclrtobot)
18941960
Window_NoArgNoReturnVoidFunction(wclear)
18951961

1896-
Window_OneArgNoReturnVoidFunction(idcok, int, "i;True(1) or False(0)")
1962+
Window_OneArgNoReturnVoidTerminalFunction(idcok, int, "i;True(1) or False(0)")
18971963
#ifdef HAVE_CURSES_IMMEDOK
18981964
Window_OneArgNoReturnVoidFunction(immedok, int, "i;True(1) or False(0)")
18991965
#endif
@@ -1905,8 +1971,8 @@ Window_NoArg2TupleReturnFunction(getmaxyx, int, "ii")
19051971
Window_NoArg2TupleReturnFunction(getparyx, int, "ii")
19061972

19071973
Window_OneArgNoReturnFunction(clearok, int, "i;True(1) or False(0)")
1908-
Window_OneArgNoReturnFunction(idlok, int, "i;True(1) or False(0)")
1909-
Window_OneArgNoReturnFunction(keypad, int, "i;True(1) or False(0)")
1974+
Window_OneArgNoReturnTerminalFunction(idlok, int, "i;True(1) or False(0)")
1975+
Window_OneArgNoReturnTerminalFunction(keypad, int, "i;True(1) or False(0)")
19101976
Window_OneArgNoReturnFunction(leaveok, int, "i;True(1) or False(0)")
19111977
Window_OneArgNoReturnFunction(nodelay, int, "i;True(1) or False(0)")
19121978
Window_OneArgNoReturnFunction(notimeout, int, "i;True(1) or False(0)")
@@ -2960,6 +3026,10 @@ _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch,
29603026
int group_right_1, attr_t attr)
29613027
/*[clinic end generated code: output=ab03afa580aa6a2a input=cd74c42aadcc7e30]*/
29623028
{
3029+
if (!curses_window_check_terminal(self)) {
3030+
return NULL;
3031+
}
3032+
29633033
chtype ch_;
29643034
#ifdef HAVE_NCURSESW
29653035
cchar_t wch;
@@ -3206,6 +3276,10 @@ _curses_window_getch_impl(PyCursesWindowObject *self, int group_right_1,
32063276
{
32073277
int rtn;
32083278

3279+
if (!curses_window_check_terminal(self)) {
3280+
return NULL;
3281+
}
3282+
32093283
Py_BEGIN_ALLOW_THREADS
32103284
if (!group_right_1) {
32113285
rtn = wgetch(self->win);
@@ -3254,6 +3328,10 @@ _curses_window_getkey_impl(PyCursesWindowObject *self, int group_right_1,
32543328
{
32553329
int rtn;
32563330

3331+
if (!curses_window_check_terminal(self)) {
3332+
return NULL;
3333+
}
3334+
32573335
Py_BEGIN_ALLOW_THREADS
32583336
if (!group_right_1) {
32593337
rtn = wgetch(self->win);
@@ -3305,6 +3383,10 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1,
33053383
int y, int x)
33063384
/*[clinic end generated code: output=9f4f86e91fe50ef3 input=dd7e5367fb49dc48]*/
33073385
{
3386+
if (!curses_window_check_terminal(self)) {
3387+
return NULL;
3388+
}
3389+
33083390
#ifdef HAVE_NCURSESW
33093391
int ct;
33103392
wint_t rtn;
@@ -3454,6 +3536,10 @@ static PyObject *
34543536
PyCursesWindow_getstr(PyObject *op, PyObject *args)
34553537
{
34563538
PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op);
3539+
3540+
if (!curses_window_check_terminal(self)) {
3541+
return NULL;
3542+
}
34573543
return curses_window_getstr_bytes(self, args, "_curses.window.getstr");
34583544
}
34593545

@@ -3755,6 +3841,10 @@ static PyObject *
37553841
PyCursesWindow_get_wstr(PyObject *op, PyObject *args)
37563842
{
37573843
PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op);
3844+
3845+
if (!curses_window_check_terminal(self)) {
3846+
return NULL;
3847+
}
37583848
#ifdef HAVE_NCURSESW
37593849
int rtn, use_xy = 0, y = 0, x = 0;
37603850
unsigned int max_buf_size = 2048;
@@ -4271,6 +4361,10 @@ _curses_window_noutrefresh_impl(PyCursesWindowObject *self)
42714361
{
42724362
int rtn;
42734363

4364+
if (!curses_window_check_terminal(self)) {
4365+
return NULL;
4366+
}
4367+
42744368
#ifdef py_is_pad
42754369
if (py_is_pad(self->win)) {
42764370
if (!group_right_1) {
@@ -4501,6 +4595,10 @@ _curses_window_refresh_impl(PyCursesWindowObject *self, int group_right_1,
45014595
{
45024596
int rtn;
45034597

4598+
if (!curses_window_check_terminal(self)) {
4599+
return NULL;
4600+
}
4601+
45044602
#ifdef py_is_pad
45054603
if (py_is_pad(self->win)) {
45064604
if (!group_right_1) {

0 commit comments

Comments
 (0)