From e684bee2353e633a56b7dbda0f36269e82f00518 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 4 Nov 2026 10:09:15 +0200 Subject: [PATCH 1/2] docs(xml): describe the parser that enforces limits and builds the tree The documents still described a lexical pass before every parse and an xmloxide DOM projected into the semantic arena. An xmloxide parse of a document without a DOCTYPE now enforces the node, depth and namespace limits as it reads and builds the semantic arena directly, reading entity replacement text into the same tree; a DOCTYPE document and roxmltree or differential parses pass one strict scan that checks the limits while recording positions. The projection benchmark covers roxmltree only. Closes #387 --- docs/benchmarks.md | 7 ++++--- docs/xml-backend-patches.md | 31 ++++++++++++++++++++----------- docs/xmldsig.md | 7 +++++-- docs/xmlenc.md | 7 ++++--- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 866bb2b4..6e9f8bd4 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -45,9 +45,10 @@ selected provider. Rejection cases check corrupted signature evidence and malfor XML on every backend, rather than interpreting an error as a successful operation. RSA-SHA256 sign/verify use the selected provider's actual key handle. -Projection calls the production normalizer over a prepared backend DOM and lexical -positions, without a second parser invocation. Differential parsing has no -single-backend projection measurement. Graph compilation and execution are separate +Projection calls the production normalizer over a prepared roxmltree DOM and lexical +positions, without a second parser invocation. It is measured for roxmltree only: +xmloxide builds the semantic arena as it parses, so its parse case covers that +work, and differential parsing has no single-backend projection. Graph compilation and execution are separate synthetic fan-out measurements at 1/8/32/64 references, not substitutes for complete operation measurements. Typed policy validation/checks are measured independently; their cost must not be subtracted from an end-to-end result to claim an unsafe path. diff --git a/docs/xml-backend-patches.md b/docs/xml-backend-patches.md index ac0a0b0a..44b1e144 100644 --- a/docs/xml-backend-patches.md +++ b/docs/xml-backend-patches.md @@ -51,15 +51,24 @@ of a replacement node arena solely to drop the consumed parser. The maintained construction kernels specialize storage without changing parser selection or grammar. Scoped reference replay pays raw parser state, DTD models, -namespace indexes, source positions and the semantic arena with its current -execution. DTD entity/default tables move into runtime state; entity fragments -move node payloads while rebasing links instead of cloning recursive subtrees. -Element and DTD-group parsing use bounded, paid continuation stacks. Diagnostic -labels retain their allocation charges, and resource refusal is terminal rather -than a malformed-input rejection or a reason to try another backend. +namespace indexes and the semantic arena with its current execution. DTD +entity/default tables move into runtime state. Element and DTD-group parsing use +bounded, paid continuation stacks. Diagnostic labels retain their allocation +charges, and resource refusal is terminal rather than a malformed-input +rejection or a reason to try another backend. -The backend adapters continue to use the shared lexical preflight and semantic -arena. Backend selection does not introduce implicit fallback, a second ID -resolver or an external entity acquisition path. Differential mode still -compares the two semantic projections rather than choosing whichever parser -accepts an input. +The xmloxide parser is generic over the tree it writes to. xml-sec parses into +the semantic arena directly, with no intermediate xmloxide DOM: the parser +supplies every source range, and an entity's replacement text is read into the +same tree, its nodes taking the outermost reference's range and remaining +unactionable. A DOCTYPE is recognized only by its case-sensitive keyword +(XML 1.0 production 28). Without a DOCTYPE the parser itself enforces the node, +depth and namespace limits as it reads; a document with a DOCTYPE first passes +the strict lexical scan that bounds its declarations and entity expansion. + +roxmltree builds its own DOM, which is projected into the same semantic arena +through the source positions the lexical scan records; positions are recorded +only for a backend that reads them. Backend selection does not introduce +implicit fallback, a second ID resolver or an external entity acquisition path. +Differential mode still compares the two semantic trees rather than choosing +whichever parser accepts an input. diff --git a/docs/xmldsig.md b/docs/xmldsig.md index 40526e14..4cb4ca4e 100644 --- a/docs/xmldsig.md +++ b/docs/xmldsig.md @@ -497,8 +497,11 @@ certificates provide key material and do not become trusted merely because they certificate is present in both pools, its explicit trusted classification is retained. `ResourcePolicy::max_xml_document_bytes` rejects verification and signing inputs before DOM parsing; the same immutable ceiling is rechecked after signing mutations that enlarge the XML. -The selected node and depth ceilings are enforced by a streaming preflight before either parser -backend allocates a DOM, then rechecked on every staged copy, adapter parse, and committed generation. +The selected node, depth and namespace-binding ceilings are enforced before the node that would +exceed them is stored: an xmloxide parse of a document without a DOCTYPE counts them as it reads, +while a document with a DOCTYPE, and every roxmltree or differential parse, passes one strict +lexical scan that checks them before the parser runs. They are rechecked on every staged copy, +adapter parse, and committed generation. `ResourcePolicy::max_xml_parse_work_bytes` separately bounds cumulative parser work across the whole operation. Initial parsing, generated-template validation, binary-to-node-set adapters, staged copies, digest dependency levels, Manifest recursion, and committed generations all charge diff --git a/docs/xmlenc.md b/docs/xmlenc.md index 3c318979..4e9fe8a1 100644 --- a/docs/xmlenc.md +++ b/docs/xmlenc.md @@ -298,9 +298,10 @@ AES-GCM. Use `decrypt_document` to replace one typed `EncryptedData` in a complete XML string. Pass its `Id` when the document contains multiple encrypted regions. The compiled decryption policy checks the shared `ResourcePolicy::max_xml_document_bytes` ceiling before DOM allocation and applies its -XML node and depth ceilings to the initial document and every replacement generation. A bounded -streaming preflight enforces these limits before either parser backend allocates a DOM and charges -recursive internal-entity replacement traversal to cumulative XML parse work. +XML node and depth ceilings to the initial document and every replacement generation. A refusal +comes before the node that exceeds them is stored: an xmloxide parse of a document without a +DOCTYPE checks them as it reads, and any other parse passes one strict lexical scan first, which +also charges recursive internal-entity replacement traversal to cumulative XML parse work. `encrypt_owned_document` and `decrypt_owned_document` reuse the retained parsed view, validate replacement XML in the parent namespace context, and invalidate prior node identities after a successful mutation. String APIs From 8810c997d6e2de0d4e8dcbe77e36fd585a5bee43 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 4 Nov 2026 10:31:58 +0200 Subject: [PATCH 2/2] docs(xml): state the dispatch and every limit the parse enforces - The path is chosen by whether the text `