From 1bfcc3cc871d3fab21b77cdc47cdbfcf89fd8b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Tue, 15 Sep 2026 08:26:26 +0200 Subject: [PATCH 1/2] gh-156909: Fix `AST.__repr__` call getting the `_fields` attribute (GH-157490) --- Parser/asdl_c.py | 4 ++-- Python/Python-ast.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index 08a43327766079..2cd34e4eb70f9b 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -1480,8 +1480,8 @@ def visitModule(self, mod): return NULL; } - PyObject *fields; - if (PyObject_GetOptionalAttr((PyObject *)Py_TYPE(self), state->_fields, &fields) < 0) { + PyObject *fields = PyObject_GetAttr((PyObject *)Py_TYPE(self), state->_fields); + if (!fields) { return NULL; } diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 87f160ed19301e..83d058e8357196 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -5792,8 +5792,8 @@ ast_repr_max_depth(AST_object *self, int depth) return NULL; } - PyObject *fields; - if (PyObject_GetOptionalAttr((PyObject *)Py_TYPE(self), state->_fields, &fields) < 0) { + PyObject *fields = PyObject_GetAttr((PyObject *)Py_TYPE(self), state->_fields); + if (!fields) { return NULL; } From df02e264c8b42e0351b36e1c6e25c1e63b7a6a02 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Tue, 15 Sep 2026 03:51:51 -0400 Subject: [PATCH 2/2] gh-156946: Unlink a curses panel before dropping its user pointer (GH-156947) A __del__ of the user pointer could get the panel being deallocated from top_panel() and crash the interpreter. --- Lib/test/test_curses.py | 16 ++++++++++++++++ ...-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst | 3 +++ Modules/_curses_panel.c | 19 ++++++++++--------- 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 7b059eb2e8e141..d779955e236228 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -2431,6 +2431,22 @@ def __del__(self): panel.set_userptr(A()) panel.set_userptr(None) + @requires_curses_func('panel') + def test_userptr_dealloc_segfault(self): + w = curses.newwin(10, 10) + panel = curses.panel.new_panel(w) + seen = [] + class A: + def __del__(self): + # The panel is being deallocated, so it must already be off + # the stack: handing it back here would resurrect an object + # whose refcount is zero -- segfaults. + seen.append(curses.panel.top_panel() is None) + panel.set_userptr(A()) + del panel + gc_collect() + self.assertEqual(seen, [True]) + @cpython_only @requires_curses_func('panel') def test_disallow_instantiation(self): diff --git a/Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst b/Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst new file mode 100644 index 00000000000000..5b72fd3561e804 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-04-16-05-45.gh-issue-156946.0ZYCdh.rst @@ -0,0 +1,3 @@ +Fix a crash in :mod:`curses.panel` when the finalizer of a panel's user +pointer runs while the panel is being deallocated. The panel is now taken +off the panel stack before its user pointer is dropped. diff --git a/Modules/_curses_panel.c b/Modules/_curses_panel.c index 78d7bf7c263646..40d407742b1621 100644 --- a/Modules/_curses_panel.c +++ b/Modules/_curses_panel.c @@ -437,11 +437,11 @@ PyCursesPanel_Clear(PyObject *op) PyCursesPanelObject *self = _PyCursesPanelObject_CAST(op); PyObject *extra = (PyObject *)panel_userptr(self->pan); if (extra != NULL) { - Py_DECREF(extra); if (set_panel_userptr(self->pan, NULL) == ERR) { curses_panel_panel_set_error(self, "set_panel_userptr", NULL); return -1; } + Py_DECREF(extra); } // self->wo should not be cleared because an associated WINDOW may exist return 0; @@ -454,20 +454,21 @@ PyCursesPanel_Dealloc(PyObject *self) PyObject_GC_UnTrack(self); PyCursesPanelObject *po = _PyCursesPanelObject_CAST(self); - if (PyCursesPanel_Clear(self) < 0) { + PyObject *extra = (PyObject *)panel_userptr(po->pan); + if (extra != NULL && set_panel_userptr(po->pan, NULL) == ERR) { + curses_panel_panel_set_error(po, "set_panel_userptr", "__del__"); + PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()"); + } + if (po->wo != NULL && remove_lop(po) < 0) { + PyErr_SetString(PyExc_RuntimeError, "__del__: no panel object to delete"); PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()"); } if (del_panel(po->pan) == ERR && !PyErr_Occurred()) { curses_panel_panel_set_error(po, "del_panel", "__del__"); PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()"); } - if (po->wo != NULL) { - Py_DECREF(po->wo); - if (remove_lop(po) < 0) { - PyErr_SetString(PyExc_RuntimeError, "__del__: no panel object to delete"); - PyErr_FormatUnraisable("Exception ignored in PyCursesPanel_Dealloc()"); - } - } + Py_XDECREF(extra); + Py_XDECREF(po->wo); tp->tp_free(po); Py_DECREF(tp); }