What happened?
curses.set_term() accepts a screen returned by curses.new_prescr(), and the next
window refresh then segfaults the interpreter.
new_prescr() returns a screen that has no terminal attached. The documentation says so:
it is "a new screen that can be used to call functions that affect global state before
initscr() or newterm() is called". set_term() documents its argument as "a screen
returned by newterm()", but it does not check that, so it will happily make a
terminal-less screen the current one:
// Modules/_cursesmodule.c:6833-6837
PyCursesScreenObject *so = curses_check_screen(module, screen);
if (so == NULL) {
return NULL;
}
set_term(so->screen);
curses_check_screen() only rejects objects that are not screens and screens that are
already closed. After that, any wrefresh() dereferences NULL inside ncurses and the
process dies with SIGSEGV.
Reproducer, five lines and only documented APIs:
import curses, os
fd = os.openpty()[1]
scr = curses.newterm('xterm', fd, fd)
curses.set_term(curses.new_prescr())
scr.stdscr.refresh()
print("no crash")
$ TERM=xterm ./python repro.py; echo "rc=$?"
Segmentation fault (core dumped)
rc=139
The stack is inside ncurses, reached from window.refresh():
libncursesw.so.6, at doupdate_sp+0x274
libncursesw.so.6, at wrefresh+0x4f
_curses.cpython-316-x86_64-linux-gnu.so
It is not specific to the window that belongs to the switched-away screen:
curses.newwin(3, 3).refresh() after the same set_term() call also gives rc=139, and so
does the initscr() variant run under pty.fork().
Two things narrow it down:
- Switching between two real
newterm() screens and refreshing does not crash, so the
problem is the terminal-less screen, not cross-screen refresh in general.
- In the same broken state,
curses.doupdate() and curses.endwin() both raise
curses.error rather than crashing, so only the refresh path is affected.
A new_prescr() screen is distinguishable from a newterm() screen at the C level: the
screen object's stdscr_win is NULL for the former and set for the latter, which is also
visible from Python as screen.stdscr being None:
>>> curses.new_prescr().stdscr is None
True
So set_term() can reject exactly this input. Raising curses.error for a screen with no
standard window looks like the right behaviour, since such a screen has no terminal to
become current, but a maintainer may prefer to make the refresh path safe instead.
set_term() and new_prescr() are both new in 3.16 (gh-90092, GH-151748), so this has
never been in a release.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/fix-gh-153970-calledprocesserror-none-dirty:0aea374a54, Jul 26 2026, 18:25) [GCC 15.2.0]
The build is from a local branch, but Modules/_cursesmodule.c and
Doc/library/curses.rst in it are byte identical to main at 5afbb60, so the crash is
reproducible on a clean main build.
Linked PRs
What happened?
curses.set_term()accepts a screen returned bycurses.new_prescr(), and the nextwindow refresh then segfaults the interpreter.
new_prescr()returns a screen that has no terminal attached. The documentation says so:it is "a new screen that can be used to call functions that affect global state before
initscr()ornewterm()is called".set_term()documents its argument as "a screenreturned by
newterm()", but it does not check that, so it will happily make aterminal-less screen the current one:
curses_check_screen()only rejects objects that are not screens and screens that arealready closed. After that, any
wrefresh()dereferences NULL inside ncurses and theprocess dies with SIGSEGV.
Reproducer, five lines and only documented APIs:
The stack is inside ncurses, reached from
window.refresh():It is not specific to the window that belongs to the switched-away screen:
curses.newwin(3, 3).refresh()after the sameset_term()call also gives rc=139, and sodoes the
initscr()variant run underpty.fork().Two things narrow it down:
newterm()screens and refreshing does not crash, so theproblem is the terminal-less screen, not cross-screen refresh in general.
curses.doupdate()andcurses.endwin()both raisecurses.errorrather than crashing, so only the refresh path is affected.A
new_prescr()screen is distinguishable from anewterm()screen at the C level: thescreen object's
stdscr_winis NULL for the former and set for the latter, which is alsovisible from Python as
screen.stdscrbeingNone:So
set_term()can reject exactly this input. Raisingcurses.errorfor a screen with nostandard window looks like the right behaviour, since such a screen has no terminal to
become current, but a maintainer may prefer to make the refresh path safe instead.
set_term()andnew_prescr()are both new in 3.16 (gh-90092, GH-151748), so this hasnever been in a release.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/fix-gh-153970-calledprocesserror-none-dirty:0aea374a54, Jul 26 2026, 18:25) [GCC 15.2.0]
The build is from a local branch, but
Modules/_cursesmodule.candDoc/library/curses.rstin it are byte identical to main at 5afbb60, so the crash isreproducible on a clean main build.
Linked PRs