Skip to content

gh-120950: Fix overflow in math.log() with large int-like argument #121011

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,22 @@ def __init__(self, value):
def __index__(self):
return self.value

class IndexableFloatLike:
def __init__(self, float_value, index_value):
self.float_value = float_value
self.index_value = index_value

def __float__(self):
if isinstance(self.float_value, BaseException):
raise self.float_value
return self.float_value

def __index__(self):
if isinstance(self.index_value, BaseException):
raise self.index_value
return self.index_value


class BadDescr:
def __get__(self, obj, objtype=None):
raise ValueError
Expand Down Expand Up @@ -1225,13 +1241,32 @@ def testLog(self):
self.ftest('log(10**40, 10**20)', math.log(10**40, 10**20), 2)
self.ftest('log(10**1000)', math.log(10**1000),
2302.5850929940457)
self.ftest('log(10**2000, 10**1000)', math.log(10**2000, 10**1000), 2)
self.ftest('log(MyIndexable(32), MyIndexable(2))',
math.log(MyIndexable(32), MyIndexable(2)), 5)
self.ftest('log(MyIndexable(10**1000))',
math.log(MyIndexable(10**1000)),
2302.5850929940457)
self.ftest('log(MyIndexable(10**2000), MyIndexable(10**1000))',
math.log(MyIndexable(10**2000), MyIndexable(10**1000)),
2)
self.assertRaises(ValueError, math.log, 0.0)
self.assertRaises(ValueError, math.log, 0)
self.assertRaises(ValueError, math.log, MyIndexable(0))
self.assertRaises(ValueError, math.log, -1.5)
self.assertRaises(ValueError, math.log, -1)
self.assertRaises(ValueError, math.log, MyIndexable(-1))
self.assertRaises(ValueError, math.log, -10**1000)
self.assertRaises(ValueError, math.log, MyIndexable(-10**1000))
self.assertRaises(ValueError, math.log, 10, -10)
self.assertRaises(ValueError, math.log, NINF)
self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log(NAN)))

self.assertEqual(math.log(IndexableFloatLike(math.e, 10**1000)), 1.0)
self.assertAlmostEqual(math.log(IndexableFloatLike(OverflowError(), 10**1000)),
2302.5850929940457)

def testLog1p(self):
self.assertRaises(TypeError, math.log1p)
for n in [2, 2**90, 2**300]:
Expand All @@ -1247,16 +1282,28 @@ def testLog2(self):
self.assertEqual(math.log2(1), 0.0)
self.assertEqual(math.log2(2), 1.0)
self.assertEqual(math.log2(4), 2.0)
self.assertEqual(math.log2(MyIndexable(4)), 2.0)

# Large integer values
self.assertEqual(math.log2(2**1023), 1023.0)
self.assertEqual(math.log2(2**1024), 1024.0)
self.assertEqual(math.log2(2**2000), 2000.0)
self.assertEqual(math.log2(MyIndexable(2**2000)), 2000.0)

self.assertRaises(ValueError, math.log2, 0.0)
self.assertRaises(ValueError, math.log2, 0)
self.assertRaises(ValueError, math.log2, MyIndexable(0))
self.assertRaises(ValueError, math.log2, -1.5)
self.assertRaises(ValueError, math.log2, -1)
self.assertRaises(ValueError, math.log2, MyIndexable(-1))
self.assertRaises(ValueError, math.log2, -2**2000)
self.assertRaises(ValueError, math.log2, MyIndexable(-2**2000))
self.assertRaises(ValueError, math.log2, NINF)
self.assertTrue(math.isnan(math.log2(NAN)))

self.assertEqual(math.log2(IndexableFloatLike(8.0, 2**2000)), 3.0)
self.assertEqual(math.log2(IndexableFloatLike(OverflowError(), 2**2000)), 2000.0)

@requires_IEEE_754
# log2() is not accurate enough on Mac OS X Tiger (10.4)
@support.requires_mac_ver(10, 5)
Expand All @@ -1272,12 +1319,24 @@ def testLog10(self):
self.ftest('log10(1)', math.log10(1), 0)
self.ftest('log10(10)', math.log10(10), 1)
self.ftest('log10(10**1000)', math.log10(10**1000), 1000.0)
self.ftest('log10(MyIndexable(10))', math.log10(MyIndexable(10)), 1)
self.ftest('log10(MyIndexable(10**1000))',
math.log10(MyIndexable(10**1000)), 1000.0)
self.assertRaises(ValueError, math.log10, 0.0)
self.assertRaises(ValueError, math.log10, 0)
self.assertRaises(ValueError, math.log10, MyIndexable(0))
self.assertRaises(ValueError, math.log10, -1.5)
self.assertRaises(ValueError, math.log10, -1)
self.assertRaises(ValueError, math.log10, MyIndexable(-1))
self.assertRaises(ValueError, math.log10, -10**1000)
self.assertRaises(ValueError, math.log10, MyIndexable(-10**1000))
self.assertRaises(ValueError, math.log10, NINF)
self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log10(NAN)))

self.assertEqual(math.log10(IndexableFloatLike(100.0, 10**1000)), 2.0)
self.assertEqual(math.log10(IndexableFloatLike(OverflowError(), 10**1000)), 1000.0)

@support.bigmemtest(2**32, memuse=0.2)
def test_log_huge_integer(self, size):
v = 1 << size
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`math.log` now supports arbitrary large integer-like arguments in the
same way as arbitrary large integer arguments.
79 changes: 49 additions & 30 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2212,43 +2212,62 @@ math_modf_impl(PyObject *module, double x)
in that int is larger than PY_SSIZE_T_MAX. */

static PyObject*
loghelper(PyObject* arg, double (*func)(double))
loghelper_int(PyObject* arg, double (*func)(double))
{
/* If it is int, do it ourselves. */
if (PyLong_Check(arg)) {
double x, result;
int64_t e;
double x, result;
int64_t e;

/* Negative or zero inputs give a ValueError. */
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
/* The input can be an arbitrary large integer, so we
don't include it's value in the error message. */
PyErr_SetString(PyExc_ValueError,
"expected a positive input");
return NULL;
}
/* Negative or zero inputs give a ValueError. */
if (!_PyLong_IsPositive((PyLongObject *)arg)) {
PyErr_SetString(PyExc_ValueError,
"expected a positive input");
return NULL;
}

x = PyLong_AsDouble(arg);
if (x == -1.0 && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_OverflowError))
return NULL;
/* Here the conversion to double overflowed, but it's possible
to compute the log anyway. Clear the exception and continue. */
PyErr_Clear();
x = _PyLong_Frexp((PyLongObject *)arg, &e);
assert(e >= 0);
assert(!PyErr_Occurred());
/* Value is ~= x * 2**e, so the log ~= log(x) + log(2) * e. */
result = func(x) + func(2.0) * e;
}
else
/* Successfully converted x to a double. */
result = func(x);
return PyFloat_FromDouble(result);
x = PyLong_AsDouble(arg);
if (x == -1.0 && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_OverflowError))
return NULL;
/* Here the conversion to double overflowed, but it's possible
to compute the log anyway. Clear the exception and continue. */
PyErr_Clear();
x = _PyLong_Frexp((PyLongObject *)arg, &e);
assert(!PyErr_Occurred());
/* Value is ~= x * 2**e, so the log ~= log(x) + log(2) * e. */
result = func(x) + func(2.0) * e;
}
else
/* Successfully converted x to a double. */
result = func(x);
return PyFloat_FromDouble(result);
}

static PyObject*
loghelper(PyObject* arg, double (*func)(double))
{
/* If it is int, do it ourselves. */
if (PyLong_Check(arg)) {
return loghelper_int(arg, func);
}
/* Else let libm handle it by itself. */
return math_1(arg, func, 0, "expected a positive input, got %s");
PyObject *res = math_1(arg, func, 0, "expected a positive input, got %s");
if (res == NULL &&
PyErr_ExceptionMatches(PyExc_OverflowError) &&
PyIndex_Check(arg))
{
/* Here the conversion to double overflowed, but it's possible
to compute the log anyway. Clear the exception, convert to
integer and continue. */
PyErr_Clear();
arg = _PyNumber_Index(arg);
if (arg == NULL) {
return NULL;
}
res = loghelper_int(arg, func);
Py_DECREF(arg);
}
return res;
}


Expand Down
Loading