Crash report
What happened?
_curses releases the GIL around ncurses calls (refresh, getch, resizeterm, …) with no lock, while stock ncurses (built without --enable-reentrant, the Ubuntu/Debian default) is not thread-safe and keeps all screen state in global curscr/newscr row buffers. refresh()→doupdate uses those rows while resizeterm()→wresize frees them (wresize.c:227-240). Two Python threads therefore race into a use-after-free read+write of the shared screen → heap corruption → crash.
---ncurses version:
/usr/local/lib/libncursesw.so
/usr/local/lib/libncursesw.so.6
/usr/local/lib/libncursesw.so.6.4
ncurses 6.4.20240113
---OS:
PRETTY_NAME="Ubuntu 24.04.4 LTS"
#!/usr/bin/env python3
"""
Reliable use-after-free crash in CPython's `curses` module.
Root cause (Modules/_cursesmodule.c): several ncurses calls release the GIL
with no lock of their own -- notably window.refresh() -> wrefresh()->doupdate()
(_cursesmodule.c:4447/4459) and curses.resizeterm(). Stock ncurses (built
without --enable-reentrant, as on Ubuntu/Debian) is NOT thread-safe and keeps
all screen state in process-global structures.
doupdate()'s TransformLine() walks and writes the global curscr/newscr row
buffers (ncurses/tty/tty_update.c:1307-1308,1494,1625), while
resizeterm()->wresize() FREES those same buffers (ncurses/base/wresize.c:227-240).
Two Python threads therefore execute ncurses C concurrently on the shared,
unlocked SCREEN -> use-after-free read and write -> heap corruption ->
SIGSEGV / SIGABRT ("corrupted size vs. prev_size", "double free or
corruption").
Run (Unix, any CPython with _curses + non-reentrant ncurses):
python3 poc_uaf_curses.py
Crashes reliably (usually within a couple of seconds). Re-run if it survives
one pass; raising NTHREADS/DURATION increases the chance.
"""
import curses
import sys
import threading
import time
NTHREADS = 8 # refresh threads racing on the shared SCREEN
DURATION = 15 # seconds to race before giving up
def main():
stop = threading.Event()
def hammer(win, tag):
win.nodelay(True)
i = 0
while not stop.is_set():
i += 1
try:
# dirty the window so doupdate has real work, then refresh
# (refresh releases the GIL inside wrefresh()->doupdate()).
win.addstr(0, 0, f"{tag}{i:06d}" + "x" * 40)
win.refresh()
except curses.error:
pass
def resizer():
# Frees + reallocates curscr/newscr/stdscr row buffers via wresize()
# while the refresh threads are inside doupdate() (GIL released).
flip = False
while not stop.is_set():
flip = not flip
try:
curses.resizeterm(24 if flip else 25, 80 if flip else 81)
except curses.error:
pass
def run(stdscr):
curses.curs_set(0)
stdscr.nodelay(True)
wins = [curses.newwin(3, 60, (i % 20), 0) for i in range(NTHREADS)]
threads = [threading.Thread(target=hammer, args=(w, chr(65 + i)),
daemon=True)
for i, w in enumerate(wins)]
threads += [threading.Thread(target=resizer, daemon=True) for _ in range(2)]
for t in threads:
t.start()
t0 = time.monotonic()
while time.monotonic() - t0 < DURATION and not stop.is_set():
try:
stdscr.noutrefresh()
curses.doupdate() # another GIL-releasing racer
except curses.error:
pass
time.sleep(0.01)
stop.set()
curses.wrapper(run)
print("Survived this run -- re-run, or raise NTHREADS/DURATION.",
file=sys.stderr)
if __name__ == "__main__":
main()
I would like to fix this myself by adding locking to the curses module.
CPython versions tested on:
3.16
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:c08c89a, Jul 19 2026, 02:39:49) [GCC 13.3.0]
Linked PRs
Crash report
What happened?
_curses releases the GIL around ncurses calls (refresh, getch, resizeterm, …) with no lock, while stock ncurses (built without --enable-reentrant, the Ubuntu/Debian default) is not thread-safe and keeps all screen state in global curscr/newscr row buffers. refresh()→doupdate uses those rows while resizeterm()→wresize frees them (wresize.c:227-240). Two Python threads therefore race into a use-after-free read+write of the shared screen → heap corruption → crash.
I would like to fix this myself by adding locking to the curses module.
CPython versions tested on:
3.16
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.16.0a0 (heads/main:c08c89a, Jul 19 2026, 02:39:49) [GCC 13.3.0]
Linked PRs