Skip to content

Commit 97d67e9

Browse files
[3.12] gh-107915: Handle errors in C API functions PyErr_Set*() and PyErr_Format() (GH-107918) (#108134)
* 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. (cherry picked from commit 633ea21) Co-authored-by: Serhiy Storchaka <[email protected]> * Define PY_SSIZE_T_CLEAN. --------- Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 7f5a741 commit 97d67e9

File tree

5 files changed

+219
-9
lines changed

5 files changed

+219
-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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
#define PY_SSIZE_T_CLEAN
12
#include "parts.h"
23
#include "clinic/exceptions.c.h"
34

5+
#define NULLABLE(x) do { if (x == Py_None) x = NULL; } while (0);
6+
47
/*[clinic input]
58
module _testcapi
69
[clinic start generated code]*/
@@ -129,6 +132,43 @@ _testcapi_exc_set_object_fetch_impl(PyObject *module, PyObject *exc,
129132
return value;
130133
}
131134

135+
/*[clinic input]
136+
_testcapi.err_setstring
137+
exc: object
138+
value: str(zeroes=True, accept={robuffer, str, NoneType})
139+
/
140+
[clinic start generated code]*/
141+
142+
static PyObject *
143+
_testcapi_err_setstring_impl(PyObject *module, PyObject *exc,
144+
const char *value, Py_ssize_t value_length)
145+
/*[clinic end generated code: output=fba8705e5703dd3f input=e8a95fad66d9004b]*/
146+
{
147+
NULLABLE(exc);
148+
PyErr_SetString(exc, value);
149+
return NULL;
150+
}
151+
152+
/*[clinic input]
153+
_testcapi.err_setfromerrnowithfilename
154+
error: int
155+
exc: object
156+
value: str(zeroes=True, accept={robuffer, str, NoneType})
157+
/
158+
[clinic start generated code]*/
159+
160+
static PyObject *
161+
_testcapi_err_setfromerrnowithfilename_impl(PyObject *module, int error,
162+
PyObject *exc, const char *value,
163+
Py_ssize_t value_length)
164+
/*[clinic end generated code: output=d02df5749a01850e input=ff7c384234bf097f]*/
165+
{
166+
NULLABLE(exc);
167+
errno = error;
168+
PyErr_SetFromErrnoWithFilename(exc, value);
169+
return NULL;
170+
}
171+
132172
/*[clinic input]
133173
_testcapi.raise_exception
134174
exception as exc: object
@@ -338,6 +378,8 @@ static PyMethodDef test_methods[] = {
338378
_TESTCAPI_MAKE_EXCEPTION_WITH_DOC_METHODDEF
339379
_TESTCAPI_EXC_SET_OBJECT_METHODDEF
340380
_TESTCAPI_EXC_SET_OBJECT_FETCH_METHODDEF
381+
_TESTCAPI_ERR_SETSTRING_METHODDEF
382+
_TESTCAPI_ERR_SETFROMERRNOWITHFILENAME_METHODDEF
341383
_TESTCAPI_RAISE_EXCEPTION_METHODDEF
342384
_TESTCAPI_RAISE_MEMORYERROR_METHODDEF
343385
_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
@@ -915,7 +917,13 @@ PyErr_SetFromErrnoWithFilenameObjects(PyObject *exc, PyObject *filenameObject, P
915917
PyObject *
916918
PyErr_SetFromErrnoWithFilename(PyObject *exc, const char *filename)
917919
{
918-
PyObject *name = filename ? PyUnicode_DecodeFSDefault(filename) : NULL;
920+
PyObject *name = NULL;
921+
if (filename) {
922+
name = PyUnicode_DecodeFSDefault(filename);
923+
if (name == NULL) {
924+
return NULL;
925+
}
926+
}
919927
PyObject *result = PyErr_SetFromErrnoWithFilenameObjects(exc, name, NULL);
920928
Py_XDECREF(name);
921929
return result;
@@ -1012,7 +1020,13 @@ PyObject *PyErr_SetExcFromWindowsErrWithFilename(
10121020
int ierr,
10131021
const char *filename)
10141022
{
1015-
PyObject *name = filename ? PyUnicode_DecodeFSDefault(filename) : NULL;
1023+
PyObject *name = NULL;
1024+
if (filename) {
1025+
name = PyUnicode_DecodeFSDefault(filename);
1026+
if (name == NULL) {
1027+
return NULL;
1028+
}
1029+
}
10161030
PyObject *ret = PyErr_SetExcFromWindowsErrWithFilenameObjects(exc,
10171031
ierr,
10181032
name,
@@ -1036,7 +1050,13 @@ PyObject *PyErr_SetFromWindowsErrWithFilename(
10361050
int ierr,
10371051
const char *filename)
10381052
{
1039-
PyObject *name = filename ? PyUnicode_DecodeFSDefault(filename) : NULL;
1053+
PyObject *name = NULL;
1054+
if (filename) {
1055+
name = PyUnicode_DecodeFSDefault(filename);
1056+
if (name == NULL) {
1057+
return NULL;
1058+
}
1059+
}
10401060
PyObject *result = PyErr_SetExcFromWindowsErrWithFilenameObjects(
10411061
PyExc_OSError,
10421062
ierr, name, NULL);
@@ -1161,9 +1181,10 @@ _PyErr_FormatV(PyThreadState *tstate, PyObject *exception,
11611181
_PyErr_Clear(tstate);
11621182

11631183
string = PyUnicode_FromFormatV(format, vargs);
1164-
1165-
_PyErr_SetObject(tstate, exception, string);
1166-
Py_XDECREF(string);
1184+
if (string != NULL) {
1185+
_PyErr_SetObject(tstate, exception, string);
1186+
Py_DECREF(string);
1187+
}
11671188
return NULL;
11681189
}
11691190

0 commit comments

Comments
 (0)