Description
_elementtree_Element_get_impl() evaluates Py_NewRef(self->extra->attrib) without protecting the checks, field load, and reference increment as one operation. Concurrent Element.clear() can detach and free ElementObjectExtra and its attribute dictionary, while Element.set() can publish a replacement. A reader can therefore load through a freed extra or increment a reclaimed dictionary pointer.
Observed Behavior
Ten threads called Element.get() while another repeatedly called clear() followed by set() on the same childless Element. The free-threaded ASan build aborted in _PyObject_ResurrectStart with an invalid reference-count resurrection assertion. The same executable completed the GIL-enabled control normally.
Affected Version
CPython 3.14.7 at commit 823f0323ee6ec1402088b73bce1a38473cac36dc, built with --disable-gil --with-address-sanitizer.
Reproduction
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 PYTHON_GIL=0 python3.14 poc/reproduce.py --seconds 10
Setting PYTHON_GIL=1 provides the GIL-enabled control.
PoC Source Code
poc/reproduce.py
#!/usr/bin/env python3
"""Race Element.get() with clear/set on one shared Element."""
import argparse
import threading
import time
import xml.etree.ElementTree as ET
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seconds", type=float, default=10.0)
parser.add_argument("--readers", type=int, default=10)
args = parser.parse_args()
root = ET.Element("root", key="value")
stop = threading.Event()
counts = [0] * (args.readers + 1)
def mutate() -> None:
generation = 0
while not stop.is_set():
root.clear()
root.set("key", str(generation))
generation += 1
counts[-1] += 1
def read(slot: int) -> None:
while not stop.is_set():
root.get("key")
counts[slot] += 1
threads = [threading.Thread(target=mutate)]
threads.extend(
threading.Thread(target=read, args=(i,)) for i in range(args.readers)
)
for thread in threads:
thread.start()
time.sleep(args.seconds)
stop.set()
for thread in threads:
thread.join()
print(
{"reads": sum(counts[:-1]), "mutations": counts[-1]}
)
if __name__ == "__main__":
main()
Description
_elementtree_Element_get_impl()evaluatesPy_NewRef(self->extra->attrib)without protecting the checks, field load, and reference increment as one operation. ConcurrentElement.clear()can detach and freeElementObjectExtraand its attribute dictionary, whileElement.set()can publish a replacement. A reader can therefore load through a freedextraor increment a reclaimed dictionary pointer.Observed Behavior
Ten threads called
Element.get()while another repeatedly calledclear()followed byset()on the same childless Element. The free-threaded ASan build aborted in_PyObject_ResurrectStartwith an invalid reference-count resurrection assertion. The same executable completed the GIL-enabled control normally.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, built with--disable-gil --with-address-sanitizer.Reproduction
Setting
PYTHON_GIL=1provides the GIL-enabled control.PoC Source Code
poc/reproduce.py