Skip to content

Element.append() can use freed child storage during concurrent clear() #157088

Description

@Nievesjyl

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()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions