What happened?
_format_TracebackException assumes the TracebackException.format result is a newline-terminated buffer and truncates it in place, so a crafted override that returns an empty sequence makes the code treat a one-byte allocation as if it held a trailing \n, writing past the buffer and turning the mistaken type expectation into a heap buffer overflow.
Proof of Concept:
import traceback import _interpreters orig_format = traceback.TracebackException.format def empty_format(self): return [] traceback.TracebackException.format = empty_format try: raise ValueError("boom") except Exception as exc: _interpreters.capture_exception(exc) finally: traceback.TracebackException.format = orig_format
Vulnerable Code Snippet:
Click to expand/* Buggy Re-entrant Path */ static PyObject * _interpreters_capture_exception_impl(PyObject *module, PyObject *exc_arg) { PyObject *exc = exc_arg; /* ... */ _PyXI_excinfo *info = _PyXI_NewExcInfo(exc); if (info == NULL) { goto finally; } /* ... */ PyObject *formatted = _PyXI_FormatExcInfo(info); if (formatted == NULL) { Py_CLEAR(captured); goto finally; } /* ... */ return captured; } _PyXI_excinfo * _PyXI_NewExcInfo(PyObject *exc) { _PyXI_excinfo *info = PyMem_RawCalloc(1, sizeof(_PyXI_excinfo)); /* ... */ if (PyExceptionInstance_Check(exc) || PyExceptionClass_Check(exc)) { failure = _PyXI_excinfo_InitFromException(info, exc); } /* ... */ return info; } static const char * _PyXI_excinfo_InitFromException(_PyXI_excinfo *info, PyObject *exc) { /* ... */ info->errdisplay = _format_TracebackException(tbexc); /* ... */ return NULL; } static const char * _format_TracebackException(PyObject *tbexc) { PyObject *lines = PyObject_CallMethod(tbexc, "format", NULL); /* Reentrant call site */ if (lines == NULL) { return NULL; } assert(_Py_EMPTY_STR != NULL); PyObject *formatted_obj = PyUnicode_Join(_Py_EMPTY_STR, lines); Py_DECREF(lines); if (formatted_obj == NULL) { return NULL; } Py_ssize_t size = -1; const char *formatted = _copy_string_obj_raw(formatted_obj, &size); /* crashing pointer derived */ Py_DECREF(formatted_obj); // Assume the formatted ends with \n while we can return an empty object // We remove trailing the newline added by TracebackException.format(). assert(formatted[size-1] == '\n'); ((char *)formatted)[size-1] = '\0'; /* Crash site */ return formatted; } /* Clobbering Path */ static const char * _copy_string_obj_raw(PyObject *strobj, Py_ssize_t *p_size) { Py_ssize_t size = -1; const char *str = PyUnicode_AsUTF8AndSize(strobj, &size); if (str == NULL) { return NULL; } /* ... */ char *copied = PyMem_RawMalloc(size+1); if (copied == NULL) { PyErr_NoMemory(); return NULL; } strcpy(copied, str); if (p_size != NULL) { *p_size = size; /* state mutate site */ } return copied; }
Sanitizer Output:
Click to expand=================================================================
==2203897==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50200002df2f at pc 0x627ab61ef34d bp 0x7fffb6056900 sp 0x7fffb60568f0
WRITE of size 1 at 0x50200002df2f thread T0
#0 0x627ab61ef34c in _format_TracebackException Python/crossinterp.c:1146
#1 0x627ab61ef34c in _PyXI_excinfo_InitFromException Python/crossinterp.c:1407
#2 0x627ab61f7dea in _PyXI_NewExcInfo Python/crossinterp.c:1649
#3 0x73d11eb12510 in _interpreters_capture_exception_impl Modules/_interpretersmodule.c:1522
#4 0x73d11eb12510 in _interpreters_capture_exception Modules/clinic/_interpretersmodule.c.h:1196
#5 0x627ab5e1e3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
#6 0x627ab5e1e3e7 in PyObject_Vectorcall Objects/call.c:327
#7 0x627ab5cd25a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
#8 0x627ab619cad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
#9 0x627ab619cad6 in _PyEval_Vector Python/ceval.c:2001
#10 0x627ab619cad6 in PyEval_EvalCode Python/ceval.c:884
#11 0x627ab62e216e in run_eval_code_obj Python/pythonrun.c:1365
#12 0x627ab62e216e in run_mod Python/pythonrun.c:1459
#13 0x627ab62e6e17 in pyrun_file Python/pythonrun.c:1293
#14 0x627ab62e6e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
#15 0x627ab62e793c in _PyRun_AnyFileObject Python/pythonrun.c:81
#16 0x627ab635ae3c in pymain_run_file_obj Modules/main.c:410
#17 0x627ab635ae3c in pymain_run_file Modules/main.c:429
#18 0x627ab635ae3c in pymain_run_python Modules/main.c:691
#19 0x627ab635c71e in Py_RunMain Modules/main.c:772
#20 0x627ab635c71e in pymain_main Modules/main.c:802
#21 0x627ab635c71e in Py_BytesMain Modules/main.c:826
#22 0x73d11f82a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#23 0x73d11f82a28a in __libc_start_main_impl ../csu/libc-start.c:360
#24 0x627ab5cf6634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)
0x50200002df2f is located 1 bytes before 1-byte region [0x50200002df30,0x50200002df31)
allocated by thread T0 here:
#0 0x73d11fcfd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x627ab61ee6f9 in _copy_string_obj_raw Python/crossinterp.c:1055
#2 0x627ab61ee6f9 in _format_TracebackException Python/crossinterp.c:1142
#3 0x627ab61ee6f9 in _PyXI_excinfo_InitFromException Python/crossinterp.c:1407
#4 0x627ab61f7dea in _PyXI_NewExcInfo Python/crossinterp.c:1649
#5 0x73d11eb12510 in _interpreters_capture_exception_impl Modules/_interpretersmodule.c:1522
#6 0x73d11eb12510 in _interpreters_capture_exception Modules/clinic/_interpretersmodule.c.h:1196
#7 0x627ab5e1e3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
#8 0x627ab5e1e3e7 in PyObject_Vectorcall Objects/call.c:327
#9 0x627ab5cd25a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
#10 0x627ab619cad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
#11 0x627ab619cad6 in _PyEval_Vector Python/ceval.c:2001
#12 0x627ab619cad6 in PyEval_EvalCode Python/ceval.c:884
#13 0x627ab62e216e in run_eval_code_obj Python/pythonrun.c:1365
#14 0x627ab62e216e in run_mod Python/pythonrun.c:1459
#15 0x627ab62e6e17 in pyrun_file Python/pythonrun.c:1293
#16 0x627ab62e6e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
#17 0x627ab62e793c in _PyRun_AnyFileObject Python/pythonrun.c:81
#18 0x627ab635ae3c in pymain_run_file_obj Modules/main.c:410
#19 0x627ab635ae3c in pymain_run_file Modules/main.c:429
#20 0x627ab635ae3c in pymain_run_python Modules/main.c:691
#21 0x627ab635c71e in Py_RunMain Modules/main.c:772
#22 0x627ab635c71e in pymain_main Modules/main.c:802
#23 0x627ab635c71e in Py_BytesMain Modules/main.c:826
#24 0x73d11f82a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#25 0x73d11f82a28a in __libc_start_main_impl ../csu/libc-start.c:360
#26 0x627ab5cf6634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)
SUMMARY: AddressSanitizer: heap-buffer-overflow Python/crossinterp.c:1146 in _format_TracebackException
Shadow bytes around the buggy address:
0x50200002dc80: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fa
0x50200002dd00: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x50200002dd80: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x50200002de00: fa fa fd fa fa fa fd fa fa fa 00 00 fa fa 00 03
0x50200002de80: fa fa 00 03 fa fa 00 01 fa fa 05 fa fa fa fd fa
=>0x50200002df00: fa fa fd fa fa[fa]01 fa fa fa fa fa fa fa fa fa
0x50200002df80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50200002e000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50200002e080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50200002e100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50200002e180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==2203897==ABORTING
CPython versions tested on:
Details| Python Version | Status | Exit Code |
|---|---|---|
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) |
Exception | 1 |
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] |
Exception | 1 |
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] |
Exception | 1 |
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 16:56:16) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0] |
ASAN | 1 |
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0]