Skip to content

Commit f428cb2

Browse files
committed
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.
1 parent e3287f6 commit f428cb2

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

Lib/test/test_curses.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,25 @@ def test_output_string_embedded_null_chars(self):
895895
self.assertRaises(ValueError, stdscr.insstr, arg)
896896
self.assertRaises(ValueError, stdscr.insnstr, arg, 1)
897897

898+
def test_cell_embedded_null_chars(self):
899+
# A cell cannot hold a NUL: setcchar() keeps only the text before it,
900+
# so reject it instead of silently truncating the cell.
901+
stdscr = self.stdscr
902+
for text in ['a\0', '\0', 'a\0\u0301', 'a\0b']:
903+
with self.subTest(text=text):
904+
self.assertRaises(ValueError, curses.complexchar, text)
905+
self.assertRaises(ValueError, curses.complexstr, text)
906+
self.assertRaises(ValueError, curses.complexstr, [text])
907+
if WIDE_BUILD:
908+
self.assertRaises(ValueError, stdscr.addch, 'a\0\u0301')
909+
# A lone NUL is still written as a character, like addch(0).
910+
stdscr.erase()
911+
stdscr.addch(0, 0, 0)
912+
expected = stdscr.instr(0, 0, 4)
913+
stdscr.erase()
914+
stdscr.addch(0, 0, '\0')
915+
self.assertEqual(stdscr.instr(0, 0, 4), expected)
916+
898917
def test_add_string_behavior(self):
899918
# addstr() advances the cursor past the written text; addnstr()
900919
# writes at most n characters.

Modules/_cursesmodule.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@ PyCurses_ConvertToWideCell(PyObject *obj, wchar_t *wch)
561561
setcchar() would silently drop a trailing spacing character, or fail
562562
with a generic error for a control-character base. */
563563
if (nch > 1) {
564+
if (wmemchr(wch, L'\0', nch) != NULL) {
565+
PyErr_SetString(PyExc_ValueError, "embedded null character");
566+
return -1;
567+
}
564568
int bad = wcwidth(wch[0]) < 0;
565569
for (Py_ssize_t i = 1; !bad && i < nch; i++) {
566570
bad = wcwidth(wch[i]) != 0;
@@ -835,6 +839,10 @@ static int
835839
curses_cell_pack(cursesmodule_state *state, curses_cell_t *cell,
836840
PyObject *text, attr_t attr, int pair, const char *funcname)
837841
{
842+
if (PyUnicode_FindChar(text, 0, 0, PyUnicode_GET_LENGTH(text), 1) >= 0) {
843+
PyErr_SetString(PyExc_ValueError, "embedded null character");
844+
return -1;
845+
}
838846
#ifdef HAVE_NCURSESW
839847
wchar_t wstr[CCHARW_MAX + 1];
840848
if (PyCurses_ConvertToWideCell(text, wstr) < 0) {
@@ -1318,6 +1326,10 @@ static PyObject *
13181326
complexstr_from_string(cursesmodule_state *state, PyObject *str,
13191327
attr_t attr, int pair)
13201328
{
1329+
if (PyUnicode_FindChar(str, 0, 0, PyUnicode_GET_LENGTH(str), 1) >= 0) {
1330+
PyErr_SetString(PyExc_ValueError, "embedded null character");
1331+
return NULL;
1332+
}
13211333
#ifdef HAVE_NCURSESW
13221334
Py_ssize_t n;
13231335
wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n);

0 commit comments

Comments
 (0)