What happened?
PyCursesPanel_Dealloc() in Modules/_curses_panel.c drops the panel's user pointer before it takes the panel off the panel stack and out of the module's internal registry. Dropping that reference can run a __del__, and at that moment top_panel(), bottom_panel(), above() and below() still hand out a new reference to the panel that is being deallocated, whose refcount is already zero. The panel is then torn down a second time and the interpreter dies with SIGSEGV.
Doc/library/curses.panel.rst says the user pointer "can be any Python object", so an object with a finalizer is a documented use, and there is no way for that finalizer to tell it is running inside the panel's own deallocation.
Reproducer:
import pty, curses, curses.panel as P
curses.setupterm(fd=pty.openpty()[1])
curses.initscr()
p = P.new_panel(curses.newwin(3, 6, 0, 0))
class A:
def __del__(self):
print("in __del__: top_panel() ->", P.top_panel())
p.set_userptr(A())
del p
print("survived")
$ TERM=xterm-256color ./python -u -X faulthandler repro.py; echo "rc=$?"
in __del__: top_panel() -> <curses.panel.panel object at 0x700b22abfbb0>
in __del__: top_panel() -> None
Fatal Python error: Segmentation fault
Current thread 0x0000700b23470780 [python] (most recent call first):
File "repro.py", line 9 in <module>
Current thread's C stack trace (most recent call first):
...
Binary file "/usr/lib/x86_64-linux-gnu/libtinfo.so.6", at _nc_screen_of+0x9 [0x700b22925649]
Binary file "/usr/lib/x86_64-linux-gnu/libpanelw.so.6", at del_panel+0x29 [0x700b2290e669]
Binary file ".../_curses_panel.cpython-316-x86_64-linux-gnu.so", at +0x333a [0x700b231d133a]
Binary file "./python", at _Py_Dealloc+0x176 [0x5a6c4d1efb76]
...
rc=139
__del__ runs twice for the same panel: the first call is handed the dying panel, and dropping that reference re-enters the deallocator. survived is never printed.
This is not new in 3.16. The same script segfaults on released 3.14:
$ TERM=xterm-256color /usr/bin/python3.14 -u repro.py; echo "rc=$?"
in __del__: top_panel() -> <_curses_panel.panel object at 0x7eadd6bc02e0>
in __del__: top_panel() -> None
rc=139
The same hazard was fixed for panel.set_userptr() itself in gh-62313, where the fix was to clear the stored pointer before dropping the reference; test_userptr_segfault in Lib/test/test_curses.py still pins it. The deallocator has the same shape and was never covered. gh-155860 is a different panel crash in the same file (replace() with a detached window) and is already fixed.
CPython versions tested on:
CPython main branch, 3.14
Operating systems tested on:
Linux
Linked PRs
What happened?
PyCursesPanel_Dealloc()inModules/_curses_panel.cdrops the panel's user pointer before it takes the panel off the panel stack and out of the module's internal registry. Dropping that reference can run a__del__, and at that momenttop_panel(),bottom_panel(),above()andbelow()still hand out a new reference to the panel that is being deallocated, whose refcount is already zero. The panel is then torn down a second time and the interpreter dies with SIGSEGV.Doc/library/curses.panel.rstsays the user pointer "can be any Python object", so an object with a finalizer is a documented use, and there is no way for that finalizer to tell it is running inside the panel's own deallocation.Reproducer:
__del__runs twice for the same panel: the first call is handed the dying panel, and dropping that reference re-enters the deallocator.survivedis never printed.This is not new in 3.16. The same script segfaults on released 3.14:
The same hazard was fixed for
panel.set_userptr()itself in gh-62313, where the fix was to clear the stored pointer before dropping the reference;test_userptr_segfaultinLib/test/test_curses.pystill pins it. The deallocator has the same shape and was never covered. gh-155860 is a different panel crash in the same file (replace()with a detached window) and is already fixed.CPython versions tested on:
CPython main branch, 3.14
Operating systems tested on:
Linux
Linked PRs