[3.13] gh-143377: fix crashes in _interpreters.capture_exception (GH-143418) (#143653)

(cherry picked from commit ce6bae92da)
This commit is contained in:
Bénédikt Tran 2026-01-10 14:59:14 +01:00 committed by GitHub
parent 523d866205
commit 733a5cf51c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 62 additions and 5 deletions

View file

@ -3,6 +3,7 @@
import sys
from textwrap import dedent, indent
import threading
import traceback
import types
import unittest
@ -1667,6 +1668,55 @@ def test_set___main___attrs(self):
self.assertEqual(rc, 0)
class CaptureExceptionTests(unittest.TestCase):
# Prevent crashes with incompatible TracebackException.format().
# Regression test for https://github.com/python/cpython/issues/143377.
def capture_with_formatter(self, exc, formatter):
with support.swap_attr(traceback.TracebackException, "format", formatter):
return _interpreters.capture_exception(exc)
def test_capture_exception(self):
captured = _interpreters.capture_exception(ValueError("hello"))
self.assertEqual(captured.type.__name__, "ValueError")
self.assertEqual(captured.type.__qualname__, "ValueError")
self.assertEqual(captured.type.__module__, "builtins")
self.assertEqual(captured.msg, "hello")
self.assertEqual(captured.formatted, "ValueError: hello")
def test_capture_exception_custom_format(self):
exc = ValueError("good bye!")
formatter = lambda self: ["hello\n", "world\n"]
captured = self.capture_with_formatter(exc, formatter)
self.assertEqual(captured.msg, "good bye!")
self.assertEqual(captured.formatted, "ValueError: good bye!")
self.assertEqual(captured.errdisplay, "hello\nworld")
@support.subTests("exc_lines", ([], ["x-no-nl"], ["x-no-nl", "y-no-nl"]))
def test_capture_exception_invalid_format(self, exc_lines):
formatter = lambda self: exc_lines
captured = self.capture_with_formatter(ValueError(), formatter)
self.assertEqual(captured.msg, "")
self.assertEqual(captured.formatted, "ValueError: ")
self.assertEqual(captured.errdisplay, "".join(exc_lines))
@unittest.skipUnless(
support.Py_DEBUG,
"printing subinterpreter unraisable exceptions requires DEBUG build",
)
def test_capture_exception_unraisable_exception(self):
formatter = lambda self: 1
with support.catch_unraisable_exception() as cm:
captured = self.capture_with_formatter(ValueError(), formatter)
self.assertFalse(hasattr(captured, "errdisplay"))
self.assertEqual(cm.unraisable.exc_type, TypeError)
self.assertEqual(str(cm.unraisable.exc_value),
"can only join an iterable")
if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports.
unittest.main()

View file

@ -0,0 +1,2 @@
Fix a crash in :func:`!_interpreters.capture_exception` when
the exception is incorrectly formatted. Patch by Bénédikt Tran.

View file

@ -337,7 +337,7 @@ _PyCrossInterpreterData_ReleaseAndRawFree(_PyCrossInterpreterData *data)
/* convenience utilities */
/*************************/
static const char *
static char *
_copy_string_obj_raw(PyObject *strobj, Py_ssize_t *p_size)
{
Py_ssize_t size = -1;
@ -441,11 +441,16 @@ _format_TracebackException(PyObject *tbexc)
}
Py_ssize_t size = -1;
const char *formatted = _copy_string_obj_raw(formatted_obj, &size);
char *formatted = _copy_string_obj_raw(formatted_obj, &size);
Py_DECREF(formatted_obj);
// We remove trailing the newline added by TracebackException.format().
assert(formatted[size-1] == '\n');
((char *)formatted)[size-1] = '\0';
if (formatted == NULL || size == 0) {
return formatted;
}
assert(formatted[size] == '\0');
// Remove a trailing newline if needed.
if (formatted[size-1] == '\n') {
formatted[size-1] = '\0';
}
return formatted;
}