winsch() recurses without bound when wcwidth() returns -1

Serhiy Storchaka <[email protected]> Thu, 30 Jul 2026 19:19:10 +0300
Newsgroups gmane.comp.lib.ncurses.bugs
Message-ID <[email protected]>
ncurses/widechar/lib_ins_wch.c:

     int cells = _nc_wacs_width(CharOf(CHDEREF(wch)));

     if (cells < 0) {
	code = winsch(win, (chtype) CharOf(CHDEREF(wch)));
     } else {

winsch() calls _nc_insert_ch(), which passes a non-control character
to its wide branch, where _nc_build_wch() builds the same character
and gives it back to _nc_insert_wch(), which finds the same negative
width:

     frame #0: _nc_build_wch + 197
     frame #1: _nc_insert_ch + 395
     frame #2: winsch + 41
     frame #3: _nc_insert_ch + 415
     frame #4: winsch + 41
     ...

To repeat:

     /* cc winsch_recurse.c -lncursesw -o winsch_recurse
        LC_ALL=en_US.ISO8859-15 TERM=xterm ./winsch_recurse 0xA4 */
     #include <curses.h>
     #include <locale.h>
     #include <stdio.h>

     int main(void)
     {
         FILE *f = fopen("/dev/null", "w+");

         setlocale(LC_ALL, "");
         newterm("xterm", f, f);
         winsch(stdscr, (chtype) 0xA4);
         fprintf(stderr, "ok\n");
         endwin();
         return 0;
     }

It takes a byte that is not a control character and whose wcwidth() is
negative.  FreeBSD 15.1 has 57 of them in en_US.ISO8859-15 and 54 in
uk_UA.KOI8-U, because there wchar_t holds the locale value rather than
a code point; each crashes with the system libncursesw.so.9
(6.6.20251230) and with the 2026-07-19 snapshot.  On glibc such a byte
does not seem to exist, and the same snapshot does not crash in any
locale I tried.  UTF-8 locales are unaffected on both.

Found through Python's curses module, whose test suite calls insstr().

A character that the locale cannot represent has no width to insert,
so returning ERR ends the recursion:

--- a/ncurses/widechar/lib_ins_wch.c
+++ b/ncurses/widechar/lib_ins_wch.c
@@
      if (cells < 0) {
-	code = winsch(win, (chtype) CharOf(CHDEREF(wch)));
+	code = ERR;
      } else {

With that the reproducer returns for every byte and locale I tried,
and a byte with a positive width is unaffected.  I do not know whether
ERR is the right return there, or whether such a character should be
treated as one cell.