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


Reply via email to