Skip to content

Concurrent XMLParser.feed() calls can corrupt Expat state #157095

Description

@Nievesjyl

Description

XMLParser.feed(), flush(), close(), and _parse_whole() access one Expat parser and its handler, target, name-cache, and error state without a per-instance critical section. Concurrent calls can alter parser state while another thread is inside XML_Parse() or reading the resulting error fields. The observed path passes a null or stale Expat error string into strlen() through PyUnicode_FromFormat().

Observed Behavior

Eight threads fed complete self-closing child fragments to one parser after an opening root tag. Every possible serialized fragment order is valid XML. The free-threaded ASan build segfaulted in strlen() from expat_set_error(), reached through expat_parse() and XMLParser.feed(). The same executable parsed and closed successfully with the GIL enabled.

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 8 --feeders 8

Setting PYTHON_GIL=1 provides the GIL-enabled control.

PoC Source Code

poc/reproduce.py

#!/usr/bin/env python3
"""Concurrently feed serialization-independent fragments to one XMLParser."""

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=8.0)
    parser.add_argument("--feeders", type=int, default=8)
    args = parser.parse_args()

    xml_parser = ET.XMLParser()
    xml_parser.feed("<root>")
    stop = threading.Event()
    errors: list[str] = []

    def feed(slot: int) -> None:
        sequence = 0
        try:
            while not stop.is_set():
                xml_parser.feed(f"<tag{slot}_{sequence}/>")
                sequence += 1
        except BaseException as exc:
            errors.append(repr(exc))
            stop.set()

    threads = [
        threading.Thread(target=feed, args=(slot,)) for slot in range(args.feeders)
    ]
    for thread in threads:
        thread.start()
    time.sleep(args.seconds)
    stop.set()
    for thread in threads:
        thread.join()
    if not errors:
        xml_parser.feed("</root>")
        xml_parser.close()
    print({"errors": errors})


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