From a4ebf5d1f7342d4c360a72862f76331a917b7e2f Mon Sep 17 00:00:00 2001 From: Michael Forney Date: Sun, 4 May 2025 03:29:44 -0700 Subject: [PATCH] [3.13] bpo-44172: Keep reference to original window in curses subwindow objects (GH-26226) The X/Open curses specification[0] and ncurses documentation[1] both state that subwindows must be deleted before the main window. Deleting the windows in the wrong order causes a double-free with NetBSD's curses implementation. To fix this, keep track of the original window object in the subwindow object, and keep a reference to the original for the lifetime of the subwindow. [0] https://pubs.opengroup.org/onlinepubs/7908799/xcurses/delwin.html [1] https://invisible-island.net/ncurses/man/curs_window.3x.html (cherry picked from commit 0af61fe2f41048d66b0a973bbff056690446d3df) Co-authored-by: Michael Forney Co-authored-by: Serhiy Storchaka --- Include/py_curses.h | 3 ++- Lib/test/test_curses.py | 11 +++++++++- .../2021-05-18-19-12-58.bpo-44172.rJ_-CI.rst | 2 ++ Modules/_cursesmodule.c | 20 +++++++++++-------- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-05-18-19-12-58.bpo-44172.rJ_-CI.rst diff --git a/Include/py_curses.h b/Include/py_curses.h index 79b1b01fcfa594..3e8b16c201f810 100644 --- a/Include/py_curses.h +++ b/Include/py_curses.h @@ -75,10 +75,11 @@ extern "C" { /* Type declarations */ -typedef struct { +typedef struct PyCursesWindowObject { PyObject_HEAD WINDOW *win; char *encoding; + struct PyCursesWindowObject *orig; } PyCursesWindowObject; #define PyCursesWindow_Check(v) Py_IS_TYPE((v), &PyCursesWindow_Type) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 6fe0e7fd4b7fe9..778f040f7a6e77 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -8,7 +8,8 @@ from unittest.mock import MagicMock from test.support import (requires, verbose, SaveSignals, cpython_only, - check_disallow_instantiation, MISSING_C_DOCSTRINGS) + check_disallow_instantiation, MISSING_C_DOCSTRINGS, + gc_collect) from test.support.import_helper import import_module # Optionally test curses module. This currently requires that the @@ -187,6 +188,14 @@ def test_create_windows(self): self.assertEqual(win3.getparyx(), (2, 1)) self.assertEqual(win3.getmaxyx(), (6, 11)) + def test_subwindows_references(self): + win = curses.newwin(5, 10) + win2 = win.subwin(3, 7) + del win + gc_collect() + del win2 + gc_collect() + def test_move_cursor(self): stdscr = self.stdscr win = stdscr.subwin(10, 15, 2, 5) diff --git a/Misc/NEWS.d/next/Library/2021-05-18-19-12-58.bpo-44172.rJ_-CI.rst b/Misc/NEWS.d/next/Library/2021-05-18-19-12-58.bpo-44172.rJ_-CI.rst new file mode 100644 index 00000000000000..d53f3725100eb2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-05-18-19-12-58.bpo-44172.rJ_-CI.rst @@ -0,0 +1,2 @@ +Keep a reference to original :mod:`curses` windows in subwindows so +that the original window does not get deleted before subwindows. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 02b04645036d72..06943aefde6c86 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -666,7 +666,8 @@ Window_TwoArgNoReturnFunction(wresize, int, "ii;lines,columns") /* Allocation and deallocation of Window Objects */ static PyObject * -PyCursesWindow_New(WINDOW *win, const char *encoding) +PyCursesWindow_New(WINDOW *win, const char *encoding, + PyCursesWindowObject *orig) { PyCursesWindowObject *wo; @@ -697,6 +698,8 @@ PyCursesWindow_New(WINDOW *win, const char *encoding) PyErr_NoMemory(); return NULL; } + wo->orig = orig; + Py_XINCREF(orig); return (PyObject *)wo; } @@ -706,6 +709,7 @@ PyCursesWindow_Dealloc(PyCursesWindowObject *wo) if (wo->win != stdscr) delwin(wo->win); if (wo->encoding != NULL) PyMem_Free(wo->encoding); + Py_XDECREF(wo->orig); PyObject_Free(wo); } @@ -1309,7 +1313,7 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1, return NULL; } - return (PyObject *)PyCursesWindow_New(win, NULL); + return (PyObject *)PyCursesWindow_New(win, NULL, self); } /*[clinic input] @@ -2336,7 +2340,7 @@ _curses_window_subwin_impl(PyCursesWindowObject *self, int group_left_1, return NULL; } - return (PyObject *)PyCursesWindow_New(win, self->encoding); + return (PyObject *)PyCursesWindow_New(win, self->encoding, self); } /*[clinic input] @@ -3084,7 +3088,7 @@ _curses_getwin(PyObject *module, PyObject *file) PyErr_SetString(PyCursesError, catchall_NULL); goto error; } - res = PyCursesWindow_New(win, NULL); + res = PyCursesWindow_New(win, NULL, NULL); error: fclose(fp); @@ -3257,7 +3261,7 @@ _curses_initscr_impl(PyObject *module) if (initialised) { wrefresh(stdscr); - return (PyObject *)PyCursesWindow_New(stdscr, NULL); + return (PyObject *)PyCursesWindow_New(stdscr, NULL, NULL); } win = initscr(); @@ -3349,7 +3353,7 @@ _curses_initscr_impl(PyObject *module) SetDictInt("LINES", LINES); SetDictInt("COLS", COLS); - winobj = (PyCursesWindowObject *)PyCursesWindow_New(win, NULL); + winobj = (PyCursesWindowObject *)PyCursesWindow_New(win, NULL, NULL); screen_encoding = winobj->encoding; return (PyObject *)winobj; } @@ -3728,7 +3732,7 @@ _curses_newpad_impl(PyObject *module, int nlines, int ncols) return NULL; } - return (PyObject *)PyCursesWindow_New(win, NULL); + return (PyObject *)PyCursesWindow_New(win, NULL, NULL); } /*[clinic input] @@ -3767,7 +3771,7 @@ _curses_newwin_impl(PyObject *module, int nlines, int ncols, return NULL; } - return (PyObject *)PyCursesWindow_New(win, NULL); + return (PyObject *)PyCursesWindow_New(win, NULL, NULL); } /*[clinic input]