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

A __del__ of the user pointer could get the panel being deallocated
from top_panel() and crash the interpreter.
This commit is contained in:
Vyron Vasileiadis authored and GitHub committed 2026-09-15 07:51:51 +00:00
1 parent 1bfcc3cc87
commit df02e264c8
3 files changed
+29 -9

No files matched your search

+16
View File
@@ -2431,6 +2431,22 @@ class TestCurses(unittest.TestCase):
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):
@@ -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.
+10 -9
View File
@@ -437,11 +437,11 @@ 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_panel_set_error(self, "set_panel_userptr", NULL);
return -1;
}
Py_DECREF(extra);
}
// self->wo should not be cleared because an associated WINDOW may exist
return 0;
@@ -454,20 +454,21 @@ 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_panel_set_error(po, "set_panel_userptr", "__del__");
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
}
if (po->wo != NULL && remove_lop(po) < 0) {
PyErr_SetString(PyExc_RuntimeError, "__del__: no panel object to delete");
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
}
if (del_panel(po->pan) == ERR && !PyErr_Occurred()) {
curses_panel_panel_set_error(po, "del_panel", "__del__");
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
}
if (po->wo != NULL) {
Py_DECREF(po->wo);
if (remove_lop(po) < 0) {
PyErr_SetString(PyExc_RuntimeError, "__del__: no panel object to delete");
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
}
}
Py_XDECREF(extra);
Py_XDECREF(po->wo);
tp->tp_free(po);
Py_DECREF(tp);
}