https://github.com/python/cpython/commit/89e3aae7ad19efcc9cc35c5d9a411d2d2a245024
commit: 89e3aae7ad19efcc9cc35c5d9a411d2d2a245024
branch: main
author: Vyron Vasileiadis <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-16T08:29:48+03:00
summary:

gh-155860: Reject a detached window in panel.replace() (GH-155861)

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.

files:
M Doc/library/curses.panel.rst
M Lib/test/test_curses.py
M Modules/_curses_panel.c

diff --git a/Doc/library/curses.panel.rst b/Doc/library/curses.panel.rst
index dd345bff428ad68..50e16847993e148 100644
--- a/Doc/library/curses.panel.rst
+++ b/Doc/library/curses.panel.rst
@@ -116,6 +116,8 @@ Panel objects
 .. method:: panel.replace(win)
 
    Change the window associated with the panel to the window *win*.
+   Raise :exc:`curses.panel.error` if *win* has been detached from its
+   screen by :meth:`screen.close() <curses.screen.close>`.
 
 
 .. method:: panel.set_userptr(obj)
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index d87374a298fc337..08afa587f041954 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -3079,6 +3079,22 @@ def test_close(self):
         # close() is idempotent.
         screen.close()
 
+    @requires_curses_func('panel')
+    def test_close_then_panel_replace(self):
+        # A detached window has no underlying curses window, so replace()
+        # must reject it.  It used to be accepted, and the panel then
+        # crashed inside curses on its next use.
+        s = self.make_pty()
+        screen = curses.newterm('xterm', s, s)
+        win = screen.stdscr
+        panel = curses.panel.new_panel(curses.newwin(3, 6, 0, 0))
+        # Drop the panel from the global stack before later tests inspect it.
+        self.addCleanup(gc_collect)
+        screen.close()
+        self.assertRaises(curses.panel.error, panel.replace, win)
+        # The panel kept its own window, so it still works.
+        panel.move(1, 1)
+
     @unittest.skipUnless(hasattr(curses, 'new_prescr'),
                          'requires curses.new_prescr()')
     def test_new_prescr(self):
diff --git a/Modules/_curses_panel.c b/Modules/_curses_panel.c
index 742a3310bc3528a..78d7bf7c2636465 100644
--- a/Modules/_curses_panel.c
+++ b/Modules/_curses_panel.c
@@ -594,6 +594,12 @@ _curses_panel_panel_replace_impl(PyCursesPanelObject *self,
         return NULL;
     }
 
+    if (win->win == NULL) {
+        _curses_panel_state *state = get_curses_panel_state_by_panel(self);
+        PyErr_SetString(state->error, "the window has been detached");
+        return NULL;
+    }
+
     int rtn = replace_panel(self->pan, win->win);
     if (rtn == ERR) {
         curses_panel_panel_set_error(self, "replace_panel", "replace");

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to