From b41aec8f21c55a344c5e30c77baafdd65c890b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:37:54 +0100 Subject: [PATCH 1/3] [3.13] gh-143377: fix crashes in `_interpreters.capture_exception` (GH-143418) (cherry picked from commit ce6bae92da671e31013b00901591ce2b595b61ce) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_interpreters/test_api.py | 49 +++++++++++++++++++ ...-01-04-16-56-17.gh-issue-143377.YJqMCa.rst | 2 + Python/crossinterp.c | 15 ++++-- 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2026-01-04-16-56-17.gh-issue-143377.YJqMCa.rst diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 61beb317c37975..1d97e3706708d9 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -1667,6 +1667,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.assertNotHasAttr(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() diff --git a/Misc/NEWS.d/next/Core and Builtins/2026-01-04-16-56-17.gh-issue-143377.YJqMCa.rst b/Misc/NEWS.d/next/Core and Builtins/2026-01-04-16-56-17.gh-issue-143377.YJqMCa.rst new file mode 100644 index 00000000000000..fc58554781f0d3 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2026-01-04-16-56-17.gh-issue-143377.YJqMCa.rst @@ -0,0 +1,2 @@ +Fix a crash in :func:`!_interpreters.capture_exception` when +the exception is incorrectly formatted. Patch by Bénédikt Tran. diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 2f6324d300dee0..caa1cb6702b8a6 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -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; } From 22214bb0df398f2f47d32c531e79398b899fd7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:15:06 +0100 Subject: [PATCH 2/3] add missing import --- Lib/test/test_interpreters/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 1d97e3706708d9..1bf8e1b9ae48fc 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -3,6 +3,7 @@ import sys from textwrap import dedent, indent import threading +import traceback import types import unittest From e48b0c6cb98a3419281d93b9e6d71c1a2f9d66b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:36:41 +0100 Subject: [PATCH 3/3] Use legacy alternative to `self.assertNotHasAttr` --- Lib/test/test_interpreters/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 1bf8e1b9ae48fc..b9c0c45e0160bf 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -1711,7 +1711,7 @@ 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.assertNotHasAttr(captured, "errdisplay") + self.assertFalse(hasattr(captured, "errdisplay")) self.assertEqual(cm.unraisable.exc_type, TypeError) self.assertEqual(str(cm.unraisable.exc_value), "can only join an iterable")