Skip to content

Commit f63df37

Browse files
[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 df02e26) Co-authored-by: Vyron Vasileiadis <hi@fedonman.com>
1 parent c698c63 commit f63df37

3 files changed

Lines changed: 33 additions & 6 deletions

File tree

Lib/test/test_curses.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,22 @@ def __del__(self):
13791379
panel.set_userptr(A())
13801380
panel.set_userptr(None)
13811381

1382+
@requires_curses_func('panel')
1383+
def test_userptr_dealloc_segfault(self):
1384+
w = curses.newwin(10, 10)
1385+
panel = curses.panel.new_panel(w)
1386+
seen = []
1387+
class A:
1388+
def __del__(self):
1389+
# The panel is being deallocated, so it must already be off
1390+
# the stack: handing it back here would resurrect an object
1391+
# whose refcount is zero -- segfaults.
1392+
seen.append(curses.panel.top_panel() is None)
1393+
panel.set_userptr(A())
1394+
del panel
1395+
gc_collect()
1396+
self.assertEqual(seen, [True])
1397+
13821398
@cpython_only
13831399
@requires_curses_func('panel')
13841400
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: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,13 @@ PyCursesPanel_Clear(PyObject *op)
311311
PyCursesPanelObject *self = _PyCursesPanelObject_CAST(op);
312312
PyObject *extra = (PyObject *)panel_userptr(self->pan);
313313
if (extra != NULL) {
314-
Py_DECREF(extra);
315314
if (set_panel_userptr(self->pan, NULL) == ERR) {
316315
_curses_panel_state *state = get_curses_panel_state_by_panel(self);
317316
PyErr_SetString(state->PyCursesError,
318317
"set_panel_userptr() returned ERR");
319318
return -1;
320319
}
320+
Py_DECREF(extra);
321321
}
322322
// self->wo should not be cleared because an associated WINDOW may exist
323323
return 0;
@@ -330,18 +330,26 @@ PyCursesPanel_Dealloc(PyObject *self)
330330
PyObject_GC_UnTrack(self);
331331

332332
PyCursesPanelObject *po = _PyCursesPanelObject_CAST(self);
333-
if (PyCursesPanel_Clear(self) < 0) {
333+
PyObject *extra = (PyObject *)panel_userptr(po->pan);
334+
if (extra != NULL && set_panel_userptr(po->pan, NULL) == ERR) {
335+
_curses_panel_state *state = get_curses_panel_state_by_panel(po);
336+
PyErr_SetString(state->PyCursesError,
337+
"set_panel_userptr() returned ERR");
334338
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
335339
}
340+
if (po->wo != NULL) {
341+
remove_lop(po);
342+
if (PyErr_Occurred()) {
343+
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
344+
}
345+
}
336346
if (del_panel(po->pan) == ERR && !PyErr_Occurred()) {
337347
_curses_panel_state *state = get_curses_panel_state_by_panel(po);
338348
PyErr_SetString(state->PyCursesError, "del_panel() returned ERR");
339349
PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()");
340350
}
341-
if (po->wo != NULL) {
342-
Py_DECREF(po->wo);
343-
remove_lop(po);
344-
}
351+
Py_XDECREF(extra);
352+
Py_XDECREF(po->wo);
345353
tp->tp_free(po);
346354
Py_DECREF(tp);
347355
}

0 commit comments

Comments
 (0)