Repository navigation
🐛 fix(relaxng): reject invalid grammars at compile - #951
Merged
Merged
Conversation
gaborbernat
force-pushed
the
fix/relaxng-compile-guards
branch
from
October 1, 2026 14:38
5c55a42 to
47691f9
Compare
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_feature[compile-rng] |
89.8 µs | 101.5 µs | -11.49% |
| ⚡ | test_feature[validate-rng] |
8 ms | 6.6 ms | +21.84% |
| ⚡ | test_feature[is-valid-rng-valid] |
8 ms | 6.6 ms | +21.23% |
| ⚡ | test_feature[validate-rng-reuse-interleave] |
1.4 ms | 1.2 ms | +19.96% |
| ⚡ | test_feature[validate-rng-reuse] |
5.2 ms | 4.7 ms | +10.98% |
| ⚡ | test_feature[select-relative-sibling] |
45.6 µs | 42.9 µs | +6.31% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing gaborbernat:fix/relaxng-compile-guards (ade2943) with main (2af1136)
Footnotes
-
32 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
gaborbernat
force-pushed
the
fix/relaxng-compile-guards
branch
3 times, most recently
from
October 1, 2026 16:43
bb18ec2 to
e513245
Compare
Enforce the RELAX NG grammar restrictions at compile time so a malformed or adversarial schema fails with a ValueError instead of crashing, hanging, or exhausting memory when a document is validated. - A <ref> with no name dereferenced a null attribute (SIGSEGV); reject it as the spec (4.10) requires. - A reference cycle that never crosses an element hung or overflowed the stack; detect it with the 4.19 depth-stamp walk libxml2/jing/MSV use. - An interleave whose branches share an element name or both match text violates 4.19/7.4 and drove the derivative into exponential memory; reject it at compile. - A legal but ambiguous choice doubled the residual per child; hash-cons the derivative patterns and drop duplicate choice branches so it stays bounded. The runtime ref-cycle guards are now unreachable and removed; the compile check is the single source of cycle-freedom.
gaborbernat
force-pushed
the
fix/relaxng-compile-guards
branch
from
October 1, 2026 16:51
e513245 to
ade2943
Compare
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.
Compiling a
RelaxNGschema accepted several grammar classes the specification forbids and the C derivative engine cannot process safely, so a malformed or adversarial schema crashed, hung, or exhausted memory (CWE-476, CWE-674, CWE-407). 🔒 The fix enforces the restrictions at compile, so an invalid grammar fails fast with aValueErrorinstead of at validation time; the thin Python shim is unchanged and all of the work is in therelaxng.hC core.A
<ref>with nonameattribute dereferenced a NULL attribute pointer and crashed the interpreter (SIGSEGV). It is now rejected during compilation, matching libxml2'sXML_RNGP_REF_NO_NAME, and the specification requires the name onref(section 4.10).A reference cycle whose expansion never passes through an
element, a self-referential or left-recursivedefine, hung or overflowed the stack. Compilation now detects it with a reachable-only ref-graph walk that stamps depth across eachelementboundary, the section 4.19 rule that libxml2, James Clark's jing and Sun's MSV all enforce, and raisesdefine '<name>' references itself with no element in between. Because the compile check now guarantees cycle-freedom, the now-unreachable runtimebuildingguards in the derivative functions were removed, so the compile pass is the single source of that invariant, as it is in jing and MSV, which carry no runtime cycle guard.An
interleavewhose branches can match an element with the same name, or can both match text, violates the section 7.4 restriction. It previously compiled and then drove the derivative into exponential memory on a tiny document; it is now rejected at compile, as libxml2 ("Element or text conflicts in interleave"), jing and MSV do.A legal but ambiguous
choice, for exampleoneOrMore(choice(a, group(a, a))), doubled the residual pattern on each child element and exhausted memory at about 26 children. The per-validation derivative now hash-conses patterns and drops a duplicate choice branch, following James Clark's derivative algorithm and the same interning jing uses in itsPatternInterner/makeChoiceand MSV in itsExpressionPool, so 400 children validate in flat memory, the result lxml also returns.One intentional divergence from lxml: libxml2 rejects a conflicting construct even inside an unreferenced
define, while this fix scans only patterns reachable fromstart, because an unreachable<define>is never built or validated and so cannot crash or run away. That choice is memory-safe, preserves every correctness fix for each reachable pattern, and keeps compilation of a grammar with many dead defines close to its former cost.<ref>no nameXML_RNGP_REF_NO_NAME)ExpressionPoolnameAny application that compiles RELAX NG schemas it does not fully control, or validates untrusted documents against an ambiguous schema, was open to a crash, hang, or memory exhaustion; there is no confidentiality or integrity impact. Schemas the validator already accepted compile and validate as before; the only accepted-to-rejected change is for the malformed schemas this closes (a nameless reference, a reference cycle, an ambiguous choice or interleave), which the RELAX NG spec forbids and other validators reject at compile.
This fixes the RELAX NG compile-time crash, cycle hang, and ambiguous-pattern memory exhaustion, tracked privately in GHSA-q28g-vj28-89fm.