Description
An _elementtree.Element stores its child pointer, length, and capacity in one ElementObjectExtra representation. Element.append() can resize the child array, then separately compute the destination, store the child, and increment the length without a per-element critical section. Concurrent Element.clear() detaches and frees ElementObjectExtra and its heap child array, allowing append to dereference or write through released representation state.
Observed Behavior
One thread appended children while another cleared the same Element. The free-threaded ASan build produced a near-null SEGV in element_add_subelement() at the child destination access. The GIL-enabled ASan control completed 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
Because the schedule is probabilistic, repeat a clean free-threaded run. Setting PYTHON_GIL=1 provides the GIL-enabled control.
PoC Source Code
poc/reproduce.py
#!/usr/bin/env python3
"""Race Element.append() against Element.clear() on one shared object."""
import argparse
import threading
import time
import xml.etree.ElementTree as element_tree
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seconds", type=float, default=10.0)
parser.add_argument("--initial-children", type=int, default=16)
args = parser.parse_args()
root = element_tree.Element("root")
root.extend(
element_tree.Element("initial") for _ in range(args.initial_children)
)
stop = threading.Event()
counts = [0, 0]
def append_children() -> None:
generation = 0
while not stop.is_set():
root.append(element_tree.Element(str(generation)))
generation += 1
counts[0] += 1
def clear_children() -> None:
while not stop.is_set():
root.clear()
counts[1] += 1
workers = [
threading.Thread(target=append_children),
threading.Thread(target=clear_children),
]
for worker in workers:
worker.start()
time.sleep(args.seconds)
stop.set()
for worker in workers:
worker.join()
print({"append": counts[0], "clear": counts[1]})
if __name__ == "__main__":
main()
Description
An
_elementtree.Elementstores its child pointer, length, and capacity in oneElementObjectExtrarepresentation.Element.append()can resize the child array, then separately compute the destination, store the child, and increment the length without a per-element critical section. ConcurrentElement.clear()detaches and freesElementObjectExtraand its heap child array, allowing append to dereference or write through released representation state.Observed Behavior
One thread appended children while another cleared the same Element. The free-threaded ASan build produced a near-null SEGV in
element_add_subelement()at the child destination access. The GIL-enabled ASan control completed normally.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, built with--disable-gil --with-address-sanitizer.Reproduction
Because the schedule is probabilistic, repeat a clean free-threaded run. Setting
PYTHON_GIL=1provides the GIL-enabled control.PoC Source Code
poc/reproduce.py