The Exploit
An attacker can craft a specially malformed UTF-16 encoded XML document containing low surrogate code points positioned at specific byte offsets to trigger an out-of-bounds read in libexpat's character conversion routines, causing an infinite loop that crashes the parser or consumes unbounded CPU.
POST /parse HTTP/1.1
Host: vulnerable-xml-parser.local
Content-Type: application/xml
Content-Length: 2048
<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE d [
<!ENTITY e '
followed by a binary payload with low surrogates at byte offset 1021–1025 (the offset matters—the bug manifests only when the surrogate pair handling crosses specific alignment boundaries in the conversion buffer). The parser enters _toUtf16 with misclassified code units, attempting to consume a low surrogate as if it were a high surrogate, resulting in repeated reprocessing of the same buffer offset and eventual hang or crash.
When a vulnerable libexpat instance processes this document, the HTTP request will timeout or the parser process will consume 100% CPU indefinitely, never returning from the parse call. A monitoring agent observing the parser daemon will see thread state locked in the character conversion loop with no forward progress on the input buffer.
What the Patch Did
Before:
// xmlparse.c: _toUtf16 function (vulnerable path)
// Low surrogates treated identically to high surrogates
// Parser advances buffer pointer without validating surrogate pair ordering
// If low surrogate encountered at unexpected position, reprocessing loop occurs
After:
// The fix adds explicit surrogate classification:
// - HIGH_SURROGATE: 0xD800–0xDBFF → expect low surrogate to follow
// - LOW_SURROGATE: 0xDC00–0xDFFF → reject if not preceded by high surrogate
// - Enforce strict ordering: reject low surrogate unless valid pair in progress
// Return error status (XML_STATUS_ERROR) on malformed surrogate sequence
// Prevents infinite loop by breaking on first malformed code unit
The patch inserted surrogate-pair validation logic into the _toUtf16 conversion path (referenced in the test regression at test_misc_low_surrogate_mozilla_bug_2053153). The security control added is strict state-machine validation of UTF-16 surrogate pairs: the parser now classifies each code unit by its numeric range and enforces a rule that low surrogates (0xDC00–0xDFFF) can only appear after a high surrogate (0xD800–0xDBFF) in the byte stream, terminating parsing with an error rather than attempting recovery via buffer reprocessing.
Root Cause
CWE-125: Out-of-Bounds Read and CWE-835: Infinite Loop with Unreachable Exit Condition.
The vulnerability exists in the UTF-16 character decoding logic within libexpat's _toUtf16 function family. When parsing XML marked as UTF-16 encoded, the parser reads 2-byte code units and interprets them according to the UTF-16 standard. UTF-16 represents characters outside the Basic Multilingual Plane (U+10000 and above) using surrogate pairs: a high surrogate (0xD800–0xDBFF) followed by a low surrogate (0xDC00–0xDFFF).
The vulnerability occurs because the unpatched code treats low surrogates identically to high surrogates during the pair-building phase. If the parser encounters a low surrogate at a position where a high surrogate is expected, it does not immediately reject the malformed sequence. Instead, the buffer pointer reprocessing logic causes the same 2-byte chunk to be re-examined in the next loop iteration, creating a condition where the parser cannot advance past the offending code unit. Combined with insufficient loop termination logic, this produces an infinite loop.
The vulnerability is particularly acute when malformed surrogates land at specific byte offsets (1021–1025 in the PoC) because these positions align with internal buffer boundaries, maximizing the reprocessing window before any bounds check or buffer refill might trigger.
Why It Works
The load-bearing security fix is the surrogate type classification check: before attempting to pair a code unit with the previous code unit, the patched code explicitly tests if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) (low surrogate range) and rejects the sequence immediately rather than entering the pair-building logic.
Without this check, the parser's loop attempts to construct a surrogate pair from an invalid sequence, and the reprocessing mechanism—which is necessary for correct parsing of valid surrogate pairs that span buffer boundaries—has no condition to detect and terminate on malformed input. The supporting changes (strict high-surrogate expectation state tracking, error return on surrogate violations) are necessary for defense-in-depth because they ensure:
- The error propagates to the caller (preventing silent corruption).
- Subsequent bytes are not processed after a malformed surrogate is detected (preventing cascading reinterpretation).
- The finite-state machine for surrogate-pair assembly cannot enter inconsistent states (preventing related infinite-loop variants via other code paths).
If the engineer had added only the low-surrogate classification without the state machine reset and error propagation, a malicious input could still trigger the infinite loop via different malformed sequences.
Hardening Checklist
-
Validate all character encoding state machines against the respective standard: for UTF-16, verify that surrogate-pair assembly strictly rejects unpaired or out-of-order surrogates, and add unit tests for each invalid sequence (low-then-low, high-then-high, lone surrogates). Reference RFC 2781 or the Unicode Standard directly.
-
Add bounds and timeout instrumentation to all multibyte character conversion loops: use loop iteration counters or byte-offset progress assertions to detect when the read pointer does not advance, and fail fast with an error rather than spinning.
-
Implement fuzzing-informed regression tests for encoding edge cases: add test cases for all surrogate offsets and combinations (the test suite now includes
test_misc_low_surrogate_mozilla_bug_2053153at offsets 1021–1025; expand this to cover buffer-boundary alignments on 4KB, 8KB, and 64KB buffer sizes). -
Use AddressSanitizer and ThreadSanitizer in CI for XML parsing jobs: ASan will catch out-of-bounds reads, and TSan will detect infinite loops via thread-state stall detection.
-
Code-review all state machine transitions in character decoders: ensure that every path either advances the input pointer by a known, non-zero amount or returns an error; reject "try again on next iteration" patterns without an explicit termination condition.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-72522
- Expat Bug Tracker: https://github.com/libexpat/libexpat (version 2.8.3 release notes)
- Mozilla Bug 2053153: Improper handling of low surrogate code points in UTF-16 XML parsing