]> git.ipfire.org Git - thirdparty/gnulib.git/commitdiff
vasnprintf: fix ("%*d", INT_MAX, n)
authorPaul Eggert <eggert@cs.ucla.edu>
Tue, 7 Jul 2026 03:35:24 +0000 (20:35 -0700)
committerPaul Eggert <eggert@cs.ucla.edu>
Tue, 7 Jul 2026 03:41:43 +0000 (20:41 -0700)
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.

18 files changed:
ChangeLog
doc/posix-functions/snprintf.texi
doc/posix-functions/vsnprintf.texi
lib/vasnprintf.c
m4/vasnprintf.m4
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
tests/test-vasnprintf-posix.c
tests/test-vasprintf-posix.c
tests/test-vaszprintf-posix.c

index 6ba52936a5ba813788ecda54c537304fb09d3b8a..fcddd3f1d6646170cb4dfcb956d9c891037b95bd 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,45 @@
+2026-07-06  Paul Eggert  <eggert@cs.ucla.edu>
+
+       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  <simon@josefsson.org>
 
        doc: Mention gnulib-202607 git bundle.
index a84ffd40ed55fb23ec7eff385915fb79c31a3bd7..2a75e2fd32a3c8ed26a1db70925bbf056fb4f60a 100644 (file)
@@ -138,6 +138,14 @@ macOS 14, OpenBSD, MSVC 14, Android.
 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.
index 21acf8776c72e011767c30cce66b26ff2a89c537..52d7dacda1945e9882250e659a0690be4bb0ce19 100644 (file)
@@ -136,6 +136,14 @@ macOS 14, OpenBSD, HP-UX 11, mingw, MSVC 14, Android.
 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.
index 366a43786304dc8a595953b0f1a10cb75fd73c4e..98d8e4a757bfde3978be856cd192231056e773aa 100644 (file)
 #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>
 #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
index 4e01fbc9e693dd32cbf4457c6f9a04e5d4916862..ff067b2a0f6c453744beb160e9609d805e39f4f0 100644 (file)
@@ -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 @@ int main()
          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.
index 37cfd14a5c3dc076f3c3c3be6e5bc6608a582a0b..1fe79632490556dca1aa3b86851b525712739443 100644 (file)
@@ -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
index 58707f65f51cddc51518ffa59f3327644ce48136..0645ee37a5edc39f7a0ab3d032741804a67a278b 100644 (file)
@@ -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:
index b040f5ab44e64308d510b5706c5d33d4387d80fa..53db0ef47cbaf1f3a1140d02ce112d079252cea1 100644 (file)
@@ -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:
index 48ef0bd0741450c8341f7b8d565cd0935542bc5d..0ff115b01c268e9cc1c77e6df43da43fdde32101 100644 (file)
@@ -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:
index 9ff85ec1ce96e5b10d8164761bf557f539d46bce..1928b3fa2d33edddedea9b828d359e2c161c34ef 100644 (file)
@@ -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:
index 3907151c266b233a521859caa5b2113c2160a555..f58da96343ef2145d86e16f0b5c685a1679caffa 100644 (file)
@@ -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:
index 1da2109becae2f373e67f554f7f0ad5fc89ca935..f68a450300d5b35db019ae13b8b48c3cb78b050e 100644 (file)
@@ -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:
index 02aa89a9363c700d59d981fa314ca7017c5c8ca8..e6b44915b40aba1fd6fdace7df80b4b03a4d13c7 100644 (file)
@@ -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:
index 798b9db377eed9223aeeaf0c12b04a8ec39fdb5f..80468bad92dd98b061dad9161e984be7d7f0190f 100644 (file)
@@ -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:
index 128a3c602b79e9d41911a4db55f3376d50fec9bd..d61016b3741ca56992ea9feac67547164d7c8ee8 100644 (file)
@@ -41,6 +41,7 @@ mbszero
 mbrtowc
 wmemcpy
 wmemset
+minmax
 streq
 
 configure.ac:
index fe0a59d647af95579affe92ed5cd6990bda6165b..98e3319920ea12b400d7a60b9d6de23c56bbf0bc 100644 (file)
@@ -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.  */
 
index 6d62468ccee8155e64ddb5f3b0f5f393956ae8ae..86ae5c12a74e14c5bd8b30643ded49b9d2ae18fc 100644 (file)
@@ -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.  */
 
index a5b8be275cbdf868492c753c6266a5c32362d086..319480371d12ff935579c95a5818c37fa96b6186 100644 (file)
@@ -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.  */