https://github.com/python/cpython/commit/ea70d164067ef1c6a3f75d3268e93eb30c652abd
commit: ea70d164067ef1c6a3f75d3268e93eb30c652abd
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: picnixz <[email protected]>
date: 2026-01-10T12:05:42Z
summary:

[3.14] gh-143377: fix crashes in `_interpreters.capture_exception` (GH-143418) 
(#143652)

gh-143377: fix crashes in `_interpreters.capture_exception` (GH-143418)
(cherry picked from commit ce6bae92da671e31013b00901591ce2b595b61ce)

Co-authored-by: Bénédikt Tran <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-01-04-16-56-17.gh-issue-143377.YJqMCa.rst
M Lib/test/test_crossinterp.py
M Python/crossinterp.c

diff --git a/Lib/test/test_crossinterp.py b/Lib/test/test_crossinterp.py
index c54635eaeab3f9..06cbe268348d1c 100644
--- a/Lib/test/test_crossinterp.py
+++ b/Lib/test/test_crossinterp.py
@@ -3,10 +3,12 @@
 import importlib.util
 import itertools
 import sys
+import traceback
 import types
 import unittest
 import warnings
 
+from test import support
 from test.support import import_helper
 
 _testinternalcapi = import_helper.import_module('_testinternalcapi')
@@ -1493,5 +1495,54 @@ def test_builtin_objects(self):
         ])
 
 
+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__':
     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 542253c14de9b8..80772fcedb46e6 100644
--- a/Python/crossinterp.c
+++ b/Python/crossinterp.c
@@ -1038,7 +1038,7 @@ _PyXIData_ReleaseAndRawFree(_PyXIData_t *xidata)
 /* convenience utilities */
 /*************************/
 
-static const char *
+static char *
 _copy_string_obj_raw(PyObject *strobj, Py_ssize_t *p_size)
 {
     Py_ssize_t size = -1;
@@ -1139,11 +1139,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]

Reply via email to