Skip to content

Commit 0af61fe

Browse files
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 <[email protected]>
1 parent ac7d5ba commit 0af61fe

File tree

4 files changed

+28
-10
lines changed

4 files changed

+28
-10
lines changed

Include/py_curses.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ extern "C" {
7575

7676
/* Type declarations */
7777

78-
typedef struct {
78+
typedef struct PyCursesWindowObject {
7979
PyObject_HEAD
8080
WINDOW *win;
8181
char *encoding;
82+
struct PyCursesWindowObject *orig;
8283
} PyCursesWindowObject;
8384

8485
#define PyCurses_CAPSULE_NAME "_curses._C_API"

Lib/test/test_curses.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from unittest.mock import MagicMock
99

1010
from test.support import (requires, verbose, SaveSignals, cpython_only,
11-
check_disallow_instantiation, MISSING_C_DOCSTRINGS)
11+
check_disallow_instantiation, MISSING_C_DOCSTRINGS,
12+
gc_collect)
1213
from test.support.import_helper import import_module
1314

1415
# Optionally test curses module. This currently requires that the
@@ -181,6 +182,14 @@ def test_create_windows(self):
181182
self.assertEqual(win3.getparyx(), (2, 1))
182183
self.assertEqual(win3.getmaxyx(), (6, 11))
183184

185+
def test_subwindows_references(self):
186+
win = curses.newwin(5, 10)
187+
win2 = win.subwin(3, 7)
188+
del win
189+
gc_collect()
190+
del win2
191+
gc_collect()
192+
184193
def test_move_cursor(self):
185194
stdscr = self.stdscr
186195
win = stdscr.subwin(10, 15, 2, 5)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Keep a reference to original :mod:`curses` windows in subwindows so
2+
that the original window does not get deleted before subwindows.

Modules/_cursesmodule.c

+14-8
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,8 @@ Window_TwoArgNoReturnFunction(wresize, int, "ii;lines,columns")
787787

788788
static PyObject *
789789
PyCursesWindow_New(cursesmodule_state *state,
790-
WINDOW *win, const char *encoding)
790+
WINDOW *win, const char *encoding,
791+
PyCursesWindowObject *orig)
791792
{
792793
if (encoding == NULL) {
793794
#if defined(MS_WINDOWS)
@@ -821,6 +822,8 @@ PyCursesWindow_New(cursesmodule_state *state,
821822
PyErr_NoMemory();
822823
return NULL;
823824
}
825+
wo->orig = orig;
826+
Py_XINCREF(orig);
824827
PyObject_GC_Track((PyObject *)wo);
825828
return (PyObject *)wo;
826829
}
@@ -838,6 +841,7 @@ PyCursesWindow_dealloc(PyObject *self)
838841
if (wo->encoding != NULL) {
839842
PyMem_Free(wo->encoding);
840843
}
844+
Py_XDECREF(wo->orig);
841845
window_type->tp_free(self);
842846
Py_DECREF(window_type);
843847
}
@@ -846,6 +850,8 @@ static int
846850
PyCursesWindow_traverse(PyObject *self, visitproc visit, void *arg)
847851
{
848852
Py_VISIT(Py_TYPE(self));
853+
PyCursesWindowObject *wo = (PyCursesWindowObject *)self;
854+
Py_VISIT(wo->orig);
849855
return 0;
850856
}
851857

@@ -1453,7 +1459,7 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1,
14531459
}
14541460

14551461
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
1456-
return PyCursesWindow_New(state, win, NULL);
1462+
return PyCursesWindow_New(state, win, NULL, self);
14571463
}
14581464

14591465
/*[clinic input]
@@ -2493,7 +2499,7 @@ _curses_window_subwin_impl(PyCursesWindowObject *self, int group_left_1,
24932499
}
24942500

24952501
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
2496-
return PyCursesWindow_New(state, win, self->encoding);
2502+
return PyCursesWindow_New(state, win, self->encoding, self);
24972503
}
24982504

24992505
/*[clinic input]
@@ -3237,7 +3243,7 @@ _curses_getwin(PyObject *module, PyObject *file)
32373243
goto error;
32383244
}
32393245
cursesmodule_state *state = get_cursesmodule_state(module);
3240-
res = PyCursesWindow_New(state, win, NULL);
3246+
res = PyCursesWindow_New(state, win, NULL, NULL);
32413247

32423248
error:
32433249
fclose(fp);
@@ -3410,7 +3416,7 @@ _curses_initscr_impl(PyObject *module)
34103416
if (curses_initscr_called) {
34113417
wrefresh(stdscr);
34123418
cursesmodule_state *state = get_cursesmodule_state(module);
3413-
return PyCursesWindow_New(state, stdscr, NULL);
3419+
return PyCursesWindow_New(state, stdscr, NULL, NULL);
34143420
}
34153421

34163422
win = initscr();
@@ -3514,7 +3520,7 @@ _curses_initscr_impl(PyObject *module)
35143520
#undef SetDictInt
35153521

35163522
cursesmodule_state *state = get_cursesmodule_state(module);
3517-
PyObject *winobj = PyCursesWindow_New(state, win, NULL);
3523+
PyObject *winobj = PyCursesWindow_New(state, win, NULL, NULL);
35183524
if (winobj == NULL) {
35193525
return NULL;
35203526
}
@@ -3898,7 +3904,7 @@ _curses_newpad_impl(PyObject *module, int nlines, int ncols)
38983904
}
38993905

39003906
cursesmodule_state *state = get_cursesmodule_state(module);
3901-
return PyCursesWindow_New(state, win, NULL);
3907+
return PyCursesWindow_New(state, win, NULL, NULL);
39023908
}
39033909

39043910
/*[clinic input]
@@ -3939,7 +3945,7 @@ _curses_newwin_impl(PyObject *module, int nlines, int ncols,
39393945
}
39403946

39413947
cursesmodule_state *state = get_cursesmodule_state(module);
3942-
return PyCursesWindow_New(state, win, NULL);
3948+
return PyCursesWindow_New(state, win, NULL, NULL);
39433949
}
39443950

39453951
/*[clinic input]

0 commit comments

Comments
 (0)