Skip to content

Segfault after curses.set_term() is given a new_prescr() screen #154749

Description

@fedonman

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions