Re: ncurses 5 ABI compatibility mode can have spaces replaced with null bytes

Thomas Dickey <[email protected]> Sun, 19 Jul 2026 19:16:57 -0400
Newsgroups gmane.comp.lib.ncurses.bugs
Message-ID <[email protected]>
--YDQRr2p1JhIF0aQN
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable

On Sun, Jul 19, 2026 at 02:54:54PM +0100, Platon wrote:
> Hi!
>=20
> I've encountered an interesting issue when running a program compiled for=
 ncurses 5 when linked to ncurses 6 built in compatibility mode (--with-abi=
-version=3D5).
> If a program tries to output colored text, then all spaces between words =
on screen vanish. Looks like it's because internally ncurses 5 was using (o=
r was allowing) null bytes to mean "blank cell", but in ncurses 6 it doesn'=
t work that way anymore.
>=20
> There are no sources for this program, so I had to dig into assembler to =
figure out what was going on. I managed to make a reproducer that showcases=
 the issue:
> ```
> #include <ncurses.h>
> #include <stdio.h>
>=20
> int main(void)
> {
>     initscr();
>=20
>     start_color();
>     if (has_colors()) {
>         init_pair(6, 3, 0);
>     }
>=20
>     /* I suspect it should have been something like `attron(COLOR_PAIR(6)=
)`, */
>     /* but without access to source code it's hard to tell. */
>     *(long *)((char *)stdscr + 0x10) =3D (long)(6 << 8);

use the source (ncurses.h declaration of WINDOW):
----------------------------------------------------------------------
struct _win_st
{
	NCURSES_SIZE_T _cury, _curx; /* current cursor position */

	/* window location and size */
	NCURSES_SIZE_T _maxy, _maxx; /* maximums of x and y, NOT window size */
	NCURSES_SIZE_T _begy, _begx; /* screen coords of upper-left-hand corner */

	short   _flags;		/* window state flags */

	/* attribute tracking */
	attr_t  _attrs;		/* current attribute for non-space character */
	chtype  _bkgd;		/* current background char/attribute pair */
----------------------------------------------------------------------=20
It looks as if your assignment is overwriting the background character,
which will do what you're describing.

Using the API, that's checked to ensure it's not null
----------------------------------------------------------------------
bkgd(3NCURSES)                   Library calls                   bkgd(3NCUR=
SES)

NAME
       bkgdset,  wbkgdset,  bkgd,  wbkgd,  getbkgd - manipulate background =
of a
       curses window of characters

SYNOPSIS
       #include <curses.h>

       int bkgd(chtype ch);
       int wbkgd(WINDOW *win, chtype ch);

       void bkgdset(chtype ch);
       void wbkgdset(WINDOW *win, chtype ch);

       chtype getbkgd(WINDOW *win);
----------------------------------------------------------------------
>     wmove(stdscr, 0, 0);
>     waddnstr(stdscr, "A D O M", -1);
>     wrefresh(stdscr);
>=20
>     wgetch(stdscr);
>     endwin();
>     return 0;
> }
> /* compile with gcc repro.c -o repro /usr/lib/libncurses.so.5 */
> ```
>=20
> This program will show "ADOM", without spaces between characters. That's =
because spaces become null bytes in the final output.
>=20
> The main problem is in this (decompiled) line:
> ```
> *(long *)((char *)stdscr + 0x10) =3D (long)(6 << 8);
> ```
> Here's the assembly for reference:
> ```
> 00298cd5 8b 05 d1        MOV        EAX,dword ptr [DAT_008e69ac]
>          dc 64 00
> 00298cdb c1 e0 04        SHL        EAX,0x4
> 00298cde 01 c7           ADD        EDI,EAX
> 00298ce0 48 63 ff        MOVSXD     RDI,EDI
> 00298ce3 48 c1 e7 08     SHL        RDI,0x8
> 00298ce7 48 89 7a 10     MOV        qword ptr [RDX + 0x10],RDI
> ```
> (DAT_008e69ac contains 0 here, EDI contains 6)
>=20
> According to the _win_st struct definition, it looks like offset 0x10 is =
_attrs field. However, _attrs field is only 4 bytes (I think), after it com=
es _bkgd field, which contains 0x20 on startup.
> And so writing 8 bytes to (stdscr + 0x10) overwrites the _bkgd with zeros=
, replacing space with null byte, which is used for output going forward.
>=20
> When ran with a "real" ncurses 5 library (I've managed to find one in Deb=
ian's libncurses5), this problem isn't manifesting - the entire 8 bytes are=
 zero from the start.
>=20
> I would appreciate any opinions on this situation.
> Did I guess correctly about changes in handling of null bytes between ncu=
rses 5 and 6?
> Could this construct (writing 8 bytes to _attrs) be a result from normal =
ncurses 5 API usage, or did the program author access undocumented opaque t=
ype field manually?
> Is it possible to adjust ncurses 6 behavior to output null bytes as space=
s when compiled with `--with-abi-version=3D5`, or will it be too difficult?
>=20
> I'm on Arch Linux, x86_64.
> The program in question is the ADOM game (one of the more famous roguelik=
es, I believe). Unfortunately, the sources are not available for it and las=
t release happened in 2019, so simply recompiling the program for newer ncu=
rses version isn't possible.
>=20
>=20

--=20
Thomas E. Dickey <[email protected]>
https://invisible-island.net

--YDQRr2p1JhIF0aQN
Content-Type: application/pgp-signature; name="signature.asc"

-----BEGIN PGP SIGNATURE-----

iQGzBAABCgAdFiEEGYgtkt2kxADCLA1WzCr0RyFnvgMFAmpdWuUACgkQzCr0RyFn
vgOzbAwA8kFesJYGFHko0TE+1IBdbVSyo5ZHxBGiZT1uUipXIDbqzjKFSbY70IjY
LDjMaOBhDpRIlzdxCAUXytCVkwXSaT+FI7E5sj9cmS9mF5uuElxxIOJ6DNpChYFg
ywFHLzAlzHLXnb3MF0wvHbvqd1Fk2+4snow78WjiNdYNXxJMbVbcBhmyQZVvT9YT
7izCfjFHKuVavnGWdcYPvejDBfAW5o333VMgLaxl8rxyRz5AXxnMmTItkSBBM/9p
0Rr+iNWqaDRozoAmXuP6q4UJ5suwSG3YAxKx/9Ebr6F6qdYaQnOolrZbImY9Xve2
hpQ7TDPLD9ku1ti2CA6RLjXxJcUOZAOdsgV9LVxTUlAtW3V69lCHn0eGiT/p0Hh8
D8X+XQmozG78sqyqT7g/kS9IICvcI5e4d7j6E7BeXlqkIDj8dgrSMXTWN1pKTNaC
OHPYRIooYDwVC06vj7Gvv8juL8S5WtyjuFkr7AN387b5sfFAahJSN0GNk+TOMwxT
HpEAdy9o
=nyB4
-----END PGP SIGNATURE-----

--YDQRr2p1JhIF0aQN--