`infocmp -E` heap overflow in `safe_name()`
Daniel Anderson <[email protected]> Wed, 13 May 2026 21:46:57 -0400
| Newsgroups | gmane.comp.lib.ncurses.bugs |
|---|---|
| Message-ID | <[email protected]> |
Good evening,
I found a heap-buffer overflow in infocmp's safe_name().
`infocmp -x -E` can overflow a heap buffer when dumping a terminfo entry with a long extended capability name. The bug is in `progs/infocmp.c:safe_name()`: the function keeps a static pointer to a heap buffer, allocates it only on the first call, sizes it from that first call's arguments, and reuses it for later calls even when later names require more space.
In a build without `--enable-string-hacks`, `_nc_SPRINTF` maps to `sprintf`, so the later call can write past the original allocation. An AddressSanitizer build confirms a heap-buffer-overflow through `sprintf`, with the stack going through `safe_name()`, `dump_initializers()`, and `main()`.
I’m attaching a suggested fix, and the AddressSanitizer report. A POC is available on request.
Full disclosure: I am building an open source bug and vulnerability scanner, N184, and used it to locate memory-safety issues in ncurses. You can find it here: https://github.com/MillaFleurs/N184
Thank you,
Dan
=================================================================
==48448==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x603000001c71 at pc 0x00010353af7c bp 0x00016d46d9d0 sp 0x00016d46d170
WRITE of size 336 at 0x603000001c71 thread T0
#0 0x00010353af78 in memcpy+0x260 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x8ef78)
#1 0x00019f723c1c in __sfvwrite+0x14c (libsystem_c.dylib:arm64e+0x4c1c)
#2 0x00019f723664 in __vfprintf+0x2bfc (libsystem_c.dylib:arm64e+0x4664)
#3 0x00019f72b800 in vsprintf_l+0xc8 (libsystem_c.dylib:arm64e+0xc800)
#4 0x0001034cb428 in vsprintf+0x6c (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x1f428)
#5 0x0001034cbae8 in sprintf+0x38 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x1fae8)
#6 0x0001029a1150 in safe_name infocmp.c:1324
#7 0x000102994910 in dump_initializers infocmp.c:1450
#8 0x000102992228 in main infocmp.c:2022
#9 0x00019f4c9d50 in start+0x1c0c (dyld:arm64e+0x8d50)
0x603000001c71 is located 0 bytes after 17-byte region [0x603000001c60,0x603000001c71)
allocated by thread T0 here:
#0 0x0001034ed164 in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x41164)
#1 0x0001029a0f30 in safe_name infocmp.c:1308
#2 0x0001029940a4 in dump_initializers infocmp.c:1425
#3 0x000102992228 in main infocmp.c:2022
#4 0x00019f4c9d50 in start+0x1c0c (dyld:arm64e+0x8d50)
SUMMARY: AddressSanitizer: heap-buffer-overflow (libsystem_c.dylib:arm64e+0x4c1c) in __sfvwrite+0x14c
Shadow bytes around the buggy address:
0x603000001980: fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00
0x603000001a00: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
0x603000001a80: 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00
0x603000001b00: fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa 00 00
0x603000001b80: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
=>0x603000001c00: 00 00 00 fa fa fa 00 00 00 02 fa fa 00 00[01]fa
0x603000001c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x603000001d00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x603000001d80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x603000001e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x603000001e80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==48448==ABORTING
patch.diff
(application/octet-stream, 1.2 KB)
diff --git a/progs/infocmp.c b/progs/infocmp.c
--- a/progs/infocmp.c
+++ b/progs/infocmp.c
@@ -1297,19 +1297,29 @@ static char *
safe_name(const char *format, const char *prefix, const char *name)
{
static char *result;
static size_t need;
+ size_t want;
char *s;
- if (result == NULL) {
- need = (strlen(prefix)
- + strlen(name)
- + strlen(format));
- result = (char *) malloc(need + 1);
+ /*
+ * Leave room for the optional "ti_" prefix, optional leading '_',
+ * the formatted name, and the trailing NUL.
+ */
+ want = strlen(prefix) + strlen(name) + strlen(format) + 5;
+ if (want > need) {
+ char *next = (char *) realloc(result, want);
+ if (next == NULL)
+ failed("safe_name");
+ result = next;
+ need = want;
+ }
+ if (result == NULL) {
+ result = (char *) malloc(need);
if (result == NULL)
failed("safe_name");
}
_nc_STRCPY(result, "", need);
@@ -1320,7 +1330,7 @@ safe_name(const char *format, const char *prefix, const char *name)
*s = 0;
if (isdigit(UChar(*name)) && !*prefix)
*s++ = '_';
- _nc_SPRINTF(s, _nc_SLIMIT(need) format, name);
+ _nc_SPRINTF(s, _nc_SLIMIT(need - (size_t) (s - result)) format, name);
return result;
}