-
-
Notifications
You must be signed in to change notification settings - Fork 34.3k
Description
Crash report
What happened?
What Happened
ctypes.Structure instances store their field data in an inline buffer called b_value (16 bytes on x86-64), allocated at object creation time. The size of this buffer is recorded in b_size and never changes.
Python's type system allows __class__ assignment between types that share the same tp_basicsize. All ctypes.Structure subclasses have the same tp_basicsize regardless of their field sizes, so assigning __class__ from a small Structure to a large one is permitted without error.
When fields of the new larger type are subsequently written, PyCField_set computes the write destination as b_ptr + byte_offset using the new type's field offsets, but b_ptr still points to the original undersized b_value buffer. There is no bounds check against b_size. A field at offset 16 or greater writes past the end of b_value into adjacent object metadata.
On Python 3.13+ the 16 bytes immediately following b_value (object+96 and
object+104) are managed dict metadata — specifically the inline values pointer
and dict pointer used to store instance attributes. The out-of-bounds write
replaces these with attacker-controlled values.
When s.cycle = cycle subsequently attempts to store an instance attribute,
store_instance_attr_lock_held in dictobject.c reads the corrupted inline
values pointer and fails the internal consistency assertion:
dict == NULL || ((PyDictObject *)dict)->ma_values == values
PoC
import ctypes, gc
class Small(ctypes.Structure):
_fields_ = [('x', ctypes.c_uint8)] # 1 byte, uses 16-byte inline b_value
class Big(ctypes.Structure):
_fields_ = [(f'f{i}', ctypes.c_uint64) for i in range(4)] # 32 bytes
s = Small()
s.__class__ = Big # allowed — tp_basicsize matches, b_size not updated
s.f2 = 0xDEADBEEFDEADBEEF # memcpy to b_ptr+16 — 8 bytes past end of b_value
s.f3 = 0xCAFEBABECAFEBABE # overwrites managed dict pointer at object+104
# Force cyclic GC to traverse s and dereference the corrupted pointer
cycle = [s]
s.cycle = cycle
gc.collect(2)Crash Behaviour
Debug build (--with-pydebug): assertion failure in store_instance_attr_lock_held
at Objects/dictobject.c:6866 when s.cycle = cycle attempts to store an instance
attribute into the corrupted managed dict. Reproducible every run.
Release build: silent memory corruption — the OOB write succeeds and the corrupted
pointer persists in the object. The interpreter crashes with SIGSEGV during GC traversal
at shutdown, or non-deterministically depending on heap layout and GC scheduling.
Confirmed via raw memory inspection on 3.13.1: object+96 and object+104 overwritten
with attacker-controlled values.
I have attached the gdb trace below to highlight the oob write.
CPython versions tested on:
3.14
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.14.2 (tags/v3.14.2:df793163d58, Mar 15 2026, 18:02:18) [GCC 13.3.0]