Breakpoint 3, set_add_entry (so=so@entry=0x5110001dcd20, key=key@entry=<weakref.ReferenceType at remote 0x50b00015e640>, hash=0) at Objects/setobject.c:191
191 return 0;
1: freeslot = (setentry *) 0x5110001dcd60 // same freeslot location
2: x/2xg freeslot
0x5110001dcd60: 0x000050b00015e640 0x0000000000000000
(gdb) py-bt
Traceback (most recent call first):
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 23, in <module>
_asyncio._register_task(a[1])
(gdb) c
Continuing.
Breakpoint 3, set_add_entry (so=so@entry=0x5110001dcd20, key=key@entry=<weakref.ReferenceType at remote 0x50b00015e590>, hash=0) at Objects/setobject.c:191
191 return 0;
1: freeslot = (setentry *) 0x5110001dcd60 // same freeslot location
2: x/2xg freeslot
0x5110001dcd60: 0x000050b00015e590 0x0000000000000000
(gdb) py-bt // different python stack
Traceback (most recent call first):
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 23, in <module>
_asyncio._register_task(a[1])
(gdb) c
Continuing.
Breakpoint 3, set_add_entry (so=so@entry=0x5110001dcd20, key=key@entry=<weakref.ReferenceType at remote 0x50b00015e4e0>, hash=0) at Objects/setobject.c:191
191 return 0;
1: freeslot = (setentry *) 0x5110001dcd60 // same freeslot location
2: x/2xg freeslot
0x5110001dcd60: 0x000050b00015e4e0 0x0000000000000000
(gdb) py-bt // different python stack
Traceback (most recent call first):
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 23, in <module>
_asyncio._register_task(a[1])
(gdb) c
Continuing.
Breakpoint 3, set_add_entry (so=so@entry=0x5110001dcd20, key=key@entry=<weakref.ReferenceType at remote 0x50b00015e430>, hash=0) at Objects/setobject.c:191
191 return 0;
1: freeslot = (setentry *) 0x5110001dcd60 // same freeslot location
2: x/2xg freeslot
0x5110001dcd60: 0x000050b00015e430 0x0000000000000000
(gdb) py-bt // different python stack
Traceback (most recent call first):
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 11, in __eq__
_asyncio._register_task(self)
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 23, in <module>
_asyncio._register_task(a[1])
(gdb) c
Continuing.
Breakpoint 3, set_add_entry (so=so@entry=0x5110001dcd20, key=key@entry=<weakref.ReferenceType at remote 0x50b00015e380>, hash=0) at Objects/setobject.c:191
191 return 0;
1: freeslot = (setentry *) 0x5110001dcd60 // same freeslot location
2: x/2xg freeslot
0x5110001dcd60: 0x000050b00015e380 0x0000000000000000
(gdb) py-bt // different python stack
Traceback (most recent call first):
File "/home/ubuntu/Python-3.13.7/Lib/_weakrefset.py", line 88, in add
self.data.add(ref(item, self._remove))
<built-in method _register_task of module object at remote 0x5080000de5c0>
File "/home/ubuntu/Python-3.13.7/test.py", line 23, in <module>
_asyncio._register_task(a[1])
(gdb) c
Crash report
What happened?
heap overflow by corrupted PySetObject internal data(
used)Version
Python 3.13.7 (main, Sep 14 2025, 14:06:10) [GCC 13.3.0]
Root Cause
CPython PySetObject entry states: Python sets maintain three types of entries:
https://github.com/python/cpython/blob/3.13/Include/cpython/setobject.h#L7-L9
Hash collision exploitation: The hash value is used for both table indexing in set_add_entry and entry comparison. Setting hash to 0 causes deterministic traversal starting from index 0, creating intentional hash collisions between entries.
https://github.com/python/cpython/blob/3.13/Objects/setobject.c#L144
Dummy entry setup: Two dummy objects that return hash 0 are inserted into the set, then deleted, leaving behind dummy entries.
Recursive eq calls and exception handling: The eq method is the callback executed when set entries have identical hashes to resolve hash collisions. Adding a corruption object triggers recursive calls that eventually hit Python's maximum recursion depth, causing an error return and raising an exception.
https://github.com/python/cpython/blob/3.13/Objects/setobject.c#L165
Freeslot handling logic: When Python set encounters a dummy entry, it stores the location in freeslot. When it subsequently encounters a null entry, it moves to the dummy_or_freeslot location.
https://github.com/python/cpython/blob/3.13/Objects/setobject.c#L175-L177
https://github.com/python/cpython/blob/3.13/Objects/setobject.c#L152-L153
Entry overwrite at dummy_or_freeslot: If an entry's hash matches a dummy's hash, Python writes the key and hash to that dummy entry at the dummy_or_freeslot location.
https://github.com/python/cpython/blob/3.13/Objects/setobject.c#L185-L191
Exception handling and recursion unwind corruption: Even though the exception is caught and handled with try-except, as the recursion stack unwinds, each recursive call attempts to complete its add_entry operation. Since freeslot was determined deterministically, all recursive calls reference the same memory address. During the unwind process, each level overwrites the same freeslot location and incorrectly increments used += 1, as the function determines the insertion was successful.
recusively overwrite same freeslot location
entry counter(used : 70 ) bug
Memory corruption during deallocation: In set_dealloc, the deallocation routine iterates through the table by incrementing addresses sequentially. When it encounters non-dummy entries, it performs cleanup and decrements used--. However, since used is now greater than the actual (fill - dummy) count due to the previous corruption, the deallocation process accesses invalid memory addresses beyond the allocated table bounds, leading to memory corruption or segmentation faults.
https://github.com/python/cpython/blob/3.13/Objects/setobject.c#L494-L514
POC
ASAN
asan
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.13.7 (main, Sep 14 2025, 14:06:10) [GCC 13.3.0]