https://github.com/python/cpython/commit/0af61fe2f41048d66b0a973bbff056690446d3df
commit: 0af61fe2f41048d66b0a973bbff056690446d3df
branch: main
author: Michael Forney <mfor...@mforney.org>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2025-05-04T13:29:44+03:00
summary:

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

Co-authored-by: Serhiy Storchaka <storch...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2021-05-18-19-12-58.bpo-44172.rJ_-CI.rst
M Include/py_curses.h
M Lib/test/test_curses.py
M Modules/_cursesmodule.c

diff --git a/Include/py_curses.h b/Include/py_curses.h
index e11bfedb17d205..49fc3c9d127aa6 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 PyCurses_CAPSULE_NAME "_curses._C_API"
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index 3f95a2b322fbb1..c307258e5657e2 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
@@ -181,6 +182,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 f2ddd55dbf3900..cd185bc2b02ea5 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -787,7 +787,8 @@ Window_TwoArgNoReturnFunction(wresize, int, 
"ii;lines,columns")
 
 static PyObject *
 PyCursesWindow_New(cursesmodule_state *state,
-                   WINDOW *win, const char *encoding)
+                   WINDOW *win, const char *encoding,
+                   PyCursesWindowObject *orig)
 {
     if (encoding == NULL) {
 #if defined(MS_WINDOWS)
@@ -821,6 +822,8 @@ PyCursesWindow_New(cursesmodule_state *state,
         PyErr_NoMemory();
         return NULL;
     }
+    wo->orig = orig;
+    Py_XINCREF(orig);
     PyObject_GC_Track((PyObject *)wo);
     return (PyObject *)wo;
 }
@@ -838,6 +841,7 @@ PyCursesWindow_dealloc(PyObject *self)
     if (wo->encoding != NULL) {
         PyMem_Free(wo->encoding);
     }
+    Py_XDECREF(wo->orig);
     window_type->tp_free(self);
     Py_DECREF(window_type);
 }
@@ -846,6 +850,8 @@ static int
 PyCursesWindow_traverse(PyObject *self, visitproc visit, void *arg)
 {
     Py_VISIT(Py_TYPE(self));
+    PyCursesWindowObject *wo = (PyCursesWindowObject *)self;
+    Py_VISIT(wo->orig);
     return 0;
 }
 
@@ -1453,7 +1459,7 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, 
int group_left_1,
     }
 
     cursesmodule_state *state = get_cursesmodule_state_by_win(self);
-    return PyCursesWindow_New(state, win, NULL);
+    return PyCursesWindow_New(state, win, NULL, self);
 }
 
 /*[clinic input]
@@ -2493,7 +2499,7 @@ _curses_window_subwin_impl(PyCursesWindowObject *self, 
int group_left_1,
     }
 
     cursesmodule_state *state = get_cursesmodule_state_by_win(self);
-    return PyCursesWindow_New(state, win, self->encoding);
+    return PyCursesWindow_New(state, win, self->encoding, self);
 }
 
 /*[clinic input]
@@ -3237,7 +3243,7 @@ _curses_getwin(PyObject *module, PyObject *file)
         goto error;
     }
     cursesmodule_state *state = get_cursesmodule_state(module);
-    res = PyCursesWindow_New(state, win, NULL);
+    res = PyCursesWindow_New(state, win, NULL, NULL);
 
 error:
     fclose(fp);
@@ -3410,7 +3416,7 @@ _curses_initscr_impl(PyObject *module)
     if (curses_initscr_called) {
         wrefresh(stdscr);
         cursesmodule_state *state = get_cursesmodule_state(module);
-        return PyCursesWindow_New(state, stdscr, NULL);
+        return PyCursesWindow_New(state, stdscr, NULL, NULL);
     }
 
     win = initscr();
@@ -3514,7 +3520,7 @@ _curses_initscr_impl(PyObject *module)
 #undef SetDictInt
 
     cursesmodule_state *state = get_cursesmodule_state(module);
-    PyObject *winobj = PyCursesWindow_New(state, win, NULL);
+    PyObject *winobj = PyCursesWindow_New(state, win, NULL, NULL);
     if (winobj == NULL) {
         return NULL;
     }
@@ -3898,7 +3904,7 @@ _curses_newpad_impl(PyObject *module, int nlines, int 
ncols)
     }
 
     cursesmodule_state *state = get_cursesmodule_state(module);
-    return PyCursesWindow_New(state, win, NULL);
+    return PyCursesWindow_New(state, win, NULL, NULL);
 }
 
 /*[clinic input]
@@ -3939,7 +3945,7 @@ _curses_newwin_impl(PyObject *module, int nlines, int 
ncols,
     }
 
     cursesmodule_state *state = get_cursesmodule_state(module);
-    return PyCursesWindow_New(state, win, NULL);
+    return PyCursesWindow_New(state, win, NULL, NULL);
 }
 
 /*[clinic input]

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to