https://github.com/python/cpython/commit/9c48dfe9927249be0c73e533e387740742c4a95f
commit: 9c48dfe9927249be0c73e533e387740742c4a95f
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T18:53:59+03:00
summary:
gh-155877: Fix a NUL in a curses character cell (GH-156158)
setcchar() takes a NUL-terminated string, so a NUL sharing a cell dropped
the rest of the cell, and an empty cell read back as complexchar('').
Reject a NUL that shares a cell or that joins a complexstr, and read an
empty cell as complexchar('\0'), as a narrow build already does.
files:
M Lib/test/test_curses.py
M Modules/_cursesmodule.c
diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py
index ac0110c671455c..ea2dcd76b585a9 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -1085,6 +1085,37 @@ def test_output_string_pair_restored(self):
curses.A_BOLD)
self.assertEqual(win.attr_get()[1], pair)
+ def test_cell_embedded_null_chars(self):
+ # A NUL cannot share a cell with another character: setcchar() takes a
+ # NUL-terminated string, so the rest of the cell would be dropped.
+ for text in ['a\0', 'a\0\u0301', 'a\0b', '\0a']:
+ with self.subTest(text=text):
+ self.assertRaises(ValueError, curses.complexchar, text)
+ if WIDE_BUILD:
+ self.assertRaises(ValueError, self.stdscr.addch, text)
+
+ def test_cell_null_char(self):
+ # A lone NUL is a character like any other, as addch(0) always was.
+ stdscr = self.stdscr
+ cell = curses.complexchar('\0')
+ self.assertEqual(str(cell), '\0')
+ self.assertEqual(eval(repr(cell), {'curses': curses}), cell)
+ stdscr.erase()
+ stdscr.addch(0, 0, 0)
+ expected = stdscr.instr(0, 0, 4)
+ for ch in ['\0', cell]:
+ with self.subTest(ch=ch):
+ stdscr.erase()
+ stdscr.addch(0, 0, ch)
+ self.assertEqual(stdscr.instr(0, 0, 4), expected)
+ # A cell holding a NUL reads back as the cell that writes it.
+ win = curses.newwin(3, 8, 0, 0)
+ win.insch(0, 0, '\0')
+ self.assertEqual(win.in_wch(0, 0), cell)
+ # A string of cells cannot hold a NUL: it would end a batch write.
+ self.assertRaises(ValueError, curses.complexstr, 'a\0b')
+ self.assertRaises(ValueError, curses.complexstr, '\0')
+
def test_add_string_behavior(self):
# addstr() advances the cursor past the written text; addnstr()
# writes at most n characters.
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index e208b2d3d52acc..7cc72b96d0a46d 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -561,6 +561,10 @@ PyCurses_ConvertToWideCell(PyObject *obj, wchar_t *wch)
setcchar() would silently drop a trailing spacing character, or fail
with a generic error for a control-character base. */
if (nch > 1) {
+ if (wmemchr(wch, L'\0', nch) != NULL) {
+ PyErr_SetString(PyExc_ValueError, "embedded null character");
+ return -1;
+ }
int bad = wcwidth(wch[0]) < 0;
for (Py_ssize_t i = 1; !bad && i < nch; i++) {
bad = wcwidth(wch[i]) != 0;
@@ -908,7 +912,9 @@ curses_cell_text(cursesmodule_state *state, const
curses_cell_t *cell)
PyErr_SetString(state->error, "getcchar() returned ERR");
return NULL;
}
- return PyUnicode_FromWideChar(wstr, -1);
+ /* setcchar() stores no text for a NUL (it takes a NUL-terminated string),
+ so an empty cell holds a NUL character, as on a narrow build. */
+ return PyUnicode_FromWideChar(wstr, wstr[0] == L'\0' ? 1 : -1);
#else
char ch = (char)(*cell & A_CHARTEXT);
return PyUnicode_Decode(&ch, 1, curses_screen_encoding, NULL);
@@ -1318,6 +1324,16 @@ static PyObject *
complexstr_from_string(cursesmodule_state *state, PyObject *str,
attr_t attr, int pair)
{
+ /* A NUL cell ends a batch write and a cell array read (add_wchnstr(3X)),
+ so a string of cells cannot hold one, as addstr() cannot either. */
+ Py_ssize_t nul = PyUnicode_FindChar(str, 0, 0, PyUnicode_GET_LENGTH(str),
1);
+ if (nul < -1) {
+ return NULL;
+ }
+ if (nul >= 0) {
+ PyErr_SetString(PyExc_ValueError, "embedded null character");
+ return NULL;
+ }
#ifdef HAVE_NCURSESW
Py_ssize_t n;
wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]