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