Skip to content

Commit c9b8e9c

Browse files
authored
bpo-42979: Enhance abstract.c assertions checking slot result (GH-24352)
* bpo-42979: Enhance abstract.c assertions checking slot result Add _Py_CheckSlotResult() function which fails with a fatal error if a slot function succeeded with an exception set or failed with no exception set: write the slot name, the type name and the current exception (if an exception is set).
1 parent eeb701a commit c9b8e9c

File tree

6 files changed

+126
-51
lines changed

6 files changed

+126
-51
lines changed

Include/cpython/abstract.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,4 +376,4 @@ PyAPI_FUNC(void) _Py_add_one_to_index_C(int nd, Py_ssize_t *index,
376376
PyAPI_FUNC(int) _Py_convert_optional_to_ssize_t(PyObject *, void *);
377377

378378
/* Same as PyNumber_Index but can return an instance of a subclass of int. */
379-
PyAPI_FUNC(PyObject *) _PyNumber_Index(PyObject *o);
379+
PyAPI_FUNC(PyObject *) _PyNumber_Index(PyObject *o);

Include/internal/pycore_object.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ _PyObject_IS_GC(PyObject *obj)
168168
// Fast inlined version of PyType_IS_GC()
169169
#define _PyType_IS_GC(t) _PyType_HasFeature((t), Py_TPFLAGS_HAVE_GC)
170170

171+
// Usage: assert(_Py_CheckSlotResult(obj, "__getitem__", result != NULL)));
172+
extern int _Py_CheckSlotResult(
173+
PyObject *obj,
174+
const char *slot_name,
175+
int success);
176+
171177
#ifdef __cplusplus
172178
}
173179
#endif

Lib/test/test_capi.py

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# these are all functions _testcapi exports whose name begins with 'test_'.
33

44
from collections import OrderedDict
5+
import importlib.machinery
6+
import importlib.util
57
import os
68
import pickle
79
import random
@@ -13,8 +15,6 @@
1315
import time
1416
import unittest
1517
import weakref
16-
import importlib.machinery
17-
import importlib.util
1818
from test import support
1919
from test.support import MISSING_C_DOCSTRINGS
2020
from test.support import import_helper
@@ -35,6 +35,10 @@
3535
Py_DEBUG = hasattr(sys, 'gettotalrefcount')
3636

3737

38+
def decode_stderr(err):
39+
return err.decode('utf-8', 'replace').replace('\r', '')
40+
41+
3842
def testfunction(self):
3943
"""some doc"""
4044
return self
@@ -207,23 +211,22 @@ def test_return_null_without_error(self):
207211
_testcapi.return_null_without_error()
208212
""")
209213
rc, out, err = assert_python_failure('-c', code)
210-
self.assertRegex(err.replace(b'\r', b''),
211-
br'Fatal Python error: _Py_CheckFunctionResult: '
212-
br'a function returned NULL '
213-
br'without setting an error\n'
214-
br'Python runtime state: initialized\n'
215-
br'SystemError: <built-in function '
216-
br'return_null_without_error> returned NULL '
217-
br'without setting an error\n'
218-
br'\n'
219-
br'Current thread.*:\n'
220-
br' File .*", line 6 in <module>')
214+
err = decode_stderr(err)
215+
self.assertRegex(err,
216+
r'Fatal Python error: _Py_CheckFunctionResult: '
217+
r'a function returned NULL without setting an exception\n'
218+
r'Python runtime state: initialized\n'
219+
r'SystemError: <built-in function return_null_without_error> '
220+
r'returned NULL without setting an exception\n'
221+
r'\n'
222+
r'Current thread.*:\n'
223+
r' File .*", line 6 in <module>\n')
221224
else:
222225
with self.assertRaises(SystemError) as cm:
223226
_testcapi.return_null_without_error()
224227
self.assertRegex(str(cm.exception),
225228
'return_null_without_error.* '
226-
'returned NULL without setting an error')
229+
'returned NULL without setting an exception')
227230

228231
def test_return_result_with_error(self):
229232
# Issue #23571: A function must not return a result with an error set
@@ -236,28 +239,58 @@ def test_return_result_with_error(self):
236239
_testcapi.return_result_with_error()
237240
""")
238241
rc, out, err = assert_python_failure('-c', code)
239-
self.assertRegex(err.replace(b'\r', b''),
240-
br'Fatal Python error: _Py_CheckFunctionResult: '
241-
br'a function returned a result '
242-
br'with an error set\n'
243-
br'Python runtime state: initialized\n'
244-
br'ValueError\n'
245-
br'\n'
246-
br'The above exception was the direct cause '
247-
br'of the following exception:\n'
248-
br'\n'
249-
br'SystemError: <built-in '
250-
br'function return_result_with_error> '
251-
br'returned a result with an error set\n'
252-
br'\n'
253-
br'Current thread.*:\n'
254-
br' File .*, line 6 in <module>')
242+
err = decode_stderr(err)
243+
self.assertRegex(err,
244+
r'Fatal Python error: _Py_CheckFunctionResult: '
245+
r'a function returned a result with an exception set\n'
246+
r'Python runtime state: initialized\n'
247+
r'ValueError\n'
248+
r'\n'
249+
r'The above exception was the direct cause '
250+
r'of the following exception:\n'
251+
r'\n'
252+
r'SystemError: <built-in '
253+
r'function return_result_with_error> '
254+
r'returned a result with an exception set\n'
255+
r'\n'
256+
r'Current thread.*:\n'
257+
r' File .*, line 6 in <module>\n')
255258
else:
256259
with self.assertRaises(SystemError) as cm:
257260
_testcapi.return_result_with_error()
258261
self.assertRegex(str(cm.exception),
259262
'return_result_with_error.* '
260-
'returned a result with an error set')
263+
'returned a result with an exception set')
264+
265+
def test_getitem_with_error(self):
266+
# Test _Py_CheckSlotResult(). Raise an exception and then calls
267+
# PyObject_GetItem(): check that the assertion catchs the bug.
268+
# PyObject_GetItem() must not be called with an exception set.
269+
code = textwrap.dedent("""
270+
import _testcapi
271+
from test import support
272+
273+
with support.SuppressCrashReport():
274+
_testcapi.getitem_with_error({1: 2}, 1)
275+
""")
276+
rc, out, err = assert_python_failure('-c', code)
277+
err = decode_stderr(err)
278+
if 'SystemError: ' not in err:
279+
self.assertRegex(err,
280+
r'Fatal Python error: _Py_CheckSlotResult: '
281+
r'Slot __getitem__ of type dict succeeded '
282+
r'with an exception set\n'
283+
r'Python runtime state: initialized\n'
284+
r'ValueError: bug\n'
285+
r'\n'
286+
r'Current thread .* \(most recent call first\):\n'
287+
r' File .*, line 6 in <module>\n'
288+
r'\n'
289+
r'Extension modules: _testcapi \(total: 1\)\n')
290+
else:
291+
# Python built with NDEBUG macro defined:
292+
# test _Py_CheckFunctionResult() instead.
293+
self.assertIn('returned a result with an exception set', err)
261294

262295
def test_buildvalue_N(self):
263296
_testcapi.test_buildvalue_N()
@@ -551,7 +584,7 @@ def check_fatal_error(self, code, expected, not_expected=()):
551584
with support.SuppressCrashReport():
552585
rc, out, err = assert_python_failure('-sSI', '-c', code)
553586

554-
err = err.replace(b'\r', b'').decode('ascii', 'replace')
587+
err = decode_stderr(err)
555588
self.assertIn('Fatal Python error: test_fatal_error: MESSAGE\n',
556589
err)
557590

Modules/_testcapimodule.c

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4736,6 +4736,18 @@ return_result_with_error(PyObject *self, PyObject *args)
47364736
Py_RETURN_NONE;
47374737
}
47384738

4739+
static PyObject*
4740+
getitem_with_error(PyObject *self, PyObject *args)
4741+
{
4742+
PyObject *map, *key;
4743+
if (!PyArg_ParseTuple(args, "OO", &map, &key)) {
4744+
return NULL;
4745+
}
4746+
4747+
PyErr_SetString(PyExc_ValueError, "bug");
4748+
return PyObject_GetItem(map, key);
4749+
}
4750+
47394751
static PyObject *
47404752
test_pytime_fromseconds(PyObject *self, PyObject *args)
47414753
{
@@ -5901,10 +5913,9 @@ static PyMethodDef TestMethods[] = {
59015913
pymarshal_read_last_object_from_file, METH_VARARGS},
59025914
{"pymarshal_read_object_from_file",
59035915
pymarshal_read_object_from_file, METH_VARARGS},
5904-
{"return_null_without_error",
5905-
return_null_without_error, METH_NOARGS},
5906-
{"return_result_with_error",
5907-
return_result_with_error, METH_NOARGS},
5916+
{"return_null_without_error", return_null_without_error, METH_NOARGS},
5917+
{"return_result_with_error", return_result_with_error, METH_NOARGS},
5918+
{"getitem_with_error", getitem_with_error, METH_VARARGS},
59085919
{"PyTime_FromSeconds", test_pytime_fromseconds, METH_VARARGS},
59095920
{"PyTime_FromSecondsObject", test_pytime_fromsecondsobject, METH_VARARGS},
59105921
{"PyTime_AsSecondsDouble", test_pytime_assecondsdouble, METH_VARARGS},

Objects/abstract.c

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* Abstract Object Interface (many thanks to Jim Fulton) */
22

33
#include "Python.h"
4-
#include "pycore_unionobject.h" // _Py_UnionType && _Py_Union()
54
#include "pycore_abstract.h" // _PyIndex_Check()
65
#include "pycore_ceval.h" // _Py_EnterRecursiveCall()
6+
#include "pycore_object.h" // _Py_CheckSlotResult()
77
#include "pycore_pyerrors.h" // _PyErr_Occurred()
88
#include "pycore_pystate.h" // _PyThreadState_GET()
9+
#include "pycore_unionobject.h" // _Py_UnionType && _Py_Union()
910
#include <ctype.h>
1011
#include <stddef.h> // offsetof()
1112
#include "longintrepr.h"
@@ -61,7 +62,7 @@ PyObject_Size(PyObject *o)
6162
m = Py_TYPE(o)->tp_as_sequence;
6263
if (m && m->sq_length) {
6364
Py_ssize_t len = m->sq_length(o);
64-
assert(len >= 0 || PyErr_Occurred());
65+
assert(_Py_CheckSlotResult(o, "__len__", len >= 0));
6566
return len;
6667
}
6768

@@ -160,7 +161,7 @@ PyObject_GetItem(PyObject *o, PyObject *key)
160161
m = Py_TYPE(o)->tp_as_mapping;
161162
if (m && m->mp_subscript) {
162163
PyObject *item = m->mp_subscript(o, key);
163-
assert((item != NULL) ^ (PyErr_Occurred() != NULL));
164+
assert(_Py_CheckSlotResult(o, "__getitem__", item != NULL));
164165
return item;
165166
}
166167

@@ -1564,7 +1565,7 @@ PySequence_Size(PyObject *s)
15641565
m = Py_TYPE(s)->tp_as_sequence;
15651566
if (m && m->sq_length) {
15661567
Py_ssize_t len = m->sq_length(s);
1567-
assert(len >= 0 || PyErr_Occurred());
1568+
assert(_Py_CheckSlotResult(s, "__len__", len >= 0));
15681569
return len;
15691570
}
15701571

@@ -1708,8 +1709,8 @@ PySequence_GetItem(PyObject *s, Py_ssize_t i)
17081709
if (i < 0) {
17091710
if (m->sq_length) {
17101711
Py_ssize_t l = (*m->sq_length)(s);
1712+
assert(_Py_CheckSlotResult(s, "__len__", l >= 0));
17111713
if (l < 0) {
1712-
assert(PyErr_Occurred());
17131714
return NULL;
17141715
}
17151716
i += l;
@@ -1762,8 +1763,8 @@ PySequence_SetItem(PyObject *s, Py_ssize_t i, PyObject *o)
17621763
if (i < 0) {
17631764
if (m->sq_length) {
17641765
Py_ssize_t l = (*m->sq_length)(s);
1766+
assert(_Py_CheckSlotResult(s, "__len__", l >= 0));
17651767
if (l < 0) {
1766-
assert(PyErr_Occurred());
17671768
return -1;
17681769
}
17691770
i += l;
@@ -1795,8 +1796,8 @@ PySequence_DelItem(PyObject *s, Py_ssize_t i)
17951796
if (i < 0) {
17961797
if (m->sq_length) {
17971798
Py_ssize_t l = (*m->sq_length)(s);
1799+
assert(_Py_CheckSlotResult(s, "__len__", l >= 0));
17981800
if (l < 0) {
1799-
assert(PyErr_Occurred());
18001801
return -1;
18011802
}
18021803
i += l;
@@ -2145,7 +2146,7 @@ PyMapping_Size(PyObject *o)
21452146
m = Py_TYPE(o)->tp_as_mapping;
21462147
if (m && m->mp_length) {
21472148
Py_ssize_t len = m->mp_length(o);
2148-
assert(len >= 0 || PyErr_Occurred());
2149+
assert(_Py_CheckSlotResult(o, "__len__", len >= 0));
21492150
return len;
21502151
}
21512152

Objects/call.c

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ _Py_CheckFunctionResult(PyThreadState *tstate, PyObject *callable,
3939
if (!_PyErr_Occurred(tstate)) {
4040
if (callable)
4141
_PyErr_Format(tstate, PyExc_SystemError,
42-
"%R returned NULL without setting an error",
42+
"%R returned NULL without setting an exception",
4343
callable);
4444
else
4545
_PyErr_Format(tstate, PyExc_SystemError,
46-
"%s returned NULL without setting an error",
46+
"%s returned NULL without setting an exception",
4747
where);
4848
#ifdef Py_DEBUG
4949
/* Ensure that the bug is caught in debug mode.
5050
Py_FatalError() logs the SystemError exception raised above. */
51-
Py_FatalError("a function returned NULL without setting an error");
51+
Py_FatalError("a function returned NULL without setting an exception");
5252
#endif
5353
return NULL;
5454
}
@@ -60,17 +60,17 @@ _Py_CheckFunctionResult(PyThreadState *tstate, PyObject *callable,
6060
if (callable) {
6161
_PyErr_FormatFromCauseTstate(
6262
tstate, PyExc_SystemError,
63-
"%R returned a result with an error set", callable);
63+
"%R returned a result with an exception set", callable);
6464
}
6565
else {
6666
_PyErr_FormatFromCauseTstate(
6767
tstate, PyExc_SystemError,
68-
"%s returned a result with an error set", where);
68+
"%s returned a result with an exception set", where);
6969
}
7070
#ifdef Py_DEBUG
7171
/* Ensure that the bug is caught in debug mode.
7272
Py_FatalError() logs the SystemError exception raised above. */
73-
Py_FatalError("a function returned a result with an error set");
73+
Py_FatalError("a function returned a result with an exception set");
7474
#endif
7575
return NULL;
7676
}
@@ -79,6 +79,30 @@ _Py_CheckFunctionResult(PyThreadState *tstate, PyObject *callable,
7979
}
8080

8181

82+
int
83+
_Py_CheckSlotResult(PyObject *obj, const char *slot_name, int success)
84+
{
85+
PyThreadState *tstate = _PyThreadState_GET();
86+
if (!success) {
87+
if (!_PyErr_Occurred(tstate)) {
88+
_Py_FatalErrorFormat(__func__,
89+
"Slot %s of type %s failed "
90+
"without setting an exception",
91+
slot_name, Py_TYPE(obj)->tp_name);
92+
}
93+
}
94+
else {
95+
if (_PyErr_Occurred(tstate)) {
96+
_Py_FatalErrorFormat(__func__,
97+
"Slot %s of type %s succeeded "
98+
"with an exception set",
99+
slot_name, Py_TYPE(obj)->tp_name);
100+
}
101+
}
102+
return 1;
103+
}
104+
105+
82106
/* --- Core PyObject call functions ------------------------------- */
83107

84108
/* Call a callable Python object without any arguments */

0 commit comments

Comments
 (0)