https://github.com/python/cpython/commit/733a5cf51c7c2947aa3bc132011afef04b271dfa
commit: 733a5cf51c7c2947aa3bc132011afef04b271dfa
branch: 3.13
author: Bénédikt Tran <[email protected]>
committer: picnixz <[email protected]>
date: 2026-01-10T13:59:14Z
summary:
[3.13] gh-143377: fix crashes in `_interpreters.capture_exception` (GH-143418)
(#143653)
(cherry picked from commit ce6bae92da671e31013b00901591ce2b595b61ce)
files:
A Misc/NEWS.d/next/Core and
Builtins/2026-01-04-16-56-17.gh-issue-143377.YJqMCa.rst
M Lib/test/test_interpreters/test_api.py
M Python/crossinterp.c
diff --git a/Lib/test/test_interpreters/test_api.py
b/Lib/test/test_interpreters/test_api.py
index 61beb317c37975..b9c0c45e0160bf 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
@@ -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()
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;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]