Skip to content

Commit 110da9d

Browse files
committed
gh-155860: Reject a detached window in panel.replace()
curses.screen.close() detaches the screen's standard window: the wrapper object stays alive but the curses window behind it is gone. panel.replace() did not check for that, so it stored the detached window in the panel and curses dereferenced it on the panel's next use, killing the interpreter with SIGSEGV. Raise curses.panel.error instead, the way new_panel() already does on the same window.
1 parent 948fd7e commit 110da9d

3 files changed

Lines changed: 24 additions & 0 deletions

File tree

Doc/library/curses.panel.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ Panel objects
116116
.. method:: panel.replace(win)
117117

118118
Change the window associated with the panel to the window *win*.
119+
Raise :exc:`curses.panel.error` if *win* has been detached from its
120+
screen by :meth:`screen.close() <curses.screen.close>`.
119121

120122

121123
.. method:: panel.set_userptr(obj)

Lib/test/test_curses.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3079,6 +3079,22 @@ def test_close(self):
30793079
# close() is idempotent.
30803080
screen.close()
30813081

3082+
@requires_curses_func('panel')
3083+
def test_close_then_panel_replace(self):
3084+
# A detached window has no underlying curses window, so replace()
3085+
# must reject it. It used to be accepted, and the panel then
3086+
# crashed inside curses on its next use.
3087+
s = self.make_pty()
3088+
screen = curses.newterm('xterm', s, s)
3089+
win = screen.stdscr
3090+
panel = curses.panel.new_panel(curses.newwin(3, 6, 0, 0))
3091+
# Drop the panel from the global stack before later tests inspect it.
3092+
self.addCleanup(gc_collect)
3093+
screen.close()
3094+
self.assertRaises(curses.panel.error, panel.replace, win)
3095+
# The panel kept its own window, so it still works.
3096+
panel.move(1, 1)
3097+
30823098
@unittest.skipUnless(hasattr(curses, 'new_prescr'),
30833099
'requires curses.new_prescr()')
30843100
def test_new_prescr(self):

Modules/_curses_panel.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,12 @@ _curses_panel_panel_replace_impl(PyCursesPanelObject *self,
594594
return NULL;
595595
}
596596

597+
if (win->win == NULL) {
598+
_curses_panel_state *state = get_curses_panel_state_by_panel(self);
599+
PyErr_SetString(state->error, "the window has been detached");
600+
return NULL;
601+
}
602+
597603
int rtn = replace_panel(self->pan, win->win);
598604
if (rtn == ERR) {
599605
curses_panel_panel_set_error(self, "replace_panel", "replace");

0 commit comments

Comments
 (0)