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-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.
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.
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.
#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
#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) \
{ \
}
#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
{
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
# 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,
the 'lc' directive.])
;;
esac
- gl_MUSL_LIBC
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.
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
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/stdint_h.m4
m4/inttypes_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
unistdio/base
multiarch
assert-h
mbszero
+minmax
streq
configure.ac:
m4/printf.m4
m4/math_h.m4
m4/exponentd.m4
+m4/musl.m4
Depends-on:
stdio-h
wchar-h
mbszero
mbsnlen
+minmax
streq
configure.ac:
mbrtowc
wmemcpy
wmemset
+minmax
streq
configure.ac:
#include <errno.h>
#include <float.h>
+#include <limits.h>
#include <stdarg.h>
#include <stdcountof.h>
#include <stddef.h>
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. */
#include <stdio.h>
+#include <errno.h>
#include <float.h>
+#include <limits.h>
#include <stdarg.h>
#include <stdcountof.h>
#include <stddef.h>
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. */
#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
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. */