Skip to content

Commit 370eb7f

Browse files
fedonmanmiss-islington
authored andcommitted
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. (cherry picked from commit df02e26) Co-authored-by: Vyron Vasileiadis <hi@fedonman.com>
1 parent 5f6fc46 commit 370eb7f

3 files changed

Lines changed: 29 additions & 9 deletions

File tree

Lib/test/test_curses.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,22 @@ def __del__(self):
13891389
panel.set_userptr(A())
13901390
panel.set_userptr(None)
13911391

1392+
@requires_curses_func('panel')
1393+
def test_userptr_dealloc_segfault(self):
1394+
w = curses.newwin(10, 10)
1395+
panel = curses.panel.new_panel(w)
1396+
seen = []
1397+
class A:
1398+
def __del__(self):
1399+
# The panel is being deallocated, so it must already be off
1400+
# the stack: handing it back here would resurrect an object
1401+
# whose refcount is zero -- segfaults.
1402+
seen.append(curses.panel.top_panel() is None)
1403+
panel.set_userptr(A())
1404+
del panel
1405+
gc_collect()
1406+
self.assertEqual(seen, [True])
1407+
13921408
@cpython_only
13931409
@requires_curses_func('panel')
13941410
def test_disallow_instantiation(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash in :mod:`curses.panel` when the finalizer of a panel's user
2+
pointer runs while the panel is being deallocated. The panel is now taken
3+
off the panel stack before its user pointer is dropped.

Modules/_curses_panel.c

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -437,11 +437,11 @@ PyCursesPanel_Clear(PyObject *op)
437437
PyCursesPanelObject *self = _PyCursesPanelObject_CAST(op);
438438
PyObject *extra = (PyObject *)panel_userptr(self->pan);
439439
if (extra != NULL) {
440-
Py_DECREF(extra);
441440
if (set_panel_userptr(self->pan, NULL) == ERR) {
442441
curses_panel_panel_set_error(self, "set_panel_userptr", NULL);
443442
return -1;
444443
}
444+
Py_DECREF(extra);
445445
}
446446
// self->wo should not be cleared because an associated WINDOW may exist
447447
return 0;
@@ -454,20 +454,21 @@ PyCursesPanel_Dealloc(PyObject *self)
454454
PyObject_GC_UnTrack(self);
455455

456456
PyCursesPanelObject *po = _PyCursesPanelObject_CAST(self);
457-
if (PyCursesPanel_Clear(self) < 0) {
457+
PyObject *extra = (PyObject *)panel_userptr(po->pan);
458+
if (extra != NULL && set_panel_userptr(po->pan, NULL) == ERR) {
459+
curses_panel_panel_set_error(po, "set_panel_userptr", "__del__");
460+
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
461+
}
462+
if (po->wo != NULL && remove_lop(po) < 0) {
463+
PyErr_SetString(PyExc_RuntimeError, "__del__: no panel object to delete");
458464
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
459465
}
460466
if (del_panel(po->pan) == ERR && !PyErr_Occurred()) {
461467
curses_panel_panel_set_error(po, "del_panel", "__del__");
462468
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
463469
}
464-
if (po->wo != NULL) {
465-
Py_DECREF(po->wo);
466-
if (remove_lop(po) < 0) {
467-
PyErr_SetString(PyExc_RuntimeError, "__del__: no panel object to delete");
468-
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
469-
}
470-
}
470+
Py_XDECREF(extra);
471+
Py_XDECREF(po->wo);
471472
tp->tp_free(po);
472473
Py_DECREF(tp);
473474
}

0 commit comments

Comments
 (0)