Skip to content

Commit c3987b1

Browse files
rwgkhenryiiiSkylion007pre-commit-ci[bot]
committed
fix: unicode surrogate character in Python exception message. (#4297)
* Fix & test for issue #4288 (unicode surrogate character in Python exception message). * DRY `message_unavailable_exc` * fix: add a constexpr Co-authored-by: Aaron Gokaslan <[email protected]> * style: pre-commit fixes Co-authored-by: Henry Schreiner <[email protected]> Co-authored-by: Aaron Gokaslan <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6d3a0fc commit c3987b1

File tree

3 files changed

+34
-7
lines changed

3 files changed

+34
-7
lines changed

include/pybind11/pytypes.h

+20-2
Original file line numberDiff line numberDiff line change
@@ -501,11 +501,29 @@ struct error_fetch_and_normalize {
501501
std::string message_error_string;
502502
if (m_value) {
503503
auto value_str = reinterpret_steal<object>(PyObject_Str(m_value.ptr()));
504+
constexpr const char *message_unavailable_exc
505+
= "<MESSAGE UNAVAILABLE DUE TO ANOTHER EXCEPTION>";
504506
if (!value_str) {
505507
message_error_string = detail::error_string();
506-
result = "<MESSAGE UNAVAILABLE DUE TO ANOTHER EXCEPTION>";
508+
result = message_unavailable_exc;
507509
} else {
508-
result = value_str.cast<std::string>();
510+
// Not using `value_str.cast<std::string>()`, to not potentially throw a secondary
511+
// error_already_set that will then result in process termination (#4288).
512+
auto value_bytes = reinterpret_steal<object>(
513+
PyUnicode_AsEncodedString(value_str.ptr(), "utf-8", "backslashreplace"));
514+
if (!value_bytes) {
515+
message_error_string = detail::error_string();
516+
result = message_unavailable_exc;
517+
} else {
518+
char *buffer = nullptr;
519+
Py_ssize_t length = 0;
520+
if (PyBytes_AsStringAndSize(value_bytes.ptr(), &buffer, &length) == -1) {
521+
message_error_string = detail::error_string();
522+
result = message_unavailable_exc;
523+
} else {
524+
result = std::string(buffer, static_cast<std::size_t>(length));
525+
}
526+
}
509527
}
510528
} else {
511529
result = "<MESSAGE UNAVAILABLE>";

tests/test_exceptions.cpp

-5
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,6 @@ struct PythonAlreadySetInDestructor {
105105
py::str s;
106106
};
107107

108-
std::string error_already_set_what(const py::object &exc_type, const py::object &exc_value) {
109-
PyErr_SetObject(exc_type.ptr(), exc_value.ptr());
110-
return py::error_already_set().what();
111-
}
112-
113108
TEST_SUBMODULE(exceptions, m) {
114109
m.def("throw_std_exception",
115110
[]() { throw std::runtime_error("This exception was intentionally thrown."); });

tests/test_exceptions.py

+14
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,20 @@ def test_local_translator(msg):
275275
assert msg(excinfo.value) == "this mod"
276276

277277

278+
def test_error_already_set_message_with_unicode_surrogate(): # Issue #4288
279+
assert m.error_already_set_what(RuntimeError, "\ud927") == (
280+
"RuntimeError: \\ud927",
281+
False,
282+
)
283+
284+
285+
def test_error_already_set_message_with_malformed_utf8():
286+
assert m.error_already_set_what(RuntimeError, b"\x80") == (
287+
"RuntimeError: b'\\x80'",
288+
False,
289+
)
290+
291+
278292
class FlakyException(Exception):
279293
def __init__(self, failure_point):
280294
if failure_point == "failure_point_init":

0 commit comments

Comments
 (0)