bpo-44172: Keep reference to original window in curses subwindow objects (GH-26226)

The X/Open curses specification[0] and ncurses documentation[1]
both state that subwindows must be deleted before the main window.

Deleting the windows in the wrong order causes a double-free with
NetBSD's curses implementation.

To fix this, keep track of the original window object in the subwindow
object, and keep a reference to the original for the lifetime of
the subwindow.

[0] https://pubs.opengroup.org/onlinepubs/7908799/xcurses/delwin.html
[1] https://invisible-island.net/ncurses/man/curs_window.3x.html

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Michael Forney 2025-05-04 03:29:44 -07:00 committed by GitHub
parent ac7d5ba96e
commit 0af61fe2f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 28 additions and 10 deletions

View file

@ -75,10 +75,11 @@ extern "C" {
/* Type declarations */
typedef struct {
typedef struct PyCursesWindowObject {
PyObject_HEAD
WINDOW *win;
char *encoding;
struct PyCursesWindowObject *orig;
} PyCursesWindowObject;
#define PyCurses_CAPSULE_NAME "_curses._C_API"

View file

@ -8,7 +8,8 @@ import unittest
from unittest.mock import MagicMock
from test.support import (requires, verbose, SaveSignals, cpython_only,
check_disallow_instantiation, MISSING_C_DOCSTRINGS)
check_disallow_instantiation, MISSING_C_DOCSTRINGS,
gc_collect)
from test.support.import_helper import import_module
# Optionally test curses module. This currently requires that the
@ -181,6 +182,14 @@ class TestCurses(unittest.TestCase):
self.assertEqual(win3.getparyx(), (2, 1))
self.assertEqual(win3.getmaxyx(), (6, 11))
def test_subwindows_references(self):
win = curses.newwin(5, 10)
win2 = win.subwin(3, 7)
del win
gc_collect()
del win2
gc_collect()
def test_move_cursor(self):
stdscr = self.stdscr
win = stdscr.subwin(10, 15, 2, 5)

View file

@ -0,0 +1,2 @@
Keep a reference to original :mod:`curses` windows in subwindows so
that the original window does not get deleted before subwindows.

View file

@ -787,7 +787,8 @@ Window_TwoArgNoReturnFunction(wresize, int, "ii;lines,columns")
static PyObject *
PyCursesWindow_New(cursesmodule_state *state,
WINDOW *win, const char *encoding)
WINDOW *win, const char *encoding,
PyCursesWindowObject *orig)
{
if (encoding == NULL) {
#if defined(MS_WINDOWS)
@ -821,6 +822,8 @@ PyCursesWindow_New(cursesmodule_state *state,
PyErr_NoMemory();
return NULL;
}
wo->orig = orig;
Py_XINCREF(orig);
PyObject_GC_Track((PyObject *)wo);
return (PyObject *)wo;
}
@ -838,6 +841,7 @@ PyCursesWindow_dealloc(PyObject *self)
if (wo->encoding != NULL) {
PyMem_Free(wo->encoding);
}
Py_XDECREF(wo->orig);
window_type->tp_free(self);
Py_DECREF(window_type);
}
@ -846,6 +850,8 @@ static int
PyCursesWindow_traverse(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(Py_TYPE(self));
PyCursesWindowObject *wo = (PyCursesWindowObject *)self;
Py_VISIT(wo->orig);
return 0;
}
@ -1453,7 +1459,7 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1,
}
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
return PyCursesWindow_New(state, win, NULL);
return PyCursesWindow_New(state, win, NULL, self);
}
/*[clinic input]
@ -2493,7 +2499,7 @@ _curses_window_subwin_impl(PyCursesWindowObject *self, int group_left_1,
}
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
return PyCursesWindow_New(state, win, self->encoding);
return PyCursesWindow_New(state, win, self->encoding, self);
}
/*[clinic input]
@ -3237,7 +3243,7 @@ _curses_getwin(PyObject *module, PyObject *file)
goto error;
}
cursesmodule_state *state = get_cursesmodule_state(module);
res = PyCursesWindow_New(state, win, NULL);
res = PyCursesWindow_New(state, win, NULL, NULL);
error:
fclose(fp);
@ -3410,7 +3416,7 @@ _curses_initscr_impl(PyObject *module)
if (curses_initscr_called) {
wrefresh(stdscr);
cursesmodule_state *state = get_cursesmodule_state(module);
return PyCursesWindow_New(state, stdscr, NULL);
return PyCursesWindow_New(state, stdscr, NULL, NULL);
}
win = initscr();
@ -3514,7 +3520,7 @@ _curses_initscr_impl(PyObject *module)
#undef SetDictInt
cursesmodule_state *state = get_cursesmodule_state(module);
PyObject *winobj = PyCursesWindow_New(state, win, NULL);
PyObject *winobj = PyCursesWindow_New(state, win, NULL, NULL);
if (winobj == NULL) {
return NULL;
}
@ -3898,7 +3904,7 @@ _curses_newpad_impl(PyObject *module, int nlines, int ncols)
}
cursesmodule_state *state = get_cursesmodule_state(module);
return PyCursesWindow_New(state, win, NULL);
return PyCursesWindow_New(state, win, NULL, NULL);
}
/*[clinic input]
@ -3939,7 +3945,7 @@ _curses_newwin_impl(PyObject *module, int nlines, int ncols,
}
cursesmodule_state *state = get_cursesmodule_state(module);
return PyCursesWindow_New(state, win, NULL);
return PyCursesWindow_New(state, win, NULL, NULL);
}
/*[clinic input]