https://github.com/python/cpython/commit/f63df3746cd439bae6abbc99e3deb0c5b44bdf9a
commit: f63df3746cd439bae6abbc99e3deb0c5b44bdf9a
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-15T08:25:53Z
summary:

[3.13] gh-156946: Unlink a curses panel before dropping its user pointer 
(GH-156947) (GH-157542)

A __del__ of the user pointer could get the panel being deallocated
from top_panel() and crash the interpreter.
(cherry picked from commit df02e264c8b42e0351b36e1c6e25c1e63b7a6a02)

Co-authored-by: Vyron Vasileiadis <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst
M Lib/test/test_curses.py
M Modules/_curses_panel.c

diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index fdd98df721c2e38..2489dbc5c467f2f 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -1379,6 +1379,22 @@ def __del__(self):
         panel.set_userptr(A())
         panel.set_userptr(None)
 
+    @requires_curses_func('panel')
+    def test_userptr_dealloc_segfault(self):
+        w = curses.newwin(10, 10)
+        panel = curses.panel.new_panel(w)
+        seen = []
+        class A:
+            def __del__(self):
+                # The panel is being deallocated, so it must already be off
+                # the stack: handing it back here would resurrect an object
+                # whose refcount is zero -- segfaults.
+                seen.append(curses.panel.top_panel() is None)
+        panel.set_userptr(A())
+        del panel
+        gc_collect()
+        self.assertEqual(seen, [True])
+
     @cpython_only
     @requires_curses_func('panel')
     def test_disallow_instantiation(self):
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst 
b/Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst
new file mode 100644
index 000000000000000..5b72fd3561e804c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst
@@ -0,0 +1,3 @@
+Fix a crash in :mod:`curses.panel` when the finalizer of a panel's user
+pointer runs while the panel is being deallocated.  The panel is now taken
+off the panel stack before its user pointer is dropped.
diff --git a/Modules/_curses_panel.c b/Modules/_curses_panel.c
index cf6fac42ca1aa0d..765a78e5937c320 100644
--- a/Modules/_curses_panel.c
+++ b/Modules/_curses_panel.c
@@ -311,13 +311,13 @@ PyCursesPanel_Clear(PyObject *op)
     PyCursesPanelObject *self = _PyCursesPanelObject_CAST(op);
     PyObject *extra = (PyObject *)panel_userptr(self->pan);
     if (extra != NULL) {
-        Py_DECREF(extra);
         if (set_panel_userptr(self->pan, NULL) == ERR) {
             _curses_panel_state *state = get_curses_panel_state_by_panel(self);
             PyErr_SetString(state->PyCursesError,
                             "set_panel_userptr() returned ERR");
             return -1;
         }
+        Py_DECREF(extra);
     }
     // self->wo should not be cleared because an associated WINDOW may exist
     return 0;
@@ -330,18 +330,26 @@ PyCursesPanel_Dealloc(PyObject *self)
     PyObject_GC_UnTrack(self);
 
     PyCursesPanelObject *po = _PyCursesPanelObject_CAST(self);
-    if (PyCursesPanel_Clear(self) < 0) {
+    PyObject *extra = (PyObject *)panel_userptr(po->pan);
+    if (extra != NULL && set_panel_userptr(po->pan, NULL) == ERR) {
+        _curses_panel_state *state = get_curses_panel_state_by_panel(po);
+        PyErr_SetString(state->PyCursesError,
+                        "set_panel_userptr() returned ERR");
         PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
     }
+    if (po->wo != NULL) {
+        remove_lop(po);
+        if (PyErr_Occurred()) {
+            PyErr_FormatUnraisable("Exception ignored in 
PyCursesPanel_Dealloc()");
+        }
+    }
     if (del_panel(po->pan) == ERR && !PyErr_Occurred()) {
         _curses_panel_state *state = get_curses_panel_state_by_panel(po);
         PyErr_SetString(state->PyCursesError, "del_panel() returned ERR");
         PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
     }
-    if (po->wo != NULL) {
-        Py_DECREF(po->wo);
-        remove_lop(po);
-    }
+    Py_XDECREF(extra);
+    Py_XDECREF(po->wo);
     tp->tp_free(po);
     Py_DECREF(tp);
 }

_______________________________________________
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