Skip to content

Commit ddc27f9

Browse files
authored
gh-128974: Fix UnicodeError.__str__ when custom attributes have side-effects (#128975)
Fix some crashes when (custom) attributes of `UnicodeError` objects implement `object.__str__` with side-effects.
1 parent 75f38af commit ddc27f9

File tree

3 files changed

+84
-15
lines changed

3 files changed

+84
-15
lines changed

Lib/test/test_exceptions.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,43 @@ def test_unicode_error_str_does_not_crash(self):
13601360
exc = UnicodeDecodeError('utf-8', encoded, start, end, '')
13611361
self.assertIsInstance(str(exc), str)
13621362

1363+
def test_unicode_error_evil_str_set_none_object(self):
1364+
def side_effect(exc):
1365+
exc.object = None
1366+
self.do_test_unicode_error_mutate(side_effect)
1367+
1368+
def test_unicode_error_evil_str_del_self_object(self):
1369+
def side_effect(exc):
1370+
del exc.object
1371+
self.do_test_unicode_error_mutate(side_effect)
1372+
1373+
def do_test_unicode_error_mutate(self, side_effect):
1374+
# Test that str(UnicodeError(...)) does not crash when
1375+
# side-effects mutate the underlying 'object' attribute.
1376+
# See https://github.com/python/cpython/issues/128974.
1377+
1378+
class Evil(str):
1379+
def __str__(self):
1380+
side_effect(exc)
1381+
return self
1382+
1383+
for reason, encoding in [
1384+
("reason", Evil("utf-8")),
1385+
(Evil("reason"), "utf-8"),
1386+
(Evil("reason"), Evil("utf-8")),
1387+
]:
1388+
with self.subTest(encoding=encoding, reason=reason):
1389+
with self.subTest(UnicodeEncodeError):
1390+
exc = UnicodeEncodeError(encoding, "x", 0, 1, reason)
1391+
self.assertRaises(TypeError, str, exc)
1392+
with self.subTest(UnicodeDecodeError):
1393+
exc = UnicodeDecodeError(encoding, b"x", 0, 1, reason)
1394+
self.assertRaises(TypeError, str, exc)
1395+
1396+
with self.subTest(UnicodeTranslateError):
1397+
exc = UnicodeTranslateError("x", 0, 1, Evil("reason"))
1398+
self.assertRaises(TypeError, str, exc)
1399+
13631400
@no_tracing
13641401
def test_badisinstance(self):
13651402
# Bug #2542: if issubclass(e, MyException) raises an exception,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash in :meth:`UnicodeError.__str__ <object.__str__>` when custom
2+
attributes implement :meth:`~object.__str__` with side-effects.
3+
Patch by Bénédikt Tran.

Objects/exceptions.c

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2784,6 +2784,8 @@ SyntaxError_str(PyObject *op)
27842784
if (!filename && !have_lineno)
27852785
return PyObject_Str(self->msg ? self->msg : Py_None);
27862786

2787+
// Even if 'filename' can be an instance of a subclass of 'str',
2788+
// we only render its "true" content and do not use str(filename).
27872789
if (filename && have_lineno)
27882790
result = PyUnicode_FromFormat("%S (%U, line %ld)",
27892791
self->msg ? self->msg : Py_None,
@@ -2903,29 +2905,47 @@ SimpleExtendsException(PyExc_ValueError, UnicodeError,
29032905

29042906
/*
29052907
* Check the validity of 'attr' as a unicode or bytes object depending
2906-
* on 'as_bytes' and return a new reference on it if it is the case.
2908+
* on 'as_bytes'.
29072909
*
29082910
* The 'name' is the attribute name and is only used for error reporting.
29092911
*
2910-
* On success, this returns a strong reference on 'attr'.
2911-
* On failure, this sets a TypeError and returns NULL.
2912+
* On success, this returns 0.
2913+
* On failure, this sets a TypeError and returns -1.
29122914
*/
2913-
static PyObject *
2914-
as_unicode_error_attribute(PyObject *attr, const char *name, int as_bytes)
2915+
static int
2916+
check_unicode_error_attribute(PyObject *attr, const char *name, int as_bytes)
29152917
{
29162918
assert(as_bytes == 0 || as_bytes == 1);
29172919
if (attr == NULL) {
2918-
PyErr_Format(PyExc_TypeError, "%s attribute not set", name);
2919-
return NULL;
2920+
PyErr_Format(PyExc_TypeError,
2921+
"UnicodeError '%s' attribute is not set",
2922+
name);
2923+
return -1;
29202924
}
29212925
if (!(as_bytes ? PyBytes_Check(attr) : PyUnicode_Check(attr))) {
29222926
PyErr_Format(PyExc_TypeError,
2923-
"%s attribute must be %s",
2924-
name,
2925-
as_bytes ? "bytes" : "unicode");
2926-
return NULL;
2927+
"UnicodeError '%s' attribute must be a %s",
2928+
name, as_bytes ? "bytes" : "string");
2929+
return -1;
29272930
}
2928-
return Py_NewRef(attr);
2931+
return 0;
2932+
}
2933+
2934+
2935+
/*
2936+
* Check the validity of 'attr' as a unicode or bytes object depending
2937+
* on 'as_bytes' and return a new reference on it if it is the case.
2938+
*
2939+
* The 'name' is the attribute name and is only used for error reporting.
2940+
*
2941+
* On success, this returns a strong reference on 'attr'.
2942+
* On failure, this sets a TypeError and returns NULL.
2943+
*/
2944+
static PyObject *
2945+
as_unicode_error_attribute(PyObject *attr, const char *name, int as_bytes)
2946+
{
2947+
int rc = check_unicode_error_attribute(attr, name, as_bytes);
2948+
return rc < 0 ? NULL : Py_NewRef(attr);
29292949
}
29302950

29312951

@@ -3591,7 +3611,10 @@ UnicodeEncodeError_str(PyObject *self)
35913611
if (encoding_str == NULL) {
35923612
goto done;
35933613
}
3594-
3614+
// calls to PyObject_Str(...) above might mutate 'exc->object'
3615+
if (check_unicode_error_attribute(exc->object, "object", false) < 0) {
3616+
goto done;
3617+
}
35953618
Py_ssize_t len = PyUnicode_GET_LENGTH(exc->object);
35963619
Py_ssize_t start = exc->start, end = exc->end;
35973620

@@ -3711,7 +3734,10 @@ UnicodeDecodeError_str(PyObject *self)
37113734
if (encoding_str == NULL) {
37123735
goto done;
37133736
}
3714-
3737+
// calls to PyObject_Str(...) above might mutate 'exc->object'
3738+
if (check_unicode_error_attribute(exc->object, "object", true) < 0) {
3739+
goto done;
3740+
}
37153741
Py_ssize_t len = PyBytes_GET_SIZE(exc->object);
37163742
Py_ssize_t start = exc->start, end = exc->end;
37173743

@@ -3807,7 +3833,10 @@ UnicodeTranslateError_str(PyObject *self)
38073833
if (reason_str == NULL) {
38083834
goto done;
38093835
}
3810-
3836+
// call to PyObject_Str(...) above might mutate 'exc->object'
3837+
if (check_unicode_error_attribute(exc->object, "object", false) < 0) {
3838+
goto done;
3839+
}
38113840
Py_ssize_t len = PyUnicode_GET_LENGTH(exc->object);
38123841
Py_ssize_t start = exc->start, end = exc->end;
38133842

0 commit comments

Comments
 (0)