gh-155877: Fix a NUL in a curses character cell (GH-156158)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.