winsch()/insch() ignores the locale for an 8-bit character, unlike waddch()

Serhiy Storchaka <[email protected]> Mon, 29 Jun 2026 09:32:53 +0300
Newsgroups gmane.comp.lib.ncurses.bugs
Message-ID <[email protected]>
ncurses 6.6.20251231, wide-character build (libncursesw), Linux/glibc.
(The code path is present in every wide build I checked, ncurses 5.9 
through 6.6.)

On a wide-character build, inserting a printable 8-bit byte > 127 with
winsch()/insch() stores the byte as a code point directly, without 
converting it
through the locale, whereas waddch() with the same byte runs the normal 
mbtowc
conversion.  So under a non-Latin-1 8-bit locale addch() decodes the 
byte and
insch() does not.

Under en_US.ISO-8859-15, byte 0xA4 is EURO SIGN (U+20AC): addch(0xA4) 
stores the
euro, but insch(0xA4) stores U+00A4 instead -- a different character.  For a
Latin-1 locale the bug is invisible, since there the byte equals the 
code point
(0xA4 -> U+00A4 either way), which is presumably why it has gone unnoticed.

   #include <locale.h>
   #include <curses.h>
   #include <stdio.h>

   int main(void) {
       char a[4] = {0}, b[4] = {0};
       setlocale(LC_ALL, "");
       initscr();
       addch(0xA4);                 /* addch decodes the byte via the 
locale */
       mvinsch(1, 0, 0xA4);         /* insch under test */
       mvwinnstr(stdscr, 0, 0, a, 1);
       mvwinnstr(stdscr, 1, 0, b, 1);
       endwin();
       fprintf(stderr, "addch(0xA4): winnstr=0x%02X\n", (unsigned 
char)a[0]);
       fprintf(stderr, "insch(0xA4): winnstr=0x%02X\n", (unsigned 
char)b[0]);
       return 0;
   }

   $ cc repro.c -o repro -lncursesw
   $ TERM=xterm LC_ALL=en_US.ISO-8859-15 ./repro

winnstr() reads each cell back as a locale byte.  Expected, with insch 
matching
addch, is 0xA4 for both.  Actual:

   addch(0xA4): winnstr=0xA4         (the euro, read back correctly)
   insch(0xA4): winnstr=0x20         (a blank: U+00A4 is not in 
ISO-8859-15, so
                                      the inserted euro was lost)

(Use addch(0xA4), not addstr("euro"): a UTF-8 source literal is misread 
under an
8-bit locale.)

In ncurses/base/lib_insch.c, _nc_insert_ch() handles a printable 8-bit 
byte with
a fast path that builds the cell straight from the byte:

       if (... (isprint(ch8) || ...)) {
           ...
           SetChar2(wch, ch);          /* byte used directly as the code 
point */
       }

The locale conversion, _nc_build_wch() (mbtowc), is only reached in the 
trailing
"else" branch, for a byte that is not isprint() in the current locale.
waddch()/wadd_wch() route every non-ASCII byte through _nc_build_wch(), so
addch() decodes 0xA4 to U+20AC while insch() short-circuits to U+00A4.  
A fix
would let the wide build take the _nc_build_wch() path for an 8-bit byte 
in the
insert case too (the isprint() fast path is fine for ASCII).

This is the write-side companion to the winch()/inch() locale issue reported
separately: there winnstr() decodes a cell via the locale while winch() 
returns
the truncated code point.  Both come from the legacy 8-bit chtype API 
treating a
byte inconsistently across functions on a wide build.