I'm still working on allowing *printf results of size > 2 GiB. One of the issues here is what to do when a width > INT_MAX is specified directly in the format string, for example "%03000000000d".
ISO C ยง 7.23.6.1 only says that this should be a "nonnegative decimal integer", but doesn't give a limit. I think it is useful to limit the width to the range 0..INT_MAX, because * That's what most platforms (all I tested, except AIX and mingw) do: if the width is > INT_MAX, the snprintf call fails (with EOVERFLOW on most platforms, and with ERANGE on MSVC.) See the attached test program. * It's consistent with passing the width as an 'int' argument: "%*d". It would be odd if an application could use the 'int' argument for some widths and had to use a dynamically created format string for larger widths. 2024-06-14 Bruno Haible <br...@clisp.org> vasnprintf: Reject a width > INT_MAX. * lib/vasnprintf.c (VASNPRINTF): If a width is > INT_MAX, fail with EOVERFLOW. * tests/test-vasnprintf.c: Include <errno.h>. (test_function): Test huge widths. * tests/test-vasnwprintf.c: Include <errno.h>. (test_function): Test huge widths. * tests/unistdio/test-u8-asnprintf1.h (test_function): Test huge widths. * tests/unistdio/test-u16-asnprintf1.h (test_function): Likewise. * tests/unistdio/test-u32-asnprintf1.h (test_function): Likewise. * tests/unistdio/test-ulc-asnprintf1.h (test_function): Likewise. * tests/unistdio/test-ulc-asnprintf1.c: Include <errno.h>. * tests/unistdio/test-ulc-vasnprintf1.c: Likewise. diff --git a/lib/vasnprintf.c b/lib/vasnprintf.c index 03a7da71f9..0242dd3f55 100644 --- a/lib/vasnprintf.c +++ b/lib/vasnprintf.c @@ -85,7 +85,7 @@ #include <string.h> /* memcpy(), strlen() */ #include <wchar.h> /* mbstate_t, mbrtowc(), mbrlen(), wcrtomb(), mbszero() */ #include <errno.h> /* errno */ -#include <limits.h> /* CHAR_BIT, INT_WIDTH, LONG_WIDTH */ +#include <limits.h> /* CHAR_BIT, INT_MAX, INT_WIDTH, LONG_WIDTH */ #include <float.h> /* DBL_MAX_EXP, LDBL_MAX_EXP, LDBL_MANT_DIG */ #if HAVE_NL_LANGINFO # include <langinfo.h> @@ -2458,6 +2458,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; has_width = 1; } @@ -2845,6 +2847,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; } { @@ -3000,6 +3004,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; has_width = 1; } @@ -3441,6 +3447,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; has_width = 1; } @@ -3629,6 +3637,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; } /* %c in vasnwprintf. See the specification of fwprintf. */ @@ -3717,6 +3727,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; has_width = 1; } @@ -4031,6 +4043,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; } has_precision = 0; @@ -4536,6 +4550,8 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; } has_precision = 0; @@ -5718,6 +5734,9 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, width = xsum (xtimes (width, 10), *digitp++ - '0'); while (digitp != dp->width_end); } + if (width > (size_t) INT_MAX) + goto overflow; +# define WIDTH_IS_CHECKED 1 # if (WIDE_CHAR_VERSION && MUSL_LIBC) || !DCHAR_IS_TCHAR || ENABLE_UNISTDIO || NEED_PRINTF_FLAG_LEFTADJUST || NEED_PRINTF_FLAG_ZERO || NEED_PRINTF_FLAG_ALT_PRECISION_ZERO || NEED_PRINTF_UNBOUNDED_PRECISION has_width = 1; # endif @@ -5867,6 +5886,43 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, if (dp->width_start != dp->width_end) { size_t n = dp->width_end - dp->width_start; +#if !WIDTH_IS_CHECKED + size_t width; + /* Reject an out-of-range width. + The underlying SNPRINTF already does this on some + platforms (glibc, musl, macOS, FreeBSD, NetBSD, + OpenBSD, Cygwin, Solaris, MSVC). However, on others + (AIX, mingw), it doesn't; thus this vasnprintf + invocation would succeed and produce a wrong result. + So, this is redundant on some platforms, but it's a + quick check anyway. */ + if (dp->width_arg_index != ARG_NONE) + { + int arg; + + if (!(a.arg[dp->width_arg_index].type == TYPE_INT)) + abort (); + arg = a.arg[dp->width_arg_index].a.a_int; + width = arg; + if (arg < 0) + { + /* "A negative field width is taken as a '-' flag + followed by a positive field width." */ + width = -width; + } + } + else + { + const FCHAR_T *digitp = dp->width_start; + + width = 0; + do + width = xsum (xtimes (width, 10), *digitp++ - '0'); + while (digitp != dp->width_end); + } + if (width > (size_t) INT_MAX) + goto overflow; +#endif /* The width specification is known to consist only of standard ASCII characters. */ if (sizeof (FCHAR_T) == sizeof (TCHAR_T)) @@ -6960,11 +7016,9 @@ VASNPRINTF (DCHAR_T *resultbuf, size_t *lengthp, not have this limitation. */ return result; -#if USE_SNPRINTF overflow: errno = EOVERFLOW; goto fail_with_errno; -#endif out_of_memory: errno = ENOMEM; diff --git a/tests/test-vasnprintf.c b/tests/test-vasnprintf.c index 655603f7e1..d6182dc461 100644 --- a/tests/test-vasnprintf.c +++ b/tests/test-vasnprintf.c @@ -20,6 +20,7 @@ #include "vasnprintf.h" +#include <errno.h> #include <stdarg.h> #include <stdlib.h> #include <string.h> @@ -86,6 +87,58 @@ test_function (char * (*my_asnprintf) (char *, size_t *, const char *, ...)) if (result != buf) free (result); } + + /* Verify that [v]asnprintf() rejects a width > 2 GiB, < 4 GiB. */ + { + size_t length; + char *s = my_asnprintf (NULL, &length, "x%03000000000dy\n", -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + char *s = my_asnprintf (NULL, &length, "x%03000000000cy\n", '@'); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + + /* Verify that [v]asnprintf() rejects a width > 4 GiB. */ + { + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%04294967306dy\n", /* 2^32 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%04294967306cy\n", /* 2^32 + 10 */ + '@'); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626dy\n", /* 2^64 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626cy\n", /* 2^64 + 10 */ + '@'); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } } static char * diff --git a/tests/test-vasnwprintf.c b/tests/test-vasnwprintf.c index 5d6034045e..a5832a9080 100644 --- a/tests/test-vasnwprintf.c +++ b/tests/test-vasnwprintf.c @@ -20,6 +20,7 @@ #include "vasnwprintf.h" +#include <errno.h> #include <stdarg.h> #include <stdlib.h> #include <string.h> @@ -87,6 +88,58 @@ test_function (wchar_t * (*my_asnwprintf) (wchar_t *, size_t *, const wchar_t *, if (result != buf) free (result); } + + /* Verify that [v]asnwprintf() rejects a width > 2 GiB, < 4 GiB. */ + { + size_t length; + wchar_t *s = my_asnwprintf (NULL, &length, L"x%03000000000dy\n", -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + wchar_t *s = my_asnwprintf (NULL, &length, L"x%03000000000cy\n", '@'); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + + /* Verify that [v]asnwprintf() rejects a width > 4 GiB. */ + { + size_t length; + wchar_t *s = + my_asnwprintf (NULL, &length, + L"x%04294967306dy\n", /* 2^32 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + wchar_t *s = + my_asnwprintf (NULL, &length, + L"x%04294967306cy\n", /* 2^32 + 10 */ + '@'); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + wchar_t *s = + my_asnwprintf (NULL, &length, + L"x%018446744073709551626dy\n", /* 2^64 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + wchar_t *s = + my_asnwprintf (NULL, &length, + L"x%018446744073709551626cy\n", /* 2^64 + 10 */ + '@'); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } } static wchar_t * diff --git a/tests/unistdio/test-u16-asnprintf1.h b/tests/unistdio/test-u16-asnprintf1.h index 4ec31a431c..2b1a9ab317 100644 --- a/tests/unistdio/test-u16-asnprintf1.h +++ b/tests/unistdio/test-u16-asnprintf1.h @@ -57,4 +57,59 @@ test_function (uint16_t * (*my_asnprintf) (uint16_t *, size_t *, const char *, . if (result != buf) free (result); } + + /* Verify that u16_[v]asnprintf() rejects a width > 2 GiB, < 4 GiB. */ + { + size_t length; + uint16_t *s = my_asnprintf (NULL, &length, "x%03000000000dy\n", -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint16_t arg[] = { '@', 0 }; + size_t length; + uint16_t *s = my_asnprintf (NULL, &length, "x%03000000000lUy\n", arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + + /* Verify that u16_[v]asnprintf() rejects a width > 4 GiB. */ + { + size_t length; + uint16_t *s = + my_asnprintf (NULL, &length, + "x%04294967306dy\n", /* 2^32 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint16_t arg[] = { '@', 0 }; + size_t length; + uint16_t *s = + my_asnprintf (NULL, &length, + "x%04294967306lUy\n", /* 2^32 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + uint16_t *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626dy\n", /* 2^64 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint16_t arg[] = { '@', 0 }; + size_t length; + uint16_t *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626lUy\n", /* 2^64 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } } diff --git a/tests/unistdio/test-u32-asnprintf1.h b/tests/unistdio/test-u32-asnprintf1.h index 85fa2cf078..af78684891 100644 --- a/tests/unistdio/test-u32-asnprintf1.h +++ b/tests/unistdio/test-u32-asnprintf1.h @@ -57,4 +57,59 @@ test_function (uint32_t * (*my_asnprintf) (uint32_t *, size_t *, const char *, . if (result != buf) free (result); } + + /* Verify that u32_[v]asnprintf() rejects a width > 2 GiB, < 4 GiB. */ + { + size_t length; + uint32_t *s = my_asnprintf (NULL, &length, "x%03000000000dy\n", -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint32_t arg[] = { '@', 0 }; + size_t length; + uint32_t *s = my_asnprintf (NULL, &length, "x%03000000000llUy\n", arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + + /* Verify that u32_[v]asnprintf() rejects a width > 4 GiB. */ + { + size_t length; + uint32_t *s = + my_asnprintf (NULL, &length, + "x%04294967306dy\n", /* 2^32 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint32_t arg[] = { '@', 0 }; + size_t length; + uint32_t *s = + my_asnprintf (NULL, &length, + "x%04294967306llUy\n", /* 2^32 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + uint32_t *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626dy\n", /* 2^64 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint32_t arg[] = { '@', 0 }; + size_t length; + uint32_t *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626llUy\n", /* 2^64 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } } diff --git a/tests/unistdio/test-u8-asnprintf1.h b/tests/unistdio/test-u8-asnprintf1.h index f48e2365cb..7c2c3622f4 100644 --- a/tests/unistdio/test-u8-asnprintf1.h +++ b/tests/unistdio/test-u8-asnprintf1.h @@ -54,4 +54,59 @@ test_function (uint8_t * (*my_asnprintf) (uint8_t *, size_t *, const char *, ... if (result != buf) free (result); } + + /* Verify that u8_[v]asnprintf() rejects a width > 2 GiB, < 4 GiB. */ + { + size_t length; + uint8_t *s = my_asnprintf (NULL, &length, "x%03000000000dy\n", -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint8_t arg[] = { '@', 0 }; + size_t length; + uint8_t *s = my_asnprintf (NULL, &length, "x%03000000000Uy\n", arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + + /* Verify that u8_[v]asnprintf() rejects a width > 4 GiB. */ + { + size_t length; + uint8_t *s = + my_asnprintf (NULL, &length, + "x%04294967306dy\n", /* 2^32 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint8_t arg[] = { '@', 0 }; + size_t length; + uint8_t *s = + my_asnprintf (NULL, &length, + "x%04294967306Uy\n", /* 2^32 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + uint8_t *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626dy\n", /* 2^64 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint8_t arg[] = { '@', 0 }; + size_t length; + uint8_t *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626Uy\n", /* 2^64 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } } diff --git a/tests/unistdio/test-ulc-asnprintf1.c b/tests/unistdio/test-ulc-asnprintf1.c index ced342e365..8fb91da026 100644 --- a/tests/unistdio/test-ulc-asnprintf1.c +++ b/tests/unistdio/test-ulc-asnprintf1.c @@ -20,6 +20,7 @@ #include "unistdio.h" +#include <errno.h> #include <stdarg.h> #include <stddef.h> #include <stdint.h> diff --git a/tests/unistdio/test-ulc-asnprintf1.h b/tests/unistdio/test-ulc-asnprintf1.h index 9e11f31465..8ede282f55 100644 --- a/tests/unistdio/test-ulc-asnprintf1.h +++ b/tests/unistdio/test-ulc-asnprintf1.h @@ -51,4 +51,59 @@ test_function (char * (*my_asnprintf) (char *, size_t *, const char *, ...)) if (result != buf) free (result); } + + /* Verify that ulc_[v]asnprintf() rejects a width > 2 GiB, < 4 GiB. */ + { + size_t length; + char *s = my_asnprintf (NULL, &length, "x%03000000000dy\n", -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint8_t arg[] = { '@', 0 }; + size_t length; + char *s = my_asnprintf (NULL, &length, "x%03000000000Uy\n", arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + + /* Verify that ulc_[v]asnprintf() rejects a width > 4 GiB. */ + { + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%04294967306dy\n", /* 2^32 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint8_t arg[] = { '@', 0 }; + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%04294967306Uy\n", /* 2^32 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626dy\n", /* 2^64 + 10 */ + -17); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } + { + static const uint8_t arg[] = { '@', 0 }; + size_t length; + char *s = + my_asnprintf (NULL, &length, + "x%018446744073709551626Uy\n", /* 2^64 + 10 */ + arg); + ASSERT (s == NULL); + ASSERT (errno == EOVERFLOW); + } } diff --git a/tests/unistdio/test-ulc-vasnprintf1.c b/tests/unistdio/test-ulc-vasnprintf1.c index 83557dbb4c..4352ff038b 100644 --- a/tests/unistdio/test-ulc-vasnprintf1.c +++ b/tests/unistdio/test-ulc-vasnprintf1.c @@ -20,6 +20,7 @@ #include "unistdio.h" +#include <errno.h> #include <stdarg.h> #include <stddef.h> #include <stdint.h>
#include <errno.h> #include <stdio.h> #include <stdlib.h> #if !defined _WIN32 #include <sys/mman.h> #endif int main () { int ret; fprintf (stderr, "Doing output to stdout...\n"); fflush (stderr); ret = printf ("x%03000000000dy\n", -17); if (ret >= 0) fprintf (stderr, " ... ret = %d\n", ret); else fprintf (stderr, " ... ret = %d, errno=%s%s%s(%d)\n", ret, errno==ENOMEM?"ENOMEM":"", errno==EOVERFLOW?"EOVERFLOW":"", errno==ERANGE?"ERANGE":"", errno); fprintf (stderr, "Doing output to string...\n"); fflush (stderr); { char *buf = malloc (3000000100U); #if !defined _WIN32 if (buf == NULL) buf = mmap (NULL, 3000000512U, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); #endif if (buf == NULL) fprintf (stderr, " ... malloc failed\n"); else { int ret = snprintf (buf, 3000000100U, "x%03000000000dy\n", -17); if (ret >= 0) fprintf (stderr, " ... ret = %d\n", ret); else fprintf (stderr, " ... ret = %d, errno=%s%s%s(%d)\n", ret, errno==ENOMEM?"ENOMEM":"", errno==EOVERFLOW?"EOVERFLOW":"", errno==ERANGE?"ERANGE":"", errno); } } return 0; } /* On 64-bit platforms, use: ./a.out >/dev/null glibc, musl, macOS, OpenBSD (after allowing 10 GiB, through "ulimit -d 10485760"): Doing output to stdout... ... ret = -1, errno=EOVERFLOW Doing output to string... ... ret = -1, errno=EOVERFLOW FreeBSD, NetBSD, Cygwin: Doing output to stdout... ... ret = 6 Doing output to string... ... ret = -1, errno=EOVERFLOW AIX: Doing output to stdout... ... ret = 6 Doing output to string... ... ret = 0 Solaris 10,11: Doing output to stdout... (takes a while) ... ret = -1, errno=EOVERFLOW Doing output to string... ... ret = -1, errno=EOVERFLOW mingw: Doing output to stdout... ... ret = 6 Doing output to string... ... ret = 6 MSVC: Doing output to stdout... ... ret = -1, errno=ERANGE Doing output to string... ... ret = -1, errno=ERANGE */