From f428cb2c2a4983f830030dd9b02108b7685d051c Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sun, 16 Aug 2026 03:53:26 +0300 Subject: [PATCH] gh-155877: Reject an embedded NUL in curses cell text setcchar() stops at the first NUL, so a cell text containing one was silently truncated: complexchar('a\0\u0301') dropped the combining character, and complexchar('\0') built a cell with no text, whose repr() is not a valid constructor call and which terminates the cchar_t array passed to add_wchnstr(). The two cell construction paths, curses_cell_pack() and complexstr_from_string(), now reject any NUL, and the shared converter rejects a NUL inside a multi-character cell. A lone NUL still reaches the write methods, so addch('\0') keeps writing what addch(0) writes. --- Lib/test/test_curses.py | 19 +++++++++++++++++++ Modules/_cursesmodule.c | 12 ++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index d87374a298fc337..a9cdc392483e017 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -895,6 +895,25 @@ def test_output_string_embedded_null_chars(self): self.assertRaises(ValueError, stdscr.insstr, arg) self.assertRaises(ValueError, stdscr.insnstr, arg, 1) + def test_cell_embedded_null_chars(self): + # A cell cannot hold a NUL: setcchar() keeps only the text before it, + # so reject it instead of silently truncating the cell. + stdscr = self.stdscr + for text in ['a\0', '\0', 'a\0\u0301', 'a\0b']: + with self.subTest(text=text): + self.assertRaises(ValueError, curses.complexchar, text) + self.assertRaises(ValueError, curses.complexstr, text) + self.assertRaises(ValueError, curses.complexstr, [text]) + if WIDE_BUILD: + self.assertRaises(ValueError, stdscr.addch, 'a\0\u0301') + # A lone NUL is still written as a character, like addch(0). + stdscr.erase() + stdscr.addch(0, 0, 0) + expected = stdscr.instr(0, 0, 4) + stdscr.erase() + stdscr.addch(0, 0, '\0') + self.assertEqual(stdscr.instr(0, 0, 4), expected) + 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 383de378670ea97..4389a5b9e07c4f5 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; @@ -835,6 +839,10 @@ static int curses_cell_pack(cursesmodule_state *state, curses_cell_t *cell, PyObject *text, attr_t attr, int pair, const char *funcname) { + if (PyUnicode_FindChar(text, 0, 0, PyUnicode_GET_LENGTH(text), 1) >= 0) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + return -1; + } #ifdef HAVE_NCURSESW wchar_t wstr[CCHARW_MAX + 1]; if (PyCurses_ConvertToWideCell(text, wstr) < 0) { @@ -1318,6 +1326,10 @@ static PyObject * complexstr_from_string(cursesmodule_state *state, PyObject *str, attr_t attr, int pair) { + if (PyUnicode_FindChar(str, 0, 0, PyUnicode_GET_LENGTH(str), 1) >= 0) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + return NULL; + } #ifdef HAVE_NCURSESW Py_ssize_t n; wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);