Skip to content

Commit 1a399e9

Browse files
Fix use-after-free of the curses screen encoding
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. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bfecfcc commit 1a399e9

2 files changed

Lines changed: 39 additions & 3 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: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,11 @@ static int curses_initscr_called = FALSE;
208208
/* Tells whether start_color() has been called to initialise color usage. */
209209
static int curses_start_color_called = FALSE;
210210

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

213217
/* Utility Error Procedures */
214218

@@ -3799,6 +3803,21 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg)
37993803
Py_RETURN_NONE;
38003804
}
38013805

3806+
/* Refresh the private copy of the screen encoding from a freshly created
3807+
stdscr window object. Returns 0 on success, -1 with an exception set. */
3808+
static int
3809+
curses_update_screen_encoding(PyObject *winobj)
3810+
{
3811+
char *copy = _PyMem_Strdup(((PyCursesWindowObject *)winobj)->encoding);
3812+
if (copy == NULL) {
3813+
PyErr_NoMemory();
3814+
return -1;
3815+
}
3816+
PyMem_Free(curses_screen_encoding);
3817+
curses_screen_encoding = copy;
3818+
return 0;
3819+
}
3820+
38023821
/*[clinic input]
38033822
_curses.initscr
38043823
@@ -3820,7 +3839,15 @@ _curses_initscr_impl(PyObject *module)
38203839
_curses_set_null_error(state, "wrefresh", "initscr");
38213840
return NULL;
38223841
}
3823-
return PyCursesWindow_New(state, stdscr, NULL, NULL);
3842+
PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL);
3843+
if (winobj == NULL) {
3844+
return NULL;
3845+
}
3846+
if (curses_update_screen_encoding(winobj) < 0) {
3847+
Py_DECREF(winobj);
3848+
return NULL;
3849+
}
3850+
return winobj;
38243851
}
38253852

38263853
win = initscr();
@@ -3927,7 +3954,10 @@ _curses_initscr_impl(PyObject *module)
39273954
if (winobj == NULL) {
39283955
return NULL;
39293956
}
3930-
curses_screen_encoding = ((PyCursesWindowObject *)winobj)->encoding;
3957+
if (curses_update_screen_encoding(winobj) < 0) {
3958+
Py_DECREF(winobj);
3959+
return NULL;
3960+
}
39313961
return winobj;
39323962
}
39333963

@@ -5480,6 +5510,8 @@ static void
54805510
cursesmodule_free(void *mod)
54815511
{
54825512
(void)cursesmodule_clear((PyObject *)mod);
5513+
PyMem_Free(curses_screen_encoding);
5514+
curses_screen_encoding = NULL;
54835515
curses_module_loaded = 0; // allow reloading once garbage-collected
54845516
}
54855517

0 commit comments

Comments
 (0)