Skip to content

gh-155860: Reject a detached window in panel.replace() - #155861

Open
fedonman wants to merge 1 commit into
python:mainfrom
fedonman:fix-panel-replace-null-window
Open

gh-155860: Reject a detached window in panel.replace()#155861
fedonman wants to merge 1 commit into
python:mainfrom
fedonman:fix-panel-replace-null-window

Conversation

@fedonman

@fedonman fedonman commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

curses.screen.close() detaches the screen's standard window: the wrapper object stays alive but the curses window behind it is gone. panel.replace() passed that window straight to replace_panel(), so the panel ended up holding a NULL window and curses dereferenced it on the panel's next use.

This adds the missing check before replace_panel(), raising curses.panel.error the way new_panel() already does when handed the same window. new_panel() is left alone: it already rejects the window, so there is nothing to fix there.

The reproducer from the issue now stops at replace() instead of dying:

$ TERM=xterm-256color ./python repro.py; echo "rc=$?"
Traceback (most recent call last):
  File "/home/vyron-vasileiadis/projects/forks/cpython/repro.py", line 7, in <module>
    panel.replace(win)              # accepted, stores a NULL window
    ~~~~~~~~~~~~~^^^^^
curses.panel.error: the window has been detached
rc=1

The new test fails without the change to Modules/_curses_panel.c. With that hunk reverted it reports FAIL, then takes the whole process down in del_panel() at teardown:

test_close_then_panel_replace (test.test_curses.ScreenTests.test_close_then_panel_replace) ... FAIL
  Binary file "/usr/lib/x86_64-linux-gnu/libpanelw.so.6", at del_panel+0x90 [0x734e0319a6d0]

With it, and the module as a whole, plus a refleak run:

test_close_then_panel_replace (test.test_curses.ScreenTests.test_close_then_panel_replace) ... ok

$ ./python -m test test_curses -u curses
Total tests: run=169 skipped=3
Result: SUCCESS

$ ./python -m test test_curses -u curses -R 3:3
Total tests: run=169 skipped=3
Result: SUCCESS

No NEWS entry: screen.close() is new in 3.16 and unreleased, so no released version can reach this crash. The change is news-exempt rather than missing an entry.

curses.screen.close() detaches the screen's standard window: the wrapper
object stays alive but the curses window behind it is gone.
panel.replace() did not check for that, so it stored the detached window
in the panel and curses dereferenced it on the panel's next use, killing
the interpreter with SIGSEGV.  Raise curses.panel.error instead, the way
new_panel() already does on the same window.
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #34083186 | 📁 Comparing 110da9d against main (948fd7e)

  🔍 Preview build  

1 file changed
± library/curses.panel.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant