From d0de40ffdf46e73d847c5955eed1241633f5d4dd Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith [Google LLC]" Date: Thu, 3 Nov 2022 12:19:07 -0700 Subject: [PATCH 1/3] gh-90716: bugfixes and more tests for _pylong. * Properly decref on _pylong import error. * Improve the error message on _pylong TypeError. * Tie the return value comments together. These are minor followups to issues not caught among the reviewers on https://github.com/python/cpython/pull/96673. --- Lib/test/test_int.py | 31 +++++++++++++++++++++++++++++++ Objects/longobject.c | 8 +++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py index f484c59f675f2a..06accd19752b2f 100644 --- a/Lib/test/test_int.py +++ b/Lib/test/test_int.py @@ -2,10 +2,13 @@ import time import unittest +from unittest import mock from test import support from test.test_grammar import (VALID_UNDERSCORE_LITERALS, INVALID_UNDERSCORE_LITERALS) +import _pylong + L = [ ('0', 0), ('1', 1), @@ -841,6 +844,34 @@ def test_pylong_str_to_int(self): with self.assertRaises(ValueError) as err: int('_' + s) + @mock.patch.object(_pylong, "int_to_decimal_string") + def test_pylong_misbehavior_error_path_to_str( + self, mock_int_to_str): + with support.adjust_int_max_str_digits(20_000): + big_value = int('7'*19_999) + mock_int_to_str.return_value = None # not a str + with self.assertRaises(TypeError) as ctx: + str(big_value) + self.assertIn('non-string', str(ctx.exception)) + mock_int_to_str.side_effect = RuntimeError("testABC") + with self.assertRaises(RuntimeError): + str(big_value) + + @mock.patch.object(_pylong, "int_from_string") + def test_pylong_misbehavior_error_path_from_str( + self, mock_int_from_str): + big_value = '7'*19_999 + with support.adjust_int_max_str_digits(20_000): + mock_int_from_str.return_value = b'not an int' + with self.assertRaises(TypeError) as ctx: + int(big_value) + self.assertIn('_pylong.int_from_string', str(ctx.exception)) + + mock_int_from_str.side_effect = RuntimeError("test123") + with self.assertRaises(RuntimeError): + int(big_value) + + if __name__ == "__main__": unittest.main() diff --git a/Objects/longobject.c b/Objects/longobject.c index cf859ccb970730..a37b6d4f1c0508 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -2362,6 +2362,7 @@ pylong_int_from_string(const char *start, const char *end, PyLongObject **res) } PyObject *s = PyUnicode_FromStringAndSize(start, end-start); if (s == NULL) { + Py_DECREF(mod); goto error; } PyObject *result = PyObject_CallMethod(mod, "int_from_string", "O", s); @@ -2371,14 +2372,14 @@ pylong_int_from_string(const char *start, const char *end, PyLongObject **res) goto error; } if (!PyLong_Check(result)) { - PyErr_SetString(PyExc_TypeError, "an integer is required"); + PyErr_SetString(PyExc_TypeError, "_pylong.int_from_string did not return an int"); goto error; } *res = (PyLongObject *)result; return 0; error: *res = NULL; - return 0; + return 0; // See the long_from_string_base() API comment. } #endif /* WITH_PYLONG_MODULE */ @@ -2617,7 +2618,8 @@ long_from_non_binary_base(const char *start, const char *end, Py_ssize_t digits, * Return values: * * - Returns -1 on syntax error (exception needs to be set, *res is untouched) - * - Returns 0 and sets *res to NULL for MemoryError/OverflowError. + * - Returns 0 and sets *res to NULL for MemoryError, OverflowError, or + * _pylong.int_from_string() errors. * - Returns 0 and sets *res to an unsigned, unnormalized PyLong (success!). * * Afterwards *str is set to point to the first non-digit (which may be *str!). From fdc71c594099c55ce57f05e5cf66be09491e596c Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith [Google LLC]" Date: Thu, 3 Nov 2022 14:45:52 -0700 Subject: [PATCH 2/3] mark the new tests cpython_only As they covers cpython internal details. --- Lib/test/test_int.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py index 06accd19752b2f..ab1c6094212592 100644 --- a/Lib/test/test_int.py +++ b/Lib/test/test_int.py @@ -7,7 +7,10 @@ from test.test_grammar import (VALID_UNDERSCORE_LITERALS, INVALID_UNDERSCORE_LITERALS) -import _pylong +try: + import _pylong +except ImportError: + _pylong = None L = [ ('0', 0), @@ -844,6 +847,8 @@ def test_pylong_str_to_int(self): with self.assertRaises(ValueError) as err: int('_' + s) + @support.cpython_only # tests implementation details of CPython. + @unittest.skipUnless(_pylong, "_pylong module required") @mock.patch.object(_pylong, "int_to_decimal_string") def test_pylong_misbehavior_error_path_to_str( self, mock_int_to_str): @@ -857,6 +862,8 @@ def test_pylong_misbehavior_error_path_to_str( with self.assertRaises(RuntimeError): str(big_value) + @support.cpython_only # tests implementation details of CPython. + @unittest.skipUnless(_pylong, "_pylong module required") @mock.patch.object(_pylong, "int_from_string") def test_pylong_misbehavior_error_path_from_str( self, mock_int_from_str): @@ -872,6 +879,5 @@ def test_pylong_misbehavior_error_path_from_str( int(big_value) - if __name__ == "__main__": unittest.main() From 5320f3b8f2e079e960d18e27cb2961eddab2c0d4 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith [Google LLC]" Date: Thu, 3 Nov 2022 15:33:39 -0700 Subject: [PATCH 3/3] Fix the assertion error in pydebug builds by adding the same check and TypeError on the to_decimal_string side. --- Lib/test/test_int.py | 6 ++++-- Objects/longobject.c | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py index ab1c6094212592..334fea0774be51 100644 --- a/Lib/test/test_int.py +++ b/Lib/test/test_int.py @@ -857,7 +857,8 @@ def test_pylong_misbehavior_error_path_to_str( mock_int_to_str.return_value = None # not a str with self.assertRaises(TypeError) as ctx: str(big_value) - self.assertIn('non-string', str(ctx.exception)) + self.assertIn('_pylong.int_to_decimal_string did not', + str(ctx.exception)) mock_int_to_str.side_effect = RuntimeError("testABC") with self.assertRaises(RuntimeError): str(big_value) @@ -872,7 +873,8 @@ def test_pylong_misbehavior_error_path_from_str( mock_int_from_str.return_value = b'not an int' with self.assertRaises(TypeError) as ctx: int(big_value) - self.assertIn('_pylong.int_from_string', str(ctx.exception)) + self.assertIn('_pylong.int_from_string did not', + str(ctx.exception)) mock_int_from_str.side_effect = RuntimeError("test123") with self.assertRaises(RuntimeError): diff --git a/Objects/longobject.c b/Objects/longobject.c index a37b6d4f1c0508..a87293899001ed 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -1753,7 +1753,11 @@ pylong_int_to_decimal_string(PyObject *aa, if (s == NULL) { goto error; } - assert(PyUnicode_Check(s)); + if (!PyUnicode_Check(s)) { + PyErr_SetString(PyExc_TypeError, + "_pylong.int_to_decimal_string did not return a str"); + goto error; + } if (writer) { Py_ssize_t size = PyUnicode_GET_LENGTH(s); if (_PyUnicodeWriter_Prepare(writer, size, '9') == -1) { @@ -2372,7 +2376,8 @@ pylong_int_from_string(const char *start, const char *end, PyLongObject **res) goto error; } if (!PyLong_Check(result)) { - PyErr_SetString(PyExc_TypeError, "_pylong.int_from_string did not return an int"); + PyErr_SetString(PyExc_TypeError, + "_pylong.int_from_string did not return an int"); goto error; } *res = (PyLongObject *)result;