Skip to content

Use-after-free when curses.initscr() follows newterm() #154751

Description

@fedonman

What happened?

When curses.initscr() is called after curses.newterm(), it returns a window that does
not keep its screen alive. The screen can then be collected while the window is still in
use, and the next method call on that window reads freed memory.

The documentation promises the opposite, in the screen objects section
(Doc/library/curses.rst:2052-2055):

   A screen is freed automatically once it is no longer referenced,
   either directly or through one of its windows.
   Each window keeps its screen alive,
   so a screen remains valid as long as any of its windows does.

In the "already initialised" branch of initscr() the window is built with no screen
(Modules/_cursesmodule.c:6576):

    if (curses_initscr_called) {
        cursesmodule_state *state = get_cursesmodule_state(module);
        int code = wrefresh(stdscr);
        ...
        PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL, NULL);

Every other window factory passes the current screen instead: getwin() at
Modules/_cursesmodule.c:6038, newpad() at :7247 and newwin() at :7287 all pass
state->topscreen, and newterm() attaches its own screen at :6795.

Reproducer, only documented APIs:

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')
print("no crash")
$ TERM=xterm ./python repro.py; echo "rc=$?"
Segmentation fault (core dumped)
rc=139

Two things confirm the missing screen reference is the cause:

  • Keeping a proper window of the same screen alive (keep = a.stdscr) before the
    initscr() call makes the same script finish normally. That window does carry a screen
    reference, so the screen survives and the memory stays valid.

  • The window returned by initscr() is not the screen's own window:

    >>> a = curses.newterm('xterm', s, s); curses.set_term(a)
    >>> curses.initscr() is a.stdscr
    False

    So this branch hands out a second, independent wrapper over the same WINDOW *.
    PyCursesWindow_dealloc only skips delwin() when the window is the current screen's
    stdscr, so after a set_term() both wrappers can call delwin() on the same window.

newterm() and set_term() are new in 3.16 (gh-90092, GH-151748), which is also where
the code at :6576 came from, so no released version is affected.

This is not the encoding use-after-free fixed by GH-151696 (gh-151695), which was about
the borrowed curses_screen_encoding pointer and landed before this code existed. It is
also separate from gh-154749, which is about set_term() accepting a screen that has no
terminal at all.

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-curses-initscr-screen-ref:5afbb60e028, Jul 27 2026, 00:05:00) [GCC 15.2.0]

The branch name is local. Its content is unmodified main at 5afbb60.

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