fix(#1856): parse XML from a bounded buffer so a large CLUT round-trips - #1995
Merged
Conversation
iccToXml could write a document iccFromXml then refused. A profile whose
CLUT serialises to a single text node larger than libxml2's
XML_MAX_TEXT_LENGTH (10 MB) was rejected on read, so the tools disagreed
about what a valid profile is.
That limit is enforced on the path that appends successive character-data
chunks to a text node as they arrive from the streaming reader. Handing
libxml2 one contiguous buffer never takes that path: measured on 2.9.14
with identical bytes and options, xmlReadFile refuses an 11 MB node while
xmlReadMemory accepts 11, 40 and 120 MB. So the round trip is restored
without XML_PARSE_HUGE, which would have disabled the nesting-depth and
name-length caps for every input.
icXmlReadFileBounded reads the document, bounds it at the existing
icXmlMaxTextFileBytes, and parses the buffer. Both xmlReadFile sites use
it - the profile entry point and the calculator <Import> path - with the
parser options unchanged: XML_PARSE_NONET, no XML_PARSE_NOENT, no
XML_PARSE_DTDLOAD. The buffer is allocated nothrow and a refusal is
reported, matching the other bounded text-buffer sites in this file and
keeping the reader's contract of returning NULL rather than throwing at a
size where allocation can plausibly fail. libxml2 copies the buffer into
the document, so it is released before returning; that was confirmed under
ASAN by clobbering and freeing the buffer before walking the tree.
This trades a per-node cap for a whole-document cap, which is the stricter
promise. CIccProfileXml::LoadXml applied no document bound at all before:
icXmlMaxTextFileBytes guarded only embedded file references, and libxml2's
caps bound a node rather than a file, so a large document made of small
nodes was never limited. A rejection now also reports the bound instead of
failing as a bare parse error.
Measured on a grid-33 CMYK->RGB device link built from tracked profiles,
whose XML carries a 21,705,957-byte text node:
- round trip restored, byte-identical ignoring creation dateTime and MD5
- peak RSS 50 -> 69 MB on that file, the raw buffer and tree now both
held; on 8 MiB of ~1M small nodes it falls from 59.0x to 34.9x input
- all 212 tracked .xml parse identically, 196 ok / 16 fail before and after
- 3000-level nesting, a 60000-byte element name and a billion-laughs
document are all still refused
iccdev.xml-parser-regressions covers the round trip and those three
hardening cases, so that a later attempt to solve a size problem by
reaching for XML_PARSE_HUGE turns red rather than quietly widening what
the parser accepts. It passes clean under -fsanitize=address,undefined
with detect_leaks=1.
colourbill-ctrl
requested review from
ChrisCoxArt,
dwtza,
maxderhak and
xsscx
as code owners
August 6, 2026 05:26
Member
@colourbill-ctrl Thank You, Please open Issue & PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1856. This is the XML half only — the JSON cap landed in #1981 and its
follow-ups moved to #1994. Item 1 of the issue ("confirm whether to enable
XML_PARSE_HUGE, CMake default or toggle?") is left open deliberately: the findingbelow is that there may be nothing to enable, but that call is @ChrisCoxArt's to make.
Detail and full measurements in
#1856 (comment).
Problem
iccToXmlcan write a documenticcFromXmlthen refuses. A profile whose CLUTserialises to a single text node above libxml2's
XML_MAX_TEXT_LENGTH(10 MB) isrejected on read, so the two tools disagree about what a valid profile is.
Reproducible in 2.5 s from tracked profiles:
The tracked corpus is already close to the limit —
Testing/Display/LaserProjector.xmlcarries a 7,473,697-byte node, 74.7% of the cap.
Why not
XML_PARSE_HUGEThe limit is enforced on the path that appends successive character-data chunks to a
text node as they arrive from the streaming reader. One contiguous buffer never takes
that path. Same bytes, same options, libxml2 2.9.14:
xmlReadFilexmlReadMemorySo the round trip is recoverable without the flag, which would additionally have
disabled the nesting-depth and name-length caps for every input. (The existing rationale
comment also credited it with holding off billion-laughs; that is actually done by not
setting
XML_PARSE_NOENT, and the comment is corrected here.)Change
icXmlReadFileBoundedreads the document, bounds it at the existingicXmlMaxTextFileBytes, and parses the buffer. BothxmlReadFilesites use it — theprofile entry point and the calculator
<Import>path — with parser options unchanged.CIccProfileXml::LoadXmlapplied no document bound at all before this:icXmlMaxTextFileBytesguarded only embedded file references, and libxml2's caps bound anode rather than a file, so a large document made of small nodes was never limited. The
change trades a per-node cap for a whole-document cap, which is the stricter promise, and
a rejection now reports the bound instead of failing as a bare parse error.
The buffer is allocated
nothrowand a refusal reported, matching the other boundedtext-buffer sites in the file and preserving the reader's contract of returning NULL
rather than throwing. libxml2 copies the buffer into the document, so it is released
before returning — confirmed under ASAN by clobbering and freeing the buffer before
walking the tree.
Evidence
.xml, parse outcomeThe RSS cost on the text-dense case is real and expected: the raw buffer and the tree are
held at once. That is the same asymmetry raised for
loadJsonFromin #1994, and itapplies here too. On the node-dense shape the buffered path is cheaper.
Test
iccdev.xml-parser-regressions(5 cases) covers the round trip and, deliberately, thethree hardening cases — so a later attempt to solve a size problem by reaching for
XML_PARSE_HUGEturns this red rather than quietly widening what the parser accepts.-fsanitize=address,undefinedwithdetect_leaks=1, verified against adeliberate-leak control so the clean result is meaningful.
spectral-tiff-preview, which fails on this machinefor a missing
imagecodecsmodule unrelated to this change.Two judgement calls, stated rather than buried
icXmlMaxTextFileBytesrather than adding anICC_XML_MAX_FILE_MBmirroringICC_JSON_MAX_FILE_MB. That introduces no new numberand no new configuration surface. If you would rather XML had the same configurability
JSON has, it is a small follow-up — a preference question, not a technical one.
measured here that is roughly an 85 MB profile; nothing tracked approaches it, and the
28 MB link in the original report would emit ~87 MB, well inside. Worth an explicit nod
rather than a silent tightening.
Not addressed here, noted while working:
LoadXml's RelaxNG validation paths returnfalsewithoutxmlFreeDoc(doc)— pre-existing, out of scope for this issue, and betterraised separately than folded into this diff.