Skip to content

Commit 56d1f5c

Browse files
authored
bpo-30697: Fix PyErr_NormalizeException() when no memory (pythonGH-2327)
1 parent 275d2d9 commit 56d1f5c

File tree

8 files changed

+203
-53
lines changed

8 files changed

+203
-53
lines changed

Doc/whatsnew/3.7.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ Build and C API Changes
440440
download a copy of 32-bit Python for this purpose. (Contributed by Zachary
441441
Ware in :issue:`30450`.)
442442

443+
* The ``PyExc_RecursionErrorInst`` singleton that was part of the public API
444+
has been removed as its members being never cleared may cause a segfault
445+
during finalization of the interpreter. Contributed by Xavier de Gaye in
446+
:issue:`22898` and :issue:`30697`.
447+
443448
* Support for building ``--without-threads`` is removed.
444449
(Contributed by Antoine Pitrou in :issue:`31370`.).
445450

Include/pyerrors.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,6 @@ PyAPI_DATA(PyObject *) PyExc_IOError;
220220
PyAPI_DATA(PyObject *) PyExc_WindowsError;
221221
#endif
222222

223-
PyAPI_DATA(PyObject *) PyExc_RecursionErrorInst;
224-
225223
/* Predefined warning categories */
226224
PyAPI_DATA(PyObject *) PyExc_Warning;
227225
PyAPI_DATA(PyObject *) PyExc_UserWarning;

Lib/test/test_exceptions.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
from test.support import (TESTFN, captured_stderr, check_impl_detail,
1212
check_warnings, cpython_only, gc_collect, run_unittest,
13-
no_tracing, unlink, import_module, script_helper)
14-
13+
no_tracing, unlink, import_module, script_helper,
14+
SuppressCrashReport)
1515
class NaiveException(Exception):
1616
def __init__(self, x):
1717
self.x = x
@@ -936,6 +936,105 @@ def g():
936936
self.assertIsInstance(v, RecursionError, type(v))
937937
self.assertIn("maximum recursion depth exceeded", str(v))
938938

939+
@cpython_only
940+
def test_recursion_normalizing_exception(self):
941+
# Issue #22898.
942+
# Test that a RecursionError is raised when tstate->recursion_depth is
943+
# equal to recursion_limit in PyErr_NormalizeException() and check
944+
# that a ResourceWarning is printed.
945+
# Prior to #22898, the recursivity of PyErr_NormalizeException() was
946+
# controled by tstate->recursion_depth and a PyExc_RecursionErrorInst
947+
# singleton was being used in that case, that held traceback data and
948+
# locals indefinitely and would cause a segfault in _PyExc_Fini() upon
949+
# finalization of these locals.
950+
code = """if 1:
951+
import sys
952+
from _testcapi import get_recursion_depth
953+
954+
class MyException(Exception): pass
955+
956+
def setrecursionlimit(depth):
957+
while 1:
958+
try:
959+
sys.setrecursionlimit(depth)
960+
return depth
961+
except RecursionError:
962+
# sys.setrecursionlimit() raises a RecursionError if
963+
# the new recursion limit is too low (issue #25274).
964+
depth += 1
965+
966+
def recurse(cnt):
967+
cnt -= 1
968+
if cnt:
969+
recurse(cnt)
970+
else:
971+
generator.throw(MyException)
972+
973+
def gen():
974+
f = open(%a, mode='rb', buffering=0)
975+
yield
976+
977+
generator = gen()
978+
next(generator)
979+
recursionlimit = sys.getrecursionlimit()
980+
depth = get_recursion_depth()
981+
try:
982+
# Upon the last recursive invocation of recurse(),
983+
# tstate->recursion_depth is equal to (recursion_limit - 1)
984+
# and is equal to recursion_limit when _gen_throw() calls
985+
# PyErr_NormalizeException().
986+
recurse(setrecursionlimit(depth + 2) - depth - 1)
987+
finally:
988+
sys.setrecursionlimit(recursionlimit)
989+
print('Done.')
990+
""" % __file__
991+
rc, out, err = script_helper.assert_python_failure("-Wd", "-c", code)
992+
# Check that the program does not fail with SIGABRT.
993+
self.assertEqual(rc, 1)
994+
self.assertIn(b'RecursionError', err)
995+
self.assertIn(b'ResourceWarning', err)
996+
self.assertIn(b'Done.', out)
997+
998+
@cpython_only
999+
def test_recursion_normalizing_infinite_exception(self):
1000+
# Issue #30697. Test that a RecursionError is raised when
1001+
# PyErr_NormalizeException() maximum recursion depth has been
1002+
# exceeded.
1003+
code = """if 1:
1004+
import _testcapi
1005+
try:
1006+
raise _testcapi.RecursingInfinitelyError
1007+
finally:
1008+
print('Done.')
1009+
"""
1010+
rc, out, err = script_helper.assert_python_failure("-c", code)
1011+
self.assertEqual(rc, 1)
1012+
self.assertIn(b'RecursionError: maximum recursion depth exceeded '
1013+
b'while normalizing an exception', err)
1014+
self.assertIn(b'Done.', out)
1015+
1016+
@cpython_only
1017+
def test_recursion_normalizing_with_no_memory(self):
1018+
# Issue #30697. Test that in the abort that occurs when there is no
1019+
# memory left and the size of the Python frames stack is greater than
1020+
# the size of the list of preallocated MemoryError instances, the
1021+
# Fatal Python error message mentions MemoryError.
1022+
code = """if 1:
1023+
import _testcapi
1024+
class C(): pass
1025+
def recurse(cnt):
1026+
cnt -= 1
1027+
if cnt:
1028+
recurse(cnt)
1029+
else:
1030+
_testcapi.set_nomemory(0)
1031+
C()
1032+
recurse(16)
1033+
"""
1034+
with SuppressCrashReport():
1035+
rc, out, err = script_helper.assert_python_failure("-c", code)
1036+
self.assertIn(b'Fatal Python error: Cannot recover from '
1037+
b'MemoryErrors while normalizing exceptions.', err)
9391038

9401039
@cpython_only
9411040
def test_MemoryError(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The `PyExc_RecursionErrorInst` singleton is removed and
2+
`PyErr_NormalizeException()` does not use it anymore. This singleton is
3+
persistent and its members being never cleared may cause a segfault during
4+
finalization of the interpreter. See also issue #22898.

Modules/_testcapimodule.c

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4940,6 +4940,61 @@ static PyTypeObject awaitType = {
49404940
};
49414941

49424942

4943+
static int recurse_infinitely_error_init(PyObject *, PyObject *, PyObject *);
4944+
4945+
static PyTypeObject PyRecursingInfinitelyError_Type = {
4946+
PyVarObject_HEAD_INIT(NULL, 0)
4947+
"RecursingInfinitelyError", /* tp_name */
4948+
sizeof(PyBaseExceptionObject), /* tp_basicsize */
4949+
0, /* tp_itemsize */
4950+
0, /* tp_dealloc */
4951+
0, /* tp_print */
4952+
0, /* tp_getattr */
4953+
0, /* tp_setattr */
4954+
0, /* tp_reserved */
4955+
0, /* tp_repr */
4956+
0, /* tp_as_number */
4957+
0, /* tp_as_sequence */
4958+
0, /* tp_as_mapping */
4959+
0, /* tp_hash */
4960+
0, /* tp_call */
4961+
0, /* tp_str */
4962+
0, /* tp_getattro */
4963+
0, /* tp_setattro */
4964+
0, /* tp_as_buffer */
4965+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
4966+
"Instantiating this exception starts infinite recursion.", /* tp_doc */
4967+
0, /* tp_traverse */
4968+
0, /* tp_clear */
4969+
0, /* tp_richcompare */
4970+
0, /* tp_weaklistoffset */
4971+
0, /* tp_iter */
4972+
0, /* tp_iternext */
4973+
0, /* tp_methods */
4974+
0, /* tp_members */
4975+
0, /* tp_getset */
4976+
0, /* tp_base */
4977+
0, /* tp_dict */
4978+
0, /* tp_descr_get */
4979+
0, /* tp_descr_set */
4980+
0, /* tp_dictoffset */
4981+
(initproc)recurse_infinitely_error_init, /* tp_init */
4982+
0, /* tp_alloc */
4983+
0, /* tp_new */
4984+
};
4985+
4986+
static int
4987+
recurse_infinitely_error_init(PyObject *self, PyObject *args, PyObject *kwds)
4988+
{
4989+
PyObject *type = (PyObject *)&PyRecursingInfinitelyError_Type;
4990+
4991+
/* Instantiating this exception starts infinite recursion. */
4992+
Py_INCREF(type);
4993+
PyErr_SetObject(type, NULL);
4994+
return -1;
4995+
}
4996+
4997+
49434998
static struct PyModuleDef _testcapimodule = {
49444999
PyModuleDef_HEAD_INIT,
49455000
"_testcapi",
@@ -4981,6 +5036,14 @@ PyInit__testcapi(void)
49815036
Py_INCREF(&awaitType);
49825037
PyModule_AddObject(m, "awaitType", (PyObject *)&awaitType);
49835038

5039+
PyRecursingInfinitelyError_Type.tp_base = (PyTypeObject *)PyExc_Exception;
5040+
if (PyType_Ready(&PyRecursingInfinitelyError_Type) < 0) {
5041+
return NULL;
5042+
}
5043+
Py_INCREF(&PyRecursingInfinitelyError_Type);
5044+
PyModule_AddObject(m, "RecursingInfinitelyError",
5045+
(PyObject *)&PyRecursingInfinitelyError_Type);
5046+
49845047
PyModule_AddObject(m, "CHAR_MAX", PyLong_FromLong(CHAR_MAX));
49855048
PyModule_AddObject(m, "CHAR_MIN", PyLong_FromLong(CHAR_MIN));
49865049
PyModule_AddObject(m, "UCHAR_MAX", PyLong_FromLong(UCHAR_MAX));

Objects/exceptions.c

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,12 +2409,6 @@ SimpleExtendsException(PyExc_Warning, ResourceWarning,
24092409

24102410

24112411

2412-
/* Pre-computed RecursionError instance for when recursion depth is reached.
2413-
Meant to be used when normalizing the exception for exceeding the recursion
2414-
depth will cause its own infinite recursion.
2415-
*/
2416-
PyObject *PyExc_RecursionErrorInst = NULL;
2417-
24182412
#define PRE_INIT(TYPE) \
24192413
if (!(_PyExc_ ## TYPE.tp_flags & Py_TPFLAGS_READY)) { \
24202414
if (PyType_Ready(&_PyExc_ ## TYPE) < 0) \
@@ -2674,37 +2668,11 @@ _PyExc_Init(PyObject *bltinmod)
26742668
ADD_ERRNO(TimeoutError, ETIMEDOUT)
26752669

26762670
preallocate_memerrors();
2677-
2678-
if (!PyExc_RecursionErrorInst) {
2679-
PyExc_RecursionErrorInst = BaseException_new(&_PyExc_RecursionError, NULL, NULL);
2680-
if (!PyExc_RecursionErrorInst)
2681-
Py_FatalError("Cannot pre-allocate RecursionError instance for "
2682-
"recursion errors");
2683-
else {
2684-
PyBaseExceptionObject *err_inst =
2685-
(PyBaseExceptionObject *)PyExc_RecursionErrorInst;
2686-
PyObject *args_tuple;
2687-
PyObject *exc_message;
2688-
exc_message = PyUnicode_FromString("maximum recursion depth exceeded");
2689-
if (!exc_message)
2690-
Py_FatalError("cannot allocate argument for RecursionError "
2691-
"pre-allocation");
2692-
args_tuple = PyTuple_Pack(1, exc_message);
2693-
if (!args_tuple)
2694-
Py_FatalError("cannot allocate tuple for RecursionError "
2695-
"pre-allocation");
2696-
Py_DECREF(exc_message);
2697-
if (BaseException_init(err_inst, args_tuple, NULL))
2698-
Py_FatalError("init of pre-allocated RecursionError failed");
2699-
Py_DECREF(args_tuple);
2700-
}
2701-
}
27022671
}
27032672

27042673
void
27052674
_PyExc_Fini(void)
27062675
{
2707-
Py_CLEAR(PyExc_RecursionErrorInst);
27082676
free_preallocated_memerrors();
27092677
Py_CLEAR(errnomap);
27102678
}

PC/python3.def

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ EXPORTS
224224
PyExc_PermissionError=python37.PyExc_PermissionError DATA
225225
PyExc_ProcessLookupError=python37.PyExc_ProcessLookupError DATA
226226
PyExc_RecursionError=python37.PyExc_RecursionError DATA
227-
PyExc_RecursionErrorInst=python37.PyExc_RecursionErrorInst DATA
228227
PyExc_ReferenceError=python37.PyExc_ReferenceError DATA
229228
PyExc_ResourceWarning=python37.PyExc_ResourceWarning DATA
230229
PyExc_RuntimeError=python37.PyExc_RuntimeError DATA

Python/errors.c

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -218,20 +218,24 @@ PyErr_ExceptionMatches(PyObject *exc)
218218
}
219219

220220

221+
#ifndef Py_NORMALIZE_RECURSION_LIMIT
222+
#define Py_NORMALIZE_RECURSION_LIMIT 32
223+
#endif
224+
221225
/* Used in many places to normalize a raised exception, including in
222226
eval_code2(), do_raise(), and PyErr_Print()
223227
224228
XXX: should PyErr_NormalizeException() also call
225229
PyException_SetTraceback() with the resulting value and tb?
226230
*/
227-
void
228-
PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb)
231+
static void
232+
PyErr_NormalizeExceptionEx(PyObject **exc, PyObject **val,
233+
PyObject **tb, int recursion_depth)
229234
{
230235
PyObject *type = *exc;
231236
PyObject *value = *val;
232237
PyObject *inclass = NULL;
233238
PyObject *initial_tb = NULL;
234-
PyThreadState *tstate = NULL;
235239

236240
if (type == NULL) {
237241
/* There was no exception, so nothing to do. */
@@ -293,6 +297,10 @@ PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb)
293297
finally:
294298
Py_DECREF(type);
295299
Py_DECREF(value);
300+
if (recursion_depth + 1 == Py_NORMALIZE_RECURSION_LIMIT) {
301+
PyErr_SetString(PyExc_RecursionError, "maximum recursion depth "
302+
"exceeded while normalizing an exception");
303+
}
296304
/* If the new exception doesn't set a traceback and the old
297305
exception had a traceback, use the old traceback for the
298306
new exception. It's better than nothing.
@@ -305,20 +313,26 @@ PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb)
305313
else
306314
Py_DECREF(initial_tb);
307315
}
308-
/* normalize recursively */
309-
tstate = PyThreadState_GET();
310-
if (++tstate->recursion_depth > Py_GetRecursionLimit()) {
311-
--tstate->recursion_depth;
312-
/* throw away the old exception and use the recursion error instead */
313-
Py_INCREF(PyExc_RecursionError);
314-
Py_SETREF(*exc, PyExc_RecursionError);
315-
Py_INCREF(PyExc_RecursionErrorInst);
316-
Py_SETREF(*val, PyExc_RecursionErrorInst);
317-
/* just keeping the old traceback */
318-
return;
316+
/* Normalize recursively.
317+
* Abort when Py_NORMALIZE_RECURSION_LIMIT has been exceeded and the
318+
* corresponding RecursionError could not be normalized.*/
319+
if (++recursion_depth > Py_NORMALIZE_RECURSION_LIMIT) {
320+
if (PyErr_GivenExceptionMatches(*exc, PyExc_MemoryError)) {
321+
Py_FatalError("Cannot recover from MemoryErrors "
322+
"while normalizing exceptions.");
323+
}
324+
else {
325+
Py_FatalError("Cannot recover from the recursive normalization "
326+
"of an exception.");
327+
}
319328
}
320-
PyErr_NormalizeException(exc, val, tb);
321-
--tstate->recursion_depth;
329+
PyErr_NormalizeExceptionEx(exc, val, tb, recursion_depth);
330+
}
331+
332+
void
333+
PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb)
334+
{
335+
PyErr_NormalizeExceptionEx(exc, val, tb, 0);
322336
}
323337

324338

0 commit comments

Comments
 (0)