From: Paul Eggert Date: Tue, 7 Jul 2026 03:35:24 +0000 (-0700) Subject: vasnprintf: fix ("%*d", INT_MAX, n) X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=08c8a9228ffce55e2e18e9ef74f3e98cc869511e;p=thirdparty%2Fgnulib.git 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 and 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. --- diff --git a/ChangeLog b/ChangeLog index 6ba52936a5..fcddd3f1d6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,45 @@ +2026-07-06 Paul Eggert + + 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 and 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 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 @@ 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. 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 @@ 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. 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 @@ -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 @@ 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. 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 #include +#include #include #include #include @@ -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 +#include #include +#include #include #include #include @@ -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 +#include +#include #include #include #include @@ -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. */