From 2f1e70c127a099ac6a9a4aeabb7a45a14b0f217c Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Tue, 29 Sep 2026 18:17:38 +0000 Subject: [PATCH 1/3] gh-158446: Reject float format precision near INT_MAX PyOS_double_to_string() now raises ValueError("precision too big") for precisions above INT_MAX - 1024, matching what the format parsers already do above INT_MAX. Buffer size computations in pystrtod.c and dtoa.c add a few hundred to the precision using int / Py_ssize_t arithmetic and could wrap for such values, leading to a crash or incorrect output. Also harden _Py_dg_dtoa() and rv_alloc() so their size arithmetic stays in range for any C caller. --- Lib/test/test_format.py | 22 +++++++++++++++++++ ...-09-29-16-32-46.gh-issue-158446.dToaPr.rst | 4 ++++ Python/dtoa.c | 11 +++++++++- Python/pystrtod.c | 17 ++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 5d322cb444cfb6..c3b96122e8cfea 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -639,6 +639,28 @@ def test_precision_c_limits(self): with self.assertRaises(ValueError) as cm: format(c, ".%sf" % (INT_MAX + 1)) + @support.cpython_only + def test_precision_near_int_max(self): + # gh-158446: Precisions just below INT_MAX are rejected before any output + # buffer size is computed from them. + _testcapi = import_module("_testcapi") + INT_MAX = _testcapi.INT_MAX + + f = 1e300 + c = complex(f) + for prec in (INT_MAX, INT_MAX - 1023): + for code in "feg": + spec = ".%d%s" % (prec, code) + with self.subTest(spec=spec): + with self.assertRaises(ValueError): + format(f, spec) + with self.assertRaises(ValueError): + format(c, spec) + with self.assertRaises(ValueError): + ("%" + spec) % f + with self.assertRaises(ValueError): + ("%" + spec).encode() % f + def test_g_format_has_no_trailing_zeros(self): # regression test for bugs.python.org/issue40780 self.assertEqual("%.3g" % 1505.0, "1.5e+03") diff --git a/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst b/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst new file mode 100644 index 00000000000000..e7e480a3a6678e --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst @@ -0,0 +1,4 @@ +Fix a crash or incorrect output when formatting a :class:`float` using the +``'f'``, ``'e'`` or ``'g'`` presentation types with a precision close to the +platform's ``INT_MAX``. Such precisions now raise :exc:`ValueError`, as +precisions above ``INT_MAX`` already did. diff --git a/Python/dtoa.c b/Python/dtoa.c index 89fadd33391cb4..75313d48a49885 100644 --- a/Python/dtoa.c +++ b/Python/dtoa.c @@ -2108,7 +2108,8 @@ _Py_dg_strtod(const char *s00, char **se) static char * rv_alloc(int i) { - int j, k, *r; + int k, *r; + size_t j; /* size_t so that j <<= 1 cannot overflow for i near INT_MAX */ j = sizeof(ULong); for(k = 0; @@ -2372,6 +2373,14 @@ _Py_dg_dtoa(double dd, int mode, int ndigits, leftright = 0; _Py_FALLTHROUGH; case 5: + /* -330 < k < 330 for any finite nonzero double. Clamp ndigits so + that ndigits + k + 1 stays within int range; no double has + anywhere near this many decimal digits so the digits returned + are unaffected. */ + if (ndigits > INT_MAX - 1024) + ndigits = INT_MAX - 1024; + else if (ndigits < -(INT_MAX - 1024)) + ndigits = -(INT_MAX - 1024); i = ndigits + k + 1; ilim = i; ilim1 = i - 1; diff --git a/Python/pystrtod.c b/Python/pystrtod.c index e8aca939d1fb98..f4a1cb1a4c5146 100644 --- a/Python/pystrtod.c +++ b/Python/pystrtod.c @@ -401,6 +401,13 @@ _Py_string_to_number_with_underscores( return NULL; } +/* Largest precision accepted by PyOS_double_to_string(). The output buffer + sizes computed below and within _Py_dg_dtoa() use int and Py_ssize_t + arithmetic on roughly precision + (digits before the point, at most + DBL_MAX_10_EXP + 1 == 309) + a few bytes of sign, point and exponent. + Staying this far below INT_MAX keeps all of those sums in range. */ +#define DOUBLE_TO_STRING_PRECISION_MAX (INT_MAX - 1024) + #if _PY_SHORT_FLOAT_REPR == 0 /* Given a string that may have a decimal point in the current @@ -766,6 +773,11 @@ char * PyOS_double_to_string(double val, int t, exp; int upper = 0; + if (precision > DOUBLE_TO_STRING_PRECISION_MAX) { + PyErr_SetString(PyExc_ValueError, "precision too big"); + return NULL; + } + /* Validate format_code, and map upper and lower case */ switch (format_code) { case 'e': /* exponent */ @@ -1227,6 +1239,11 @@ char * PyOS_double_to_string(double val, const char * const *float_strings = lc_float_strings; int mode; + if (precision > DOUBLE_TO_STRING_PRECISION_MAX) { + PyErr_SetString(PyExc_ValueError, "precision too big"); + return NULL; + } + /* Validate format_code, and map upper and lower case. Compute the mode and make any adjustments as needed. */ switch (format_code) { From c6a8db9b65ecdc75194f7f90e62f8a1318f13a7a Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Tue, 29 Sep 2026 22:28:28 +0000 Subject: [PATCH 2/3] gh-158446: Also bound large negative precisions; broaden NEWS wording Make the PyOS_double_to_string() precision check symmetric so that C callers passing a huge negative precision cannot reach the remaining precision arithmetic either. Reword the NEWS entry: the limit applies to float and complex formatting regardless of presentation type or value. --- ...26-09-29-16-32-46.gh-issue-158446.dToaPr.rst | 9 +++++---- Python/pystrtod.c | 17 ++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst b/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst index e7e480a3a6678e..f17a3f68b93e85 100644 --- a/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst +++ b/Misc/NEWS.d/next/Security/2026-09-29-16-32-46.gh-issue-158446.dToaPr.rst @@ -1,4 +1,5 @@ -Fix a crash or incorrect output when formatting a :class:`float` using the -``'f'``, ``'e'`` or ``'g'`` presentation types with a precision close to the -platform's ``INT_MAX``. Such precisions now raise :exc:`ValueError`, as -precisions above ``INT_MAX`` already did. +Fix a crash or incorrect output that could occur when formatting a +:class:`float` or :class:`complex` with a precision close to the platform's +``INT_MAX``. :c:func:`PyOS_double_to_string` now raises :exc:`ValueError` for +any precision of that magnitude, regardless of presentation type or value, as +the format string parsers already did for precisions above ``INT_MAX``. diff --git a/Python/pystrtod.c b/Python/pystrtod.c index f4a1cb1a4c5146..1234002b74f335 100644 --- a/Python/pystrtod.c +++ b/Python/pystrtod.c @@ -401,11 +401,12 @@ _Py_string_to_number_with_underscores( return NULL; } -/* Largest precision accepted by PyOS_double_to_string(). The output buffer - sizes computed below and within _Py_dg_dtoa() use int and Py_ssize_t - arithmetic on roughly precision + (digits before the point, at most - DBL_MAX_10_EXP + 1 == 309) + a few bytes of sign, point and exponent. - Staying this far below INT_MAX keeps all of those sums in range. */ +/* Largest precision magnitude accepted by PyOS_double_to_string(). The + output buffer sizes computed below and within _Py_dg_dtoa() use int and + Py_ssize_t arithmetic on roughly precision + (digits before the point, at + most DBL_MAX_10_EXP + 1 == 309) + a few bytes of sign, point and exponent. + Staying this far inside the int range keeps all of those sums in range. + (Only C callers can pass a negative precision.) */ #define DOUBLE_TO_STRING_PRECISION_MAX (INT_MAX - 1024) #if _PY_SHORT_FLOAT_REPR == 0 @@ -773,7 +774,8 @@ char * PyOS_double_to_string(double val, int t, exp; int upper = 0; - if (precision > DOUBLE_TO_STRING_PRECISION_MAX) { + if (precision > DOUBLE_TO_STRING_PRECISION_MAX + || precision < -DOUBLE_TO_STRING_PRECISION_MAX) { PyErr_SetString(PyExc_ValueError, "precision too big"); return NULL; } @@ -1239,7 +1241,8 @@ char * PyOS_double_to_string(double val, const char * const *float_strings = lc_float_strings; int mode; - if (precision > DOUBLE_TO_STRING_PRECISION_MAX) { + if (precision > DOUBLE_TO_STRING_PRECISION_MAX + || precision < -DOUBLE_TO_STRING_PRECISION_MAX) { PyErr_SetString(PyExc_ValueError, "precision too big"); return NULL; } From f86460bfd466f7d4c82c08ddb869ef0fb80e79ca Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Tue, 29 Sep 2026 22:39:22 +0000 Subject: [PATCH 3/3] gh-158446: Address review nits Note the dtoa.c deviations in its header list, cross-reference the shared bound between dtoa.c and pystrtod.c, PEP 7 brace placement, and wrap a long test comment. No functional change. --- Lib/test/test_format.py | 4 ++-- Python/dtoa.c | 7 ++++++- Python/pystrtod.c | 9 ++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index c3b96122e8cfea..4f253e01c0b174 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -641,8 +641,8 @@ def test_precision_c_limits(self): @support.cpython_only def test_precision_near_int_max(self): - # gh-158446: Precisions just below INT_MAX are rejected before any output - # buffer size is computed from them. + # gh-158446: Precisions just below INT_MAX are rejected before any + # output buffer size is computed from them. _testcapi = import_module("_testcapi") INT_MAX = _testcapi.INT_MAX diff --git a/Python/dtoa.c b/Python/dtoa.c index 75313d48a49885..f412278765c8e3 100644 --- a/Python/dtoa.c +++ b/Python/dtoa.c @@ -67,6 +67,10 @@ * 8. A corner case where _Py_dg_dtoa didn't strip trailing zeros has been * fixed. (bugs.python.org/issue40780) * + * 9. _Py_dg_dtoa clamps ndigits in modes 3 and 5 so that its buffer size + * arithmetic cannot exceed the int range, and rv_alloc's size doubling + * uses size_t. (gh-158446) + * ***************************************************************/ /* Please send bug reports for the original dtoa.c code to David M. Gay (dmg @@ -2376,7 +2380,8 @@ _Py_dg_dtoa(double dd, int mode, int ndigits, /* -330 < k < 330 for any finite nonzero double. Clamp ndigits so that ndigits + k + 1 stays within int range; no double has anywhere near this many decimal digits so the digits returned - are unaffected. */ + are unaffected (*decpt saturates in the no_digits case). Same + bound as DOUBLE_TO_STRING_PRECISION_MAX in pystrtod.c. */ if (ndigits > INT_MAX - 1024) ndigits = INT_MAX - 1024; else if (ndigits < -(INT_MAX - 1024)) diff --git a/Python/pystrtod.c b/Python/pystrtod.c index 1234002b74f335..7753d1732cf78c 100644 --- a/Python/pystrtod.c +++ b/Python/pystrtod.c @@ -406,7 +406,8 @@ _Py_string_to_number_with_underscores( Py_ssize_t arithmetic on roughly precision + (digits before the point, at most DBL_MAX_10_EXP + 1 == 309) + a few bytes of sign, point and exponent. Staying this far inside the int range keeps all of those sums in range. - (Only C callers can pass a negative precision.) */ + (Only C callers can pass a negative precision.) _Py_dg_dtoa() applies + the same bound to its ndigits argument. */ #define DOUBLE_TO_STRING_PRECISION_MAX (INT_MAX - 1024) #if _PY_SHORT_FLOAT_REPR == 0 @@ -775,7 +776,8 @@ char * PyOS_double_to_string(double val, int upper = 0; if (precision > DOUBLE_TO_STRING_PRECISION_MAX - || precision < -DOUBLE_TO_STRING_PRECISION_MAX) { + || precision < -DOUBLE_TO_STRING_PRECISION_MAX) + { PyErr_SetString(PyExc_ValueError, "precision too big"); return NULL; } @@ -1242,7 +1244,8 @@ char * PyOS_double_to_string(double val, int mode; if (precision > DOUBLE_TO_STRING_PRECISION_MAX - || precision < -DOUBLE_TO_STRING_PRECISION_MAX) { + || precision < -DOUBLE_TO_STRING_PRECISION_MAX) + { PyErr_SetString(PyExc_ValueError, "precision too big"); return NULL; }