Skip to content

Commit 25797df

Browse files
gpsheadmiss-islington
authored andcommitted
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 28f3154 commit 25797df

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
@@ -488,6 +488,28 @@ def test_precision_c_limits(self):
488488
with self.assertRaises(ValueError) as cm:
489489
format(c, ".%sf" % (INT_MAX + 1))
490490

491+
@support.cpython_only
492+
def test_precision_near_int_max(self):
493+
# gh-158446: Precisions just below INT_MAX are rejected before any
494+
# output buffer size is computed from them.
495+
_testcapi = import_module("_testcapi")
496+
INT_MAX = _testcapi.INT_MAX
497+
498+
f = 1e300
499+
c = complex(f)
500+
for prec in (INT_MAX, INT_MAX - 1023):
501+
for code in "feg":
502+
spec = ".%d%s" % (prec, code)
503+
with self.subTest(spec=spec):
504+
with self.assertRaises(ValueError):
505+
format(f, spec)
506+
with self.assertRaises(ValueError):
507+
format(c, spec)
508+
with self.assertRaises(ValueError):
509+
("%" + spec) % f
510+
with self.assertRaises(ValueError):
511+
("%" + spec).encode() % f
512+
491513
def test_g_format_has_no_trailing_zeros(self):
492514
# regression test for bugs.python.org/issue40780
493515
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
@@ -2163,7 +2167,8 @@ _Py_dg_strtod(const char *s00, char **se)
21632167
static char *
21642168
rv_alloc(int i)
21652169
{
2166-
int j, k, *r;
2170+
int k, *r;
2171+
size_t j; /* size_t so that j <<= 1 cannot overflow for i near INT_MAX */
21672172

21682173
j = sizeof(ULong);
21692174
for(k = 0;
@@ -2427,6 +2432,15 @@ _Py_dg_dtoa(double dd, int mode, int ndigits,
24272432
leftright = 0;
24282433
/* fall through */
24292434
case 5:
2435+
/* -330 < k < 330 for any finite nonzero double. Clamp ndigits so
2436+
that ndigits + k + 1 stays within int range; no double has
2437+
anywhere near this many decimal digits so the digits returned
2438+
are unaffected (*decpt saturates in the no_digits case). Same
2439+
bound as DOUBLE_TO_STRING_PRECISION_MAX in pystrtod.c. */
2440+
if (ndigits > INT_MAX - 1024)
2441+
ndigits = INT_MAX - 1024;
2442+
else if (ndigits < -(INT_MAX - 1024))
2443+
ndigits = -(INT_MAX - 1024);
24302444
i = ndigits + k + 1;
24312445
ilim = i;
24322446
ilim1 = i - 1;

‎Python/pystrtod.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,15 @@ _Py_string_to_number_with_underscores(
439439
return NULL;
440440
}
441441

442+
/* Largest precision magnitude accepted by PyOS_double_to_string(). The
443+
output buffer sizes computed below and within _Py_dg_dtoa() use int and
444+
Py_ssize_t arithmetic on roughly precision + (digits before the point, at
445+
most DBL_MAX_10_EXP + 1 == 309) + a few bytes of sign, point and exponent.
446+
Staying this far inside the int range keeps all of those sums in range.
447+
(Only C callers can pass a negative precision.) _Py_dg_dtoa() applies
448+
the same bound to its ndigits argument. */
449+
#define DOUBLE_TO_STRING_PRECISION_MAX (INT_MAX - 1024)
450+
442451
#if _PY_SHORT_FLOAT_REPR == 0
443452

444453
/* Given a string that may have a decimal point in the current
@@ -804,6 +813,13 @@ char * PyOS_double_to_string(double val,
804813
int t, exp;
805814
int upper = 0;
806815

816+
if (precision > DOUBLE_TO_STRING_PRECISION_MAX
817+
|| precision < -DOUBLE_TO_STRING_PRECISION_MAX)
818+
{
819+
PyErr_SetString(PyExc_ValueError, "precision too big");
820+
return NULL;
821+
}
822+
807823
/* Validate format_code, and map upper and lower case */
808824
switch (format_code) {
809825
case 'e': /* exponent */
@@ -1265,6 +1281,13 @@ char * PyOS_double_to_string(double val,
12651281
const char * const *float_strings = lc_float_strings;
12661282
int mode;
12671283

1284+
if (precision > DOUBLE_TO_STRING_PRECISION_MAX
1285+
|| precision < -DOUBLE_TO_STRING_PRECISION_MAX)
1286+
{
1287+
PyErr_SetString(PyExc_ValueError, "precision too big");
1288+
return NULL;
1289+
}
1290+
12681291
/* Validate format_code, and map upper and lower case. Compute the
12691292
mode and make any adjustments as needed. */
12701293
switch (format_code) {

0 commit comments

Comments
 (0)