`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;
 }