Skip to content

gh-154751: Keep the screen alive in curses.initscr() after newterm()#154752

Open
fedonman wants to merge 3 commits into
python:mainfrom
fedonman:fix-curses-initscr-screen-ref
Open

gh-154751: Keep the screen alive in curses.initscr() after newterm()#154752
fedonman wants to merge 3 commits into
python:mainfrom
fedonman:fix-curses-initscr-screen-ref

Conversation

@fedonman

@fedonman fedonman commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

curses.initscr() called while a screen from curses.newterm() is current took the
"already initialised" branch, which built the returned window with no screen:

PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL, NULL);

The window's screen field is the reference that keeps a screen alive, so this window
kept nothing alive. Dropping the screen then freed its WINDOW, and the next method call
on the window read freed memory:

import curses, gc, os
s1 = os.openpty()[1]; s2 = os.openpty()[1]
a = curses.newterm('xterm', s1, s1)
b = curses.newterm('xterm', s2, s2)
curses.set_term(a)
w = curses.initscr()
curses.set_term(b)
del a; gc.collect()
w.addstr(0, 0, 'boom')     # Segmentation fault, rc=139

It now passes state->topscreen, matching newwin(), newpad() and getwin(). That
value is NULL for the screen created by initscr(), which has no screen object, so plain
initscr() use is unchanged.

This also restores what the screen objects documentation already promises: "Each window
keeps its screen alive, so a screen remains valid as long as any of its windows does."

One thing this does not change: on this branch initscr() still returns a second window
object wrapping the same WINDOW * as screen.stdscr, so curses.initscr() is screen.stdscr is False and both wrappers can call delwin() on it after a set_term().
Returning the screen's existing window instead would fix that too, but it changes what
initscr() returns, so it seemed better to keep this change to the memory bug and leave
that decision separate.

newterm() and set_term() are new in 3.16 (gh-90092, GH-151748), so no released version
is affected.

After the change the reproducer above prints no crash and exits 0. Tests:

$ ./python -m test -u all -v test_curses -m 'test_initscr_after_newterm_keeps_screen_alive'
test_initscr_after_newterm_keeps_screen_alive (test.test_curses.ScreenTests.test_initscr_after_newterm_keeps_screen_alive) ... ok
Ran 1 test in 0.210s
OK

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

…erm()

When initscr() was called while a screen from newterm() was current, it
returned a window created with no screen. The window therefore did not hold a
reference to the screen it belonged to, so the screen could be collected while
the window was still in use, and the next method call on the window read freed
memory.

It now passes the current screen, like newwin(), newpad() and getwin() already
do. That is NULL for the screen created by initscr(), which has no screen
object, so that case is unchanged.
@serhiy-storchaka
serhiy-storchaka self-requested a review July 27, 2026 12:06
Comment thread Modules/_cursesmodule.c
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
@serhiy-storchaka

Copy link
Copy Markdown
Member

Returning the screen's existing window instead would fix that too

And this is right.

Please add a test for this case.

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.

2 participants