Skip to content

Commit d7f9ca9

Browse files
[3.14] gh-151695: Fix use-after-free of the curses screen encoding (GH-151696)
The module-global curses_screen_encoding stored a borrowed pointer to the encoding owned by the window returned by the first initscr() call. That window can be deallocated while unctrl() and ungetch(), which have no window of their own, still use the pointer to encode non-ASCII characters. Keep a private copy of the encoding instead. (cherry picked from commit 551f8e1) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9906857 commit d7f9ca9

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a use-after-free in the :mod:`curses` module. The encoding of the initial
2+
screen, used by :func:`curses.unctrl` and :func:`curses.ungetch` to encode
3+
non-ASCII characters, is now kept as a private copy instead of a borrowed
4+
pointer to a window object that may be deallocated.

Modules/_cursesmodule.c

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ static int curses_initscr_called = FALSE;
209209
/* Tells whether start_color() has been called to initialise color usage. */
210210
static int curses_start_color_called = FALSE;
211211

212-
static const char *curses_screen_encoding = NULL;
212+
/* Encoding of the initial screen, used by module-level functions that have
213+
no window object to take it from (e.g. unctrl(), ungetch()). This is a
214+
private copy: the window object that initscr() returns may be deallocated
215+
while these functions are still in use. */
216+
static char *curses_screen_encoding = NULL;
213217

214218
/* Utility Checking Procedures */
215219

@@ -3518,6 +3522,21 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg)
35183522
Py_RETURN_NONE;
35193523
}
35203524

3525+
/* Refresh the private copy of the screen encoding from a freshly created
3526+
stdscr window object. Returns 0 on success, -1 with an exception set. */
3527+
static int
3528+
curses_update_screen_encoding(PyObject *winobj)
3529+
{
3530+
char *copy = _PyMem_Strdup(((PyCursesWindowObject *)winobj)->encoding);
3531+
if (copy == NULL) {
3532+
PyErr_NoMemory();
3533+
return -1;
3534+
}
3535+
PyMem_Free(curses_screen_encoding);
3536+
curses_screen_encoding = copy;
3537+
return 0;
3538+
}
3539+
35213540
/*[clinic input]
35223541
_curses.initscr
35233542
@@ -3533,9 +3552,21 @@ _curses_initscr_impl(PyObject *module)
35333552
WINDOW *win;
35343553

35353554
if (curses_initscr_called) {
3536-
wrefresh(stdscr);
35373555
cursesmodule_state *state = get_cursesmodule_state(module);
3538-
return PyCursesWindow_New(state, stdscr, NULL, NULL);
3556+
int code = wrefresh(stdscr);
3557+
if (code == ERR) {
3558+
_curses_set_null_error(state, "wrefresh", "initscr");
3559+
return NULL;
3560+
}
3561+
PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL);
3562+
if (winobj == NULL) {
3563+
return NULL;
3564+
}
3565+
if (curses_update_screen_encoding(winobj) < 0) {
3566+
Py_DECREF(winobj);
3567+
return NULL;
3568+
}
3569+
return winobj;
35393570
}
35403571

35413572
win = initscr();
@@ -3643,7 +3674,10 @@ _curses_initscr_impl(PyObject *module)
36433674
if (winobj == NULL) {
36443675
return NULL;
36453676
}
3646-
curses_screen_encoding = ((PyCursesWindowObject *)winobj)->encoding;
3677+
if (curses_update_screen_encoding(winobj) < 0) {
3678+
Py_DECREF(winobj);
3679+
return NULL;
3680+
}
36473681
return winobj;
36483682
}
36493683

@@ -5190,6 +5224,8 @@ static void
51905224
cursesmodule_free(void *mod)
51915225
{
51925226
(void)cursesmodule_clear((PyObject *)mod);
5227+
PyMem_Free(curses_screen_encoding);
5228+
curses_screen_encoding = NULL;
51935229
curses_module_loaded = 0; // allow reloading once garbage-collected
51945230
}
51955231

0 commit comments

Comments
 (0)