Skip to content

Commit 633ea21

Browse files
gh-107915: Handle errors in C API functions PyErr_Set*() and PyErr_Format() (GH-107918)
Such C API functions as PyErr_SetString(), PyErr_Format(), PyErr_SetFromErrnoWithFilename() and many others no longer crash or ignore errors if it failed to format the error message or decode the filename. Instead, they keep a corresponding error.
1 parent 79db9d9 commit 633ea21

File tree

5 files changed

+218
-9
lines changed

5 files changed

+218
-9
lines changed

Lib/test/test_capi/test_exceptions.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import errno
2+
import os
13
import re
24
import sys
35
import unittest
46

57
from test import support
68
from test.support import import_helper
9+
from test.support.os_helper import TESTFN, TESTFN_UNDECODABLE
710
from test.support.script_helper import assert_python_failure
811
from test.support.testcase import ExceptionIsLikeMixin
912

@@ -12,6 +15,8 @@
1215
# Skip this test if the _testcapi module isn't available.
1316
_testcapi = import_helper.import_module('_testcapi')
1417

18+
NULL = None
19+
1520
class Test_Exceptions(unittest.TestCase):
1621

1722
def test_exception(self):
@@ -189,6 +194,82 @@ def __repr__(self):
189194
self.assertEqual(exc.__notes__[0],
190195
'Normalization failed: type=Broken args=<unknown>')
191196

197+
def test_set_string(self):
198+
"""Test PyErr_SetString()"""
199+
setstring = _testcapi.err_setstring
200+
with self.assertRaises(ZeroDivisionError) as e:
201+
setstring(ZeroDivisionError, b'error')
202+
self.assertEqual(e.exception.args, ('error',))
203+
with self.assertRaises(ZeroDivisionError) as e:
204+
setstring(ZeroDivisionError, 'помилка'.encode())
205+
self.assertEqual(e.exception.args, ('помилка',))
206+
207+
with self.assertRaises(UnicodeDecodeError):
208+
setstring(ZeroDivisionError, b'\xff')
209+
self.assertRaises(SystemError, setstring, list, b'error')
210+
# CRASHES setstring(ZeroDivisionError, NULL)
211+
# CRASHES setstring(NULL, b'error')
212+
213+
def test_format(self):
214+
"""Test PyErr_Format()"""
215+
import_helper.import_module('ctypes')
216+
from ctypes import pythonapi, py_object, c_char_p, c_int
217+
name = "PyErr_Format"
218+
PyErr_Format = getattr(pythonapi, name)
219+
PyErr_Format.argtypes = (py_object, c_char_p,)
220+
PyErr_Format.restype = py_object
221+
with self.assertRaises(ZeroDivisionError) as e:
222+
PyErr_Format(ZeroDivisionError, b'%s %d', b'error', c_int(42))
223+
self.assertEqual(e.exception.args, ('error 42',))
224+
with self.assertRaises(ZeroDivisionError) as e:
225+
PyErr_Format(ZeroDivisionError, b'%s', 'помилка'.encode())
226+
self.assertEqual(e.exception.args, ('помилка',))
227+
228+
with self.assertRaisesRegex(OverflowError, 'not in range'):
229+
PyErr_Format(ZeroDivisionError, b'%c', c_int(-1))
230+
with self.assertRaisesRegex(ValueError, 'format string'):
231+
PyErr_Format(ZeroDivisionError, b'\xff')
232+
self.assertRaises(SystemError, PyErr_Format, list, b'error')
233+
# CRASHES PyErr_Format(ZeroDivisionError, NULL)
234+
# CRASHES PyErr_Format(py_object(), b'error')
235+
236+
def test_setfromerrnowithfilename(self):
237+
"""Test PyErr_SetFromErrnoWithFilename()"""
238+
setfromerrnowithfilename = _testcapi.err_setfromerrnowithfilename
239+
ENOENT = errno.ENOENT
240+
with self.assertRaises(FileNotFoundError) as e:
241+
setfromerrnowithfilename(ENOENT, OSError, b'file')
242+
self.assertEqual(e.exception.args,
243+
(ENOENT, 'No such file or directory'))
244+
self.assertEqual(e.exception.errno, ENOENT)
245+
self.assertEqual(e.exception.filename, 'file')
246+
247+
with self.assertRaises(FileNotFoundError) as e:
248+
setfromerrnowithfilename(ENOENT, OSError, os.fsencode(TESTFN))
249+
self.assertEqual(e.exception.filename, TESTFN)
250+
251+
if TESTFN_UNDECODABLE:
252+
with self.assertRaises(FileNotFoundError) as e:
253+
setfromerrnowithfilename(ENOENT, OSError, TESTFN_UNDECODABLE)
254+
self.assertEqual(e.exception.filename,
255+
os.fsdecode(TESTFN_UNDECODABLE))
256+
257+
with self.assertRaises(FileNotFoundError) as e:
258+
setfromerrnowithfilename(ENOENT, OSError, NULL)
259+
self.assertIsNone(e.exception.filename)
260+
261+
with self.assertRaises(OSError) as e:
262+
setfromerrnowithfilename(0, OSError, b'file')
263+
self.assertEqual(e.exception.args, (0, 'Error'))
264+
self.assertEqual(e.exception.errno, 0)
265+
self.assertEqual(e.exception.filename, 'file')
266+
267+
with self.assertRaises(ZeroDivisionError) as e:
268+
setfromerrnowithfilename(ENOENT, ZeroDivisionError, b'file')
269+
self.assertEqual(e.exception.args,
270+
(ENOENT, 'No such file or directory', 'file'))
271+
# CRASHES setfromerrnowithfilename(ENOENT, NULL, b'error')
272+
192273

193274
class Test_PyUnstable_Exc_PrepReraiseStar(ExceptionIsLikeMixin, unittest.TestCase):
194275

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Such C API functions as ``PyErr_SetString()``, ``PyErr_Format()``,
2+
``PyErr_SetFromErrnoWithFilename()`` and many others no longer crash or
3+
ignore errors if it failed to format the error message or decode the
4+
filename. Instead, they keep a corresponding error.

Modules/_testcapi/clinic/exceptions.c.h

Lines changed: 63 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/_testcapi/exceptions.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "parts.h"
22
#include "clinic/exceptions.c.h"
33

4+
#define NULLABLE(x) do { if (x == Py_None) x = NULL; } while (0);
5+
46
/*[clinic input]
57
module _testcapi
68
[clinic start generated code]*/
@@ -129,6 +131,43 @@ _testcapi_exc_set_object_fetch_impl(PyObject *module, PyObject *exc,
129131
return value;
130132
}
131133

134+
/*[clinic input]
135+
_testcapi.err_setstring
136+
exc: object
137+
value: str(zeroes=True, accept={robuffer, str, NoneType})
138+
/
139+
[clinic start generated code]*/
140+
141+
static PyObject *
142+
_testcapi_err_setstring_impl(PyObject *module, PyObject *exc,
143+
const char *value, Py_ssize_t value_length)
144+
/*[clinic end generated code: output=fba8705e5703dd3f input=e8a95fad66d9004b]*/
145+
{
146+
NULLABLE(exc);
147+
PyErr_SetString(exc, value);
148+
return NULL;
149+
}
150+
151+
/*[clinic input]
152+
_testcapi.err_setfromerrnowithfilename
153+
error: int
154+
exc: object
155+
value: str(zeroes=True, accept={robuffer, str, NoneType})
156+
/
157+
[clinic start generated code]*/
158+
159+
static PyObject *
160+
_testcapi_err_setfromerrnowithfilename_impl(PyObject *module, int error,
161+
PyObject *exc, const char *value,
162+
Py_ssize_t value_length)
163+
/*[clinic end generated code: output=d02df5749a01850e input=ff7c384234bf097f]*/
164+
{
165+
NULLABLE(exc);
166+
errno = error;
167+
PyErr_SetFromErrnoWithFilename(exc, value);
168+
return NULL;
169+
}
170+
132171
/*[clinic input]
133172
_testcapi.raise_exception
134173
exception as exc: object
@@ -338,6 +377,8 @@ static PyMethodDef test_methods[] = {
338377
_TESTCAPI_MAKE_EXCEPTION_WITH_DOC_METHODDEF
339378
_TESTCAPI_EXC_SET_OBJECT_METHODDEF
340379
_TESTCAPI_EXC_SET_OBJECT_FETCH_METHODDEF
380+
_TESTCAPI_ERR_SETSTRING_METHODDEF
381+
_TESTCAPI_ERR_SETFROMERRNOWITHFILENAME_METHODDEF
341382
_TESTCAPI_RAISE_EXCEPTION_METHODDEF
342383
_TESTCAPI_RAISE_MEMORYERROR_METHODDEF
343384
_TESTCAPI_SET_EXC_INFO_METHODDEF

Python/errors.c

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,10 @@ _PyErr_SetString(PyThreadState *tstate, PyObject *exception,
292292
const char *string)
293293
{
294294
PyObject *value = PyUnicode_FromString(string);
295-
_PyErr_SetObject(tstate, exception, value);
296-
Py_XDECREF(value);
295+
if (value != NULL) {
296+
_PyErr_SetObject(tstate, exception, value);
297+
Py_DECREF(value);
298+
}
297299
}
298300

299301
void
@@ -891,7 +893,13 @@ PyErr_SetFromErrnoWithFilenameObjects(PyObject *exc, PyObject *filenameObject, P
891893
PyObject *
892894
PyErr_SetFromErrnoWithFilename(PyObject *exc, const char *filename)
893895
{
894-
PyObject *name = filename ? PyUnicode_DecodeFSDefault(filename) : NULL;
896+
PyObject *name = NULL;
897+
if (filename) {
898+
name = PyUnicode_DecodeFSDefault(filename);
899+
if (name == NULL) {
900+
return NULL;
901+
}
902+
}
895903
PyObject *result = PyErr_SetFromErrnoWithFilenameObjects(exc, name, NULL);
896904
Py_XDECREF(name);
897905
return result;
@@ -988,7 +996,13 @@ PyObject *PyErr_SetExcFromWindowsErrWithFilename(
988996
int ierr,
989997
const char *filename)
990998
{
991-
PyObject *name = filename ? PyUnicode_DecodeFSDefault(filename) : NULL;
999+
PyObject *name = NULL;
1000+
if (filename) {
1001+
name = PyUnicode_DecodeFSDefault(filename);
1002+
if (name == NULL) {
1003+
return NULL;
1004+
}
1005+
}
9921006
PyObject *ret = PyErr_SetExcFromWindowsErrWithFilenameObjects(exc,
9931007
ierr,
9941008
name,
@@ -1012,7 +1026,13 @@ PyObject *PyErr_SetFromWindowsErrWithFilename(
10121026
int ierr,
10131027
const char *filename)
10141028
{
1015-
PyObject *name = filename ? PyUnicode_DecodeFSDefault(filename) : NULL;
1029+
PyObject *name = NULL;
1030+
if (filename) {
1031+
name = PyUnicode_DecodeFSDefault(filename);
1032+
if (name == NULL) {
1033+
return NULL;
1034+
}
1035+
}
10161036
PyObject *result = PyErr_SetExcFromWindowsErrWithFilenameObjects(
10171037
PyExc_OSError,
10181038
ierr, name, NULL);
@@ -1137,9 +1157,10 @@ _PyErr_FormatV(PyThreadState *tstate, PyObject *exception,
11371157
_PyErr_Clear(tstate);
11381158

11391159
string = PyUnicode_FromFormatV(format, vargs);
1140-
1141-
_PyErr_SetObject(tstate, exception, string);
1142-
Py_XDECREF(string);
1160+
if (string != NULL) {
1161+
_PyErr_SetObject(tstate, exception, string);
1162+
Py_DECREF(string);
1163+
}
11431164
return NULL;
11441165
}
11451166

0 commit comments

Comments
 (0)