[PATCH] vasnprintf: fix ("%*d", INT_MAX, n)

Paul Eggert <[email protected]>
Newsgroups gmane.comp.lib.gnulib.bugs
Message-ID <[email protected]>
I ran into this problem when doing stress testing with GNU m4.
The problem is that ("%*d", INT_MAX, n) fails because the code
refuses to create a buffer of size INT_MAX + 1u,
due to problems when dealing with older nonconforming snprintf.
The simplest fix was to use snprintf only if it handles
sizes like INT_MAX + 1u, and to fall back on sprintf otherwise.
Nowadays I think snprintf should work on most practical targets.
Assuming this patch works out, perhaps we could simplify
lib/vasnprintf.c to ease further maintenance,
as nowadays I hope we can assume both HAVE_SNPRINTF_RETVAL_C99 and
HAVE_SNPRINTF_TRUNCATION_C99 on platforms where USE_SNPRINTF.
* lib/vasnprintf.c: Include minmax.h.
(USE_SNPRINTF): Define to 1 only if glibc 2+, Android, musl,
the BSDs, macOS, or Microsoft UCRT.
(VASNPRINTF) [USE_SNPRINTF]:
Allow maxlen to be 1 greater than INT_MAX on 64-bit platforms.
Document the limit’s derivation by using named locals.
Do not attempt to work around bugs in pre-C99 implementations,
as USE_SNPRINTF is pickier now.
* m4/vasnprintf.m4 (gl_PREREQ_VASNWPRINTF):
Move gl_MUSL_LIBC call from here ...
(gl_PREREQ_VASNXPRINTF): ... to here.
* modules/c-vasnprintf, modules/unistdio/u16-u16-vasnprintf:
* modules/unistdio/u16-vasnprintf:
* modules/unistdio/u32-u32-vasnprintf:
* modules/unistdio/u32-vasnprintf:
* modules/unistdio/u8-u8-vasnprintf:
* modules/unistdio/u8-vasnprintf, modules/unistdio/ulc-vasnprintf:
* modules/vasnprintf, modules/vasnwprintf:
(Files): Add m4/musl.m4 if it isn’t there already.
(Depends-on): Add minmax.
* tests/test-vasnprintf-posix.c:
* tests/test-vasprintf-posix.c:
* tests/test-vaszprintf-posix.c:
Include <limits.h> and <errno.h> if needed.
(test_function): Test for the bug, if RUN_EXPENSIVE_TESTS=yes
in the environment; this the coreutils tradition and I
didn’t see any Gnulib tradition so I just used it.
---
 ChangeLog                           | 42 +++++++++++++++++++++++
 doc/posix-functions/snprintf.texi   |  8 +++++
 doc/posix-functions/vsnprintf.texi  |  8 +++++
 lib/vasnprintf.c                    | 52 +++++++++++++++++------------
 m4/vasnprintf.m4                    |  4 +--
 modules/c-vasnprintf                |  2 ++
 modules/unistdio/u16-u16-vasnprintf |  2 ++
 modules/unistdio/u16-vasnprintf     |  2 ++
 modules/unistdio/u32-u32-vasnprintf |  2 ++
 modules/unistdio/u32-vasnprintf     |  2 ++
 modules/unistdio/u8-u8-vasnprintf   |  2 ++
 modules/unistdio/u8-vasnprintf      |  2 ++
 modules/unistdio/ulc-vasnprintf     |  2 ++
 modules/vasnprintf                  |  2 ++
 modules/vasnwprintf                 |  1 +
 tests/test-vasnprintf-posix.c       | 20 +++++++++++
 tests/test-vasprintf-posix.c        | 21 ++++++++++++
 tests/test-vaszprintf-posix.c       | 22 ++++++++++++
 18 files changed, 173 insertions(+), 23 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 6ba52936a5..fcddd3f1d6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,45 @@
+2026-07-06  Paul Eggert  <[email protected]>
+
+	vasnprintf: fix ("%*d", INT_MAX, n)
+	I ran into this problem when doing stress testing with GNU m4.
+	The problem is that ("%*d", INT_MAX, n) fails because the code
+	refuses to create a buffer of size INT_MAX + 1u,
+	due to problems when dealing with older nonconforming snprintf.
+	The simplest fix was to use snprintf only if it handles
+	sizes like INT_MAX + 1u, and to fall back on sprintf otherwise.
+	Nowadays I think snprintf should work on most practical targets.
+	Assuming this patch works out, perhaps we could simplify
+	lib/vasnprintf.c to ease further maintenance,
+	as nowadays I hope we can assume both HAVE_SNPRINTF_RETVAL_C99 and
+	HAVE_SNPRINTF_TRUNCATION_C99 on platforms where USE_SNPRINTF.
+	* lib/vasnprintf.c: Include minmax.h.
+	(USE_SNPRINTF): Define to 1 only if glibc 2+, Android, musl,
+	the BSDs, macOS, or Microsoft UCRT.
+	(VASNPRINTF) [USE_SNPRINTF]:
+	Allow maxlen to be 1 greater than INT_MAX on 64-bit platforms.
+	Document the limit’s derivation by using named locals.
+	Do not attempt to work around bugs in pre-C99 implementations,
+	as USE_SNPRINTF is pickier now.
+	* m4/vasnprintf.m4 (gl_PREREQ_VASNWPRINTF):
+	Move gl_MUSL_LIBC call from here ...
+	(gl_PREREQ_VASNXPRINTF): ... to here.
+	* modules/c-vasnprintf, modules/unistdio/u16-u16-vasnprintf:
+	* modules/unistdio/u16-vasnprintf:
+	* modules/unistdio/u32-u32-vasnprintf:
+	* modules/unistdio/u32-vasnprintf:
+	* modules/unistdio/u8-u8-vasnprintf:
+	* modules/unistdio/u8-vasnprintf, modules/unistdio/ulc-vasnprintf:
+	* modules/vasnprintf, modules/vasnwprintf:
+	(Files): Add m4/musl.m4 if it isn’t there already.
+	(Depends-on): Add minmax.
+	* tests/test-vasnprintf-posix.c:
+	* tests/test-vasprintf-posix.c:
+	* tests/test-vaszprintf-posix.c:
+	Include <limits.h> and <errno.h> if needed.
+	(test_function): Test for the bug, if RUN_EXPENSIVE_TESTS=yes
+	in the environment; this the coreutils tradition and I
+	didn’t see any Gnulib tradition so I just used it.
+
 2026-07-04  Simon Josefsson  <[email protected]>
 
 	doc: Mention gnulib-202607 git bundle.
diff --git a/doc/posix-functions/snprintf.texi b/doc/posix-functions/snprintf.texi
index a84ffd40ed..2a75e2fd32 100644
--- a/doc/posix-functions/snprintf.texi
+++ b/doc/posix-functions/snprintf.texi
@@ -138,6 +138,14 @@ snprintf
 Portability problems not fixed by Gnulib:
 @itemize
 @item
+This function fails if the buffer size exceeds @code{INT_MAX},
+even if the resulting string length would fit in @code{int}:
+Solaris, z/OS.
+@item
+This function fails if the buffer size exceeds @code{INT_MAX + 1u},
+even if the resulting string length would fit in @code{int}:
+FreeBSD, OpenBSD, NetBSD, macOS.
+@item
 The @code{%m} directive is not portable, use @code{%s} mapped to an
 argument of @code{strerror(errno)} (or a version of @code{strerror_r})
 instead.
diff --git a/doc/posix-functions/vsnprintf.texi b/doc/posix-functions/vsnprintf.texi
index 21acf8776c..52d7dacda1 100644
--- a/doc/posix-functions/vsnprintf.texi
+++ b/doc/posix-functions/vsnprintf.texi
@@ -136,6 +136,14 @@ vsnprintf
 Portability problems not fixed by Gnulib:
 @itemize
 @item
+This function fails if the buffer size exceeds @code{INT_MAX},
+even if the resulting string length would fit in @code{int}:
+Solaris, z/OS.
+@item
+This function fails if the buffer size exceeds @code{INT_MAX + 1u},
+even if the resulting string length would fit in @code{int}:
+FreeBSD, OpenBSD, NetBSD, macOS.
+@item
 The @code{%m} directive is not portable, use @code{%s} mapped to an
 argument of @code{strerror(errno)} (or a version of @code{strerror_r})
 instead.
diff --git a/lib/vasnprintf.c b/lib/vasnprintf.c
index 366a437863..98d8e4a757 100644
--- a/lib/vasnprintf.c
+++ b/lib/vasnprintf.c
@@ -110,6 +110,7 @@
 #include "xsize.h"
 
 #include "attribute.h"
+#include "minmax.h"
 
 #if NEED_PRINTF_DOUBLE || NEED_PRINTF_LONG_DOUBLE || (NEED_WPRINTF_DIRECTIVE_LA && WIDE_CHAR_VERSION)
 # include <math.h>
@@ -217,11 +218,17 @@
 #if !WIDE_CHAR_VERSION || !DCHAR_IS_TCHAR
   /* TCHAR_T is char.  */
   /* Use snprintf if it exists under the name 'snprintf' or '_snprintf'.
-     But don't use it on BeOS, since BeOS snprintf produces no output if the
-     size argument is >= 0x3000000.
-     Also don't use it on Linux libc5, since there snprintf with size = 1
-     writes any output without bounds, like sprintf.  */
-# if (HAVE_DECL__SNPRINTF || HAVE_SNPRINTF) && !defined __BEOS__ && !(__GNU_LIBRARY__ == 1)
+     But don't use it if it has problems.  For example,
+     Solaris, QNX and z/OS sprintf fail if size == INT_MAX + 1u,
+     BeOS produces no output if 0x3000000 <= size,
+     and Linux libc5 with size = 1 writes without bounds, like sprintf.
+     BSD snprintf, which fails if size == INT_MAX + 2u, is OK for us.
+     Use snprintf only on known-safe platforms:
+     glibc 2, Android, musl, the BSDs, macOS, Microsoft UCRT.  */
+# if ((HAVE_SNPRINTF || HAVE_DECL__SNPRINTF) \
+      && (2 <= __GLIBC__ || __ANDROID__ || MUSL_LIBC \
+          || __FreeBSD__ || __DragonFly__ || __NetBSD__ || __OpenBSD__ \
+          || (__APPLE__ && __MACH__) || _UCRT))
 #  define USE_SNPRINTF 1
 # else
 #  define USE_SNPRINTF 0
@@ -6858,12 +6865,20 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp,
 
 #if USE_SNPRINTF
                     int retcount = 0;
-                    size_t maxlen = allocated - length;
-                    /* SNPRINTF can fail if its second argument is
-                       > INT_MAX.  */
-                    if (maxlen > INT_MAX / TCHARS_PER_DCHAR)
-                      maxlen = INT_MAX / TCHARS_PER_DCHAR;
-                    maxlen = maxlen * TCHARS_PER_DCHAR;
+
+                    /* Keep size (in bytes) in ptrdiff_t and size_t range.
+                       Also, generate at most INT_MAX + 1 characters
+                       counting the trailing null, as that is the
+                       maximum the API allows.  */
+                    size_t
+                      bytes_max = MIN (PTRDIFF_MAX, SIZE_MAX),
+                      tchars_max = bytes_max / sizeof (TCHAR_T),
+                      API_max = MIN (tchars_max - 1, INT_MAX),
+                      maxlen_max = (API_max + 1
+                                    - (API_max + 1) % TCHARS_PER_DCHAR),
+                      maxlen = (MIN (allocated - length,
+                                     maxlen_max / TCHARS_PER_DCHAR)
+                                * TCHARS_PER_DCHAR);
 # define SNPRINTF_BUF(arg) \
                     switch (prefix_count)                                   \
                       {                                                     \
@@ -7219,17 +7234,13 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp,
                       }
 
 #if USE_SNPRINTF
-                    /* Handle overflow of the allocated buffer.
-                       If such an overflow occurs, a C99 compliant snprintf()
-                       returns a count >= maxlen.  However, a non-compliant
-                       snprintf() function returns only count = maxlen - 1.  To
-                       cover both cases, test whether count >= maxlen - 1.  */
-                    if ((unsigned int) count + 1 >= maxlen)
+                    /* Handle overflow of the allocated buffer.  */
+                    if (count >= maxlen)
                       {
                         /* If maxlen already has attained its allowed maximum,
                            allocating more memory will not increase maxlen.
                            Instead of looping, bail out.  */
-                        if (maxlen == INT_MAX / TCHARS_PER_DCHAR)
+                        if (maxlen == maxlen_max)
                           goto overflow;
                         else
                           {
@@ -7239,9 +7250,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp,
                                bytes, so that in the next round, we likely get
                                  maxlen > (unsigned int) count + 1
                                and so we don't get here again.
-                               And allocate proportionally, to avoid looping
-                               eternally if snprintf() reports a too small
-                               count.  */
+                               And allocate proportionally, to avoid
+                               quadratic behavior in large buffers.  */
                             size_t n =
                               xmax (xsum (length,
                                           ((unsigned int) count + 2
diff --git a/m4/vasnprintf.m4 b/m4/vasnprintf.m4
index 4e01fbc9e6..ff067b2a0f 100644
--- a/m4/vasnprintf.m4
+++ b/m4/vasnprintf.m4
@@ -1,5 +1,5 @@
 # vasnprintf.m4
-# serial 56
+# serial 57
 dnl Copyright (C) 2002-2004, 2006-2026 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -208,7 +208,6 @@ AC_DEFUN_ONCE([gl_PREREQ_VASNWPRINTF]
          the 'lc' directive.])
       ;;
   esac
-  gl_MUSL_LIBC
   gl_PREREQ_VASNXPRINTF
 ])
 
@@ -217,6 +216,7 @@ AC_DEFUN_ONCE([gl_PREREQ_VASNXPRINTF]
 [
   AC_REQUIRE([AC_FUNC_ALLOCA])
   AC_REQUIRE([gt_TYPE_WINT_T])
+  gl_MUSL_LIBC
   AC_CHECK_FUNCS([wcslen])
   dnl Knowing DBL_EXPBIT0_WORD and DBL_EXPBIT0_BIT enables an optimization
   dnl in the code for NEED_PRINTF_LONG_DOUBLE || NEED_PRINTF_DOUBLE.
diff --git a/modules/c-vasnprintf b/modules/c-vasnprintf
index 37cfd14a5c..1fe7963249 100644
--- a/modules/c-vasnprintf
+++ b/modules/c-vasnprintf
@@ -19,11 +19,13 @@ m4/vasnprintf.m4
 m4/printf.m4
 m4/math_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 assert-h
 attribute
 limits-h
+minmax
 stdint-h
 stdio-h
 mixin/printf-posix
diff --git a/modules/unistdio/u16-u16-vasnprintf b/modules/unistdio/u16-u16-vasnprintf
index 58707f65f5..0645ee37a5 100644
--- a/modules/unistdio/u16-u16-vasnprintf
+++ b/modules/unistdio/u16-u16-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -44,6 +45,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/unistdio/u16-vasnprintf b/modules/unistdio/u16-vasnprintf
index b040f5ab44..53db0ef47c 100644
--- a/modules/unistdio/u16-vasnprintf
+++ b/modules/unistdio/u16-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -44,6 +45,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/unistdio/u32-u32-vasnprintf b/modules/unistdio/u32-u32-vasnprintf
index 48ef0bd074..0ff115b01c 100644
--- a/modules/unistdio/u32-u32-vasnprintf
+++ b/modules/unistdio/u32-u32-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -44,6 +45,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/unistdio/u32-vasnprintf b/modules/unistdio/u32-vasnprintf
index 9ff85ec1ce..1928b3fa2d 100644
--- a/modules/unistdio/u32-vasnprintf
+++ b/modules/unistdio/u32-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -44,6 +45,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/unistdio/u8-u8-vasnprintf b/modules/unistdio/u8-u8-vasnprintf
index 3907151c26..f58da96343 100644
--- a/modules/unistdio/u8-u8-vasnprintf
+++ b/modules/unistdio/u8-u8-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -44,6 +45,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/unistdio/u8-vasnprintf b/modules/unistdio/u8-vasnprintf
index 1da2109bec..f68a450300 100644
--- a/modules/unistdio/u8-vasnprintf
+++ b/modules/unistdio/u8-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -44,6 +45,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/unistdio/ulc-vasnprintf b/modules/unistdio/ulc-vasnprintf
index 02aa89a936..e6b44915b4 100644
--- a/modules/unistdio/ulc-vasnprintf
+++ b/modules/unistdio/ulc-vasnprintf
@@ -12,6 +12,7 @@ m4/intmax_t.m4
 m4/stdint_h.m4
 m4/inttypes_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 unistdio/base
@@ -42,6 +43,7 @@ memchr
 multiarch
 assert-h
 mbszero
+minmax
 streq
 
 configure.ac:
diff --git a/modules/vasnprintf b/modules/vasnprintf
index 798b9db377..80468bad92 100644
--- a/modules/vasnprintf
+++ b/modules/vasnprintf
@@ -18,6 +18,7 @@ m4/vasnprintf.m4
 m4/printf.m4
 m4/math_h.m4
 m4/exponentd.m4
+m4/musl.m4
 
 Depends-on:
 stdio-h
@@ -35,6 +36,7 @@ assert-h
 wchar-h
 mbszero
 mbsnlen
+minmax
 streq
 
 configure.ac:
diff --git a/modules/vasnwprintf b/modules/vasnwprintf
index 128a3c602b..d61016b374 100644
--- a/modules/vasnwprintf
+++ b/modules/vasnwprintf
@@ -41,6 +41,7 @@ mbszero
 mbrtowc
 wmemcpy
 wmemset
+minmax
 streq
 
 configure.ac:
diff --git a/tests/test-vasnprintf-posix.c b/tests/test-vasnprintf-posix.c
index fe0a59d647..98e3319920 100644
--- a/tests/test-vasnprintf-posix.c
+++ b/tests/test-vasnprintf-posix.c
@@ -22,6 +22,7 @@
 
 #include <errno.h>
 #include <float.h>
+#include <limits.h>
 #include <stdarg.h>
 #include <stdcountof.h>
 #include <stddef.h>
@@ -167,6 +168,25 @@ test_function (char * (*my_asnprintf) (char *, size_t *, const char *, ...))
     free (result);
   }
 
+  {
+    char *expensive = getenv ("RUN_EXPENSIVE_TESTS");
+    if (expensive && streq (expensive, "yes"))
+      { /* Large width given as argument.  */
+        size_t length;
+        char *result =
+          my_asnprintf (NULL, &length, "%*d", INT_MAX, INT_MIN);
+        if (!result)
+          ASSERT (errno == ENOMEM);
+        else
+          {
+            ASSERT (atoi (result) == INT_MIN);
+            ASSERT (length == INT_MAX);
+            ASSERT (length == strlen (result));
+            free (result);
+          }
+      }
+  }
+
   /* Test the support of the 'a' and 'A' conversion specifier for hexadecimal
      output of floating-point numbers.  */
 
diff --git a/tests/test-vasprintf-posix.c b/tests/test-vasprintf-posix.c
index 6d62468cce..86ae5c12a7 100644
--- a/tests/test-vasprintf-posix.c
+++ b/tests/test-vasprintf-posix.c
@@ -20,7 +20,9 @@
 
 #include <stdio.h>
 
+#include <errno.h>
 #include <float.h>
+#include <limits.h>
 #include <stdarg.h>
 #include <stdcountof.h>
 #include <stddef.h>
@@ -146,6 +148,25 @@ test_function (int (*my_asprintf) (char **, const char *, ...))
     free (result);
   }
 
+  {
+    char *expensive = getenv ("RUN_EXPENSIVE_TESTS");
+    if (expensive && streq (expensive, "yes"))
+      { /* Large width given as argument.  */
+        char *result;
+        int retval =
+          my_asprintf (&result, "%*d", INT_MAX, INT_MIN);
+        if (retval < 0)
+          ASSERT (errno == ENOMEM);
+        else
+          {
+            ASSERT (atoi (result) == INT_MIN);
+            ASSERT (retval == INT_MAX);
+            ASSERT (retval == strlen (result));
+            free (result);
+          }
+      }
+  }
+
   /* Test the support of the 'a' and 'A' conversion specifier for hexadecimal
      output of floating-point numbers.  */
 
diff --git a/tests/test-vaszprintf-posix.c b/tests/test-vaszprintf-posix.c
index a5b8be275c..319480371d 100644
--- a/tests/test-vaszprintf-posix.c
+++ b/tests/test-vaszprintf-posix.c
@@ -24,6 +24,8 @@
 
 #include <stdio.h>
 
+#include <errno.h>
+#include <limits.h>
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdlib.h>
@@ -213,6 +215,26 @@ test_function (ptrdiff_t (*my_aszprintf) (char **, const char *, ...))
     free (result);
   }
 
+  {
+    char *expensive = getenv ("RUN_EXPENSIVE_TESTS");
+    if (expensive && streq (expensive, "yes"))
+      { /* Large width given as argument.  */
+        char *result;
+        ptrdiff_t retval =
+          my_aszprintf (&result, "%.*d %d", INT_MAX, 1234567, 99);
+        if (retval < 0)
+          ASSERT (errno == ENOMEM);
+        else
+          {
+            for (int i = 0; i < INT_MAX - 7; i++)
+              ASSERT (result[i] == '0');
+            ASSERT (streq (result + INT_MAX - 7, "1234567 99"));
+            ASSERT (retval == strlen (result));
+            free (result);
+          }
+      }
+  }
+
   /* Test the support of the 'b' conversion specifier for binary output of
      integers.  */
 
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.