Skip to content

Commit ab8cecf

Browse files
[3.15] gh-158446: Reject float format precision near INT_MAX (GH-158474) (#158476)
gh-158446: Reject float format precision near INT_MAX (GH-158474) Formatting a float or complex with a precision within about 1000 of INT_MAX could crash or produce incorrect output. PyOS_double_to_string() now raises ValueError("precision too big") for such precisions, as the format string parsers already do for precisions above INT_MAX. The limit applies regardless of presentation type or value, so a few calls that previously succeeded (inf, nan, or 'g' with such a precision) now raise as well. (cherry picked from commit b7b4f3e) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent 3f283c7 commit ab8cecf

4 files changed

Lines changed: 65 additions & 1 deletion

File tree

‎Lib/test/test_format.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,28 @@ def test_precision_c_limits(self):
639639
with self.assertRaises(ValueError) as cm:
640640
format(c, ".%sf" % (INT_MAX + 1))
641641

642+
@support.cpython_only
643+
def test_precision_near_int_max(self):
644+
# gh-158446: Precisions just below INT_MAX are rejected before any
645+
# output buffer size is computed from them.
646+
_testcapi = import_module("_testcapi")
647+
INT_MAX = _testcapi.INT_MAX
648+
649+
f = 1e300
650+
c = complex(f)
651+
for prec in (INT_MAX, INT_MAX - 1023):
652+
for code in "feg":
653+
spec = ".%d%s" % (prec, code)
654+
with self.subTest(spec=spec):
655+
with self.assertRaises(ValueError):
656+
format(f, spec)
657+
with self.assertRaises(ValueError):
658+
format(c, spec)
659+
with self.assertRaises(ValueError):
660+
("%" + spec) % f
661+
with self.assertRaises(ValueError):
662+
("%" + spec).encode() % f
663+
642664
def test_g_format_has_no_trailing_zeros(self):
643665
# regression test for bugs.python.org/issue40780
644666
self.assertEqual("%.3g" % 1505.0, "1.5e+03")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a crash or incorrect output that could occur when formatting a
2+
:class:`float` or :class:`complex` with a precision close to the platform's
3+
``INT_MAX``. :c:func:`PyOS_double_to_string` now raises :exc:`ValueError` for
4+
any precision of that magnitude, regardless of presentation type or value, as
5+
the format string parsers already did for precisions above ``INT_MAX``.

‎Python/dtoa.c‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
* 8. A corner case where _Py_dg_dtoa didn't strip trailing zeros has been
6868
* fixed. (bugs.python.org/issue40780)
6969
*
70+
* 9. _Py_dg_dtoa clamps ndigits in modes 3 and 5 so that its buffer size
71+
* arithmetic cannot exceed the int range, and rv_alloc's size doubling
72+
* uses size_t. (gh-158446)
73+
*
7074
***************************************************************/
7175

7276
/* Please send bug reports for the original dtoa.c code to David M. Gay (dmg
@@ -2108,7 +2112,8 @@ _Py_dg_strtod(const char *s00, char **se)
21082112
static char *
21092113
rv_alloc(int i)
21102114
{
2111-
int j, k, *r;
2115+
int k, *r;
2116+
size_t j; /* size_t so that j <<= 1 cannot overflow for i near INT_MAX */
21122117

21132118
j = sizeof(ULong);
21142119
for(k = 0;
@@ -2372,6 +2377,15 @@ _Py_dg_dtoa(double dd, int mode, int ndigits,
23722377
leftright = 0;
23732378
_Py_FALLTHROUGH;
23742379
case 5:
2380+
/* -330 < k < 330 for any finite nonzero double. Clamp ndigits so
2381+
that ndigits + k + 1 stays within int range; no double has
2382+
anywhere near this many decimal digits so the digits returned
2383+
are unaffected (*decpt saturates in the no_digits case). Same
2384+
bound as DOUBLE_TO_STRING_PRECISION_MAX in pystrtod.c. */
2385+
if (ndigits > INT_MAX - 1024)
2386+
ndigits = INT_MAX - 1024;
2387+
else if (ndigits < -(INT_MAX - 1024))
2388+
ndigits = -(INT_MAX - 1024);
23752389
i = ndigits + k + 1;
23762390
ilim = i;
23772391
ilim1 = i - 1;

‎Python/pystrtod.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,15 @@ _Py_string_to_number_with_underscores(
401401
return NULL;
402402
}
403403

404+
/* Largest precision magnitude accepted by PyOS_double_to_string(). The
405+
output buffer sizes computed below and within _Py_dg_dtoa() use int and
406+
Py_ssize_t arithmetic on roughly precision + (digits before the point, at
407+
most DBL_MAX_10_EXP + 1 == 309) + a few bytes of sign, point and exponent.
408+
Staying this far inside the int range keeps all of those sums in range.
409+
(Only C callers can pass a negative precision.) _Py_dg_dtoa() applies
410+
the same bound to its ndigits argument. */
411+
#define DOUBLE_TO_STRING_PRECISION_MAX (INT_MAX - 1024)
412+
404413
#if _PY_SHORT_FLOAT_REPR == 0
405414

406415
/* Given a string that may have a decimal point in the current
@@ -766,6 +775,13 @@ char * PyOS_double_to_string(double val,
766775
int t, exp;
767776
int upper = 0;
768777

778+
if (precision > DOUBLE_TO_STRING_PRECISION_MAX
779+
|| precision < -DOUBLE_TO_STRING_PRECISION_MAX)
780+
{
781+
PyErr_SetString(PyExc_ValueError, "precision too big");
782+
return NULL;
783+
}
784+
769785
/* Validate format_code, and map upper and lower case */
770786
switch (format_code) {
771787
case 'e': /* exponent */
@@ -1227,6 +1243,13 @@ char * PyOS_double_to_string(double val,
12271243
const char * const *float_strings = lc_float_strings;
12281244
int mode;
12291245

1246+
if (precision > DOUBLE_TO_STRING_PRECISION_MAX
1247+
|| precision < -DOUBLE_TO_STRING_PRECISION_MAX)
1248+
{
1249+
PyErr_SetString(PyExc_ValueError, "precision too big");
1250+
return NULL;
1251+
}
1252+
12301253
/* Validate format_code, and map upper and lower case. Compute the
12311254
mode and make any adjustments as needed. */
12321255
switch (format_code) {

0 commit comments

Comments
 (0)