This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/walker in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 817eab17b7d2706c37a60e4d84abbcc7bc6808ca Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 24 06:50:04 2026 +0100 Walker: bound the depth of an acyclic schema walk --- README.txt | 13 + THREAT-MODEL.md | 26 +- .../ws/commons/schema/walker/XmlSchemaScope.java | 24 +- .../ws/commons/schema/walker/XmlSchemaWalker.java | 58 ++++- .../schema/walker/SubstitutionGroupTypeTest.java | 3 +- .../schema/walker/WalkerDepthLimitTest.java | 276 +++++++++++++++++++++ 6 files changed, 388 insertions(+), 12 deletions(-) diff --git a/README.txt b/README.txt index 1e5f2019..c5153d9b 100644 --- a/README.txt +++ b/README.txt @@ -39,6 +39,17 @@ adjust the per-document limits: Maximum previously traversed events replayed while backtracking. The default is 1000000. +The XML Schema walker limits how deeply it recurses while walking a schema, +so that a deeply nested schema is rejected instead of exhausting the thread +stack. The following JVM system property adjusts the limit: + + org.apache.ws.commons.schema.walker.maxDepth + Maximum depth of nested elements, model groups and substitution group + members while walking a schema; the same limit applies separately to + levels of type derivation and of attribute group references. The + default is 256, which fits a thread stack of 512 KB. Lower it if the + walker runs on threads with smaller stacks. + The schema collection limits the work performed while resolving imported and included schemas. The following JVM system properties adjust the limits: @@ -127,6 +138,8 @@ For example, set a limit with: -Dorg.apache.ws.commons.schema.walker.maxDecisionPoints=20000 + -Dorg.apache.ws.commons.schema.walker.maxDepth=128 + -Dorg.apache.ws.commons.schema.maxImportDepth=128 -Dorg.apache.ws.commons.schema.maxNestingDepth=256 diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 639d0fca..9bda3e77 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -192,7 +192,10 @@ A finding is in-model only if it reaches a row marked **yes**. derivation, substitution groups, model groups, or attribute groups and recurse until `StackOverflowError`. The walker now tracks these expansion paths and rejects cyclic re-entry with `XmlSchemaException`; recursion that - crosses an element declaration remains supported. + crosses an element declaration remains supported. Acyclic chains of the + same expansions could also be nested deeply enough to exhaust the stack; + the walker bounds their depth and rejects a deeper chain with + `XmlSchemaException`. - **`XmlSchemaPathFinder`**: in-model when caller-supplied SAX events are matched against an attacker-controlled schema. Its backtracking work is bounded per document by configurable decision-point and @@ -239,7 +242,8 @@ A finding is in-model only if it reaches a row marked **yes**. construction is bounded by a default structural nesting depth of 512; these limits are configurable with JVM system properties. The walker has active-path cycle detection for the schema expansion graphs it traverses, - but large acyclic schemas may still consume substantial memory and CPU + and a configurable default maximum walk depth of 256, but large acyclic + schemas may still consume substantial memory and CPU *(documented: `README.txt`)*. - **System properties**: `org.apache.ws.commons.schema.extension_registry` is consulted at `XmlSchemaCollection` construction time, and the @@ -282,6 +286,7 @@ points*: | `XmlSchemaCollection.setBaseUri(String)` | unset *(documented)* | caller-supplied | base URI against which relative `schemaLocation` values resolve | | `org.apache.ws.commons.schema.walker.maxDecisionPoints` system property | `10000` *(documented: `XmlSchemaPathFinder.java`)* | operator-tunable per-process limit | maximum decision points created while matching one document | | `org.apache.ws.commons.schema.walker.maxReplayedEvents` system property | `1000000` *(documented: `XmlSchemaPathFinder.java`)* | operator-tunable per-process limit | maximum previously traversed events replayed while backtracking through one document | +| `org.apache.ws.commons.schema.walker.maxDepth` system property | `256` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum depth of nested elements, model groups and substitution group members while walking a schema, and separately of type derivation and of attribute group references; a deployment running the walker on threads with small stacks (under about 512 KB) should lower it | | `org.apache.ws.commons.schema.maxImportDepth` system property | `64` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum import/include resolution depth for one schema read | | `org.apache.ws.commons.schema.maxSchemaResolutions` system property | `1000` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum schema documents resolved during one top-level read | | `org.apache.ws.commons.schema.maxNestingDepth` system property | `512` *(documented: `README.txt`)* | operator-tunable per-process limit | maximum structural nesting depth while building the schema model, including nested include/import/redefine document resolutions | @@ -350,9 +355,13 @@ leave open. the limit is configurable with a JVM system property *(documented: `README.txt`)*. - Walker expansion cycles are rejected for type derivation, substitution - groups, model groups, and attribute groups. This prevents recursive - stack exhaustion for malformed but parseable schemas; it is not a general - limit on the size or cost of an acyclic schema. + groups, model groups, and attribute groups. The depth of an acyclic walk + is bounded by default to 256 nested elements, model groups and + substitution group members, and separately to 256 levels of type + derivation and of attribute group references; the limit is configurable + with a JVM system property *(documented: `README.txt`)*. Together these + prevent recursive stack exhaustion for malformed but parseable schemas; + they are not a general limit on the size or cost of an acyclic schema. - No rate limit on URL fetches when following `xs:import`; bounding fetch rate is a §10 caller responsibility *(maintainer — §14 Q12)*. - `XmlSchemaPathFinder` bounds decision points and replayed events per @@ -440,7 +449,8 @@ leave open. `XmlSchemaWalker.walk(XmlSchemaElement)`. - **Property**: cyclic type derivation, substitution-group, model-group, and attribute-group expansions terminate with `XmlSchemaException` rather - than recursing indefinitely. Legal recursive content that passes through + than recursing indefinitely, as do acyclic chains of them nested beyond + the configurable walk depth. Legal recursive content that passes through an element declaration remains walkable. - **Violation symptom**: a parseable schema causes the walker to recurse until `StackOverflowError` or another resource-exhaustion failure. @@ -550,8 +560,8 @@ matching disclaimer. `FEATURE_SECURE_PROCESSING=true`, but not universally. - **Schema-amplification DoS** — large or heavily-recursive acyclic schemas can exhaust memory or CPU within the documented resource limits; - structural nesting and cyclic walker expansion are rejected as described - in §8 P5 and §8 P6. + structural nesting and cyclic or over-deep walker expansion are rejected + as described in §8 P5 and §8 P6. - **Confused-deputy fetch via untrusted `baseUri` + relative `schemaLocation`** — the operator-supplied base URI is trusted. diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java index 24cd4dd8..1fcb5592 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java @@ -75,6 +75,7 @@ final class XmlSchemaScope { private XmlSchemaAnyAttribute anyAttr; private Set<QName> userRecognizedTypes; private Set<XmlSchemaType> typesInProgress; + private int maxDepth; private final Set<QName> attributeGroupsInProgress = new HashSet<QName>(); /** @@ -93,6 +94,7 @@ final class XmlSchemaScope { this.scopeCache = child.scopeCache; this.userRecognizedTypes = child.userRecognizedTypes; this.typesInProgress = child.typesInProgress; + this.maxDepth = child.maxDepth; walkWithCycleCheck(type); } @@ -101,9 +103,12 @@ final class XmlSchemaScope { * Initializes a new {@link XmlSchemaScope} with a base * {@link XmlSchemaElement}. The element type and attributes will be * traversed, and attribute lists and element children will be retrieved. + * Chains of type derivation and of attribute group references deeper + * than <code>maxDepth</code> are rejected. */ XmlSchemaScope(XmlSchemaType type, SchemasByNamespace xmlSchemasByNamespace, - Map<QName, XmlSchemaScope> scopeCache, Set<QName> userRecognizedTypes) { + Map<QName, XmlSchemaScope> scopeCache, Set<QName> userRecognizedTypes, + int maxDepth) { this(); @@ -112,11 +117,21 @@ final class XmlSchemaScope { this.userRecognizedTypes = userRecognizedTypes; this.typesInProgress = Collections.newSetFromMap(new IdentityHashMap<XmlSchemaType, Boolean>()); + this.maxDepth = maxDepth; walkWithCycleCheck(type); } private void walkWithCycleCheck(XmlSchemaType type) { + // Each level of derivation recurses; an acyclic chain can still + // exhaust the thread stack. + if (typesInProgress.size() >= maxDepth) { + throw new XmlSchemaException("The type " + getName(type, "{Anonymous}") + + " is derived through more than " + maxDepth + + " levels of base types; refusing to walk it. The limit" + + " may be changed with the " + + XmlSchemaWalker.MAX_DEPTH_PROPERTY + " system property."); + } if (!typesInProgress.add(type)) { throw new XmlSchemaException("Cyclic type derivation detected involving type " + getName(type, "{Anonymous}") + '.'); @@ -539,6 +554,13 @@ final class XmlSchemaScope { throw new XmlSchemaException("The attribute group reference " + groupName + " does not resolve to an attribute group in this collection."); } + if (attributeGroupsInProgress.size() >= maxDepth) { + throw new XmlSchemaException("The attribute group reference " + groupName + + " is nested more than " + maxDepth + + " levels deep; refusing to walk it. The limit may be" + + " changed with the " + XmlSchemaWalker.MAX_DEPTH_PROPERTY + + " system property."); + } if ((groupName != null) && !attributeGroupsInProgress.add(groupName)) { throw new XmlSchemaException("Cyclic attribute group reference detected involving " + groupName + '.'); diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java index 34601c98..59f575c1 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java @@ -65,6 +65,18 @@ public final class XmlSchemaWalker { private final IdentityHashMap<XmlSchemaType, XmlSchemaScope> anonymousScopeCache; private final Map<QName, XmlSchemaType> typesBySubstGroupHead; private Set<QName> substGroupsInProgress = new HashSet<QName>(); + + /* + * The walk recurses once per nested element, model group and + * substitution group member. An acyclic schema can nest these deeply + * enough to exhaust the thread stack, so the depth is bounded. The bound + * is read per instance, so that setting the system property takes effect + * without a class reload. + */ + static final String MAX_DEPTH_PROPERTY = "org.apache.ws.commons.schema.walker.maxDepth"; + static final int DEFAULT_MAX_DEPTH = 256; + private final int maxDepth = getMaxDepth(); + private int depth; private Set<QName> groupsInProgress = new HashSet<QName>(); /** @@ -199,6 +211,15 @@ public final class XmlSchemaWalker { * @param element The root element to start the walk from. */ public void walk(XmlSchemaElement element) { + enterNested(); + try { + walkElement(element); + } finally { + depth--; + } + } + + private void walkElement(XmlSchemaElement element) { element = getElement(element, false); final XmlSchemaElement substGroupElem = element; @@ -235,7 +256,8 @@ public final class XmlSchemaWalker { */ scope = anonymousScopeCache.get(schemaType); } else { - scope = new XmlSchemaScope(schemaType, schemasByNamespace, scopeCache, userRecognizedTypes); + scope = new XmlSchemaScope(schemaType, schemasByNamespace, scopeCache, userRecognizedTypes, + maxDepth); if (schemaType.getQName() != null) { scopeCache.put(schemaType.getQName(), scope); } else { @@ -280,7 +302,7 @@ public final class XmlSchemaWalker { } else { attrScope = new XmlSchemaScope(attrType, schemasByNamespace, scopeCache, - userRecognizedTypes); + userRecognizedTypes, maxDepth); if (attrType.getName() != null) { scopeCache.put(attrType.getQName(), attrScope); @@ -408,6 +430,15 @@ public final class XmlSchemaWalker { } private void walk(XmlSchemaGroupParticle group, long minOccurs, long maxOccurs) { + enterNested(); + try { + walkGroup(group, minOccurs, maxOccurs); + } finally { + depth--; + } + } + + private void walkGroup(XmlSchemaGroupParticle group, long minOccurs, long maxOccurs) { // Only make a copy of the particle if the minOccurs or maxOccurs was // set. @@ -676,6 +707,29 @@ public final class XmlSchemaWalker { return schemaType; } + private void enterNested() { + if (depth >= maxDepth) { + throw new XmlSchemaException("The schema is nested more than " + maxDepth + + " levels deep (nested elements, model groups and" + + " substitution group members each count one level);" + + " refusing to walk it. The limit may be changed with the " + + MAX_DEPTH_PROPERTY + " system property."); + } + depth++; + } + + static int getMaxDepth() { + try { + Integer value = Integer.getInteger(MAX_DEPTH_PROPERTY); + if (value != null) { + return value.intValue(); + } + } catch (SecurityException e) { + // fall through to the default + } + return DEFAULT_MAX_DEPTH; + } + private static QName getElementQName(XmlSchemaElement element) { if (element.isRef()) { return element.getRefBase().getTargetQName(); diff --git a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java index 26c29247..9e122685 100644 --- a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java +++ b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java @@ -127,7 +127,8 @@ public class SubstitutionGroupTypeTest extends Assert { public void testLongUntypedChainResolvesEachHeadOnce() { StringBuilder body = new StringBuilder("<xs:element name=\"root\" type=\"xs:string\"/>"); String previous = "root"; - for (int i = 0; i < 2000; i++) { + // The longest chain the walker's default depth limit allows. + for (int i = 0; i < XmlSchemaWalker.DEFAULT_MAX_DEPTH - 1; i++) { body.append("<xs:element name=\"e").append(i).append("\" substitutionGroup=\"tns:") .append(previous).append("\"/>"); previous = "e" + i; diff --git a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/WalkerDepthLimitTest.java b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/WalkerDepthLimitTest.java new file mode 100644 index 00000000..f4cd8570 --- /dev/null +++ b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/WalkerDepthLimitTest.java @@ -0,0 +1,276 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.ws.commons.schema.walker; + +import java.io.StringReader; + +import javax.xml.namespace.QName; + +import org.apache.ws.commons.schema.XmlSchemaCollection; +import org.apache.ws.commons.schema.XmlSchemaElement; +import org.apache.ws.commons.schema.XmlSchemaException; +import org.apache.ws.commons.schema.docpath.XmlSchemaStateMachineGenerator; + +import org.junit.Assert; +import org.junit.Test; + +/** + * An acyclic schema can nest elements, model groups, substitution group + * members, type derivations or attribute group references deeply enough to + * exhaust the thread stack while it is walked. Each of these is bounded by + * the walker's maximum depth, and exceeding it is reported as an + * XmlSchemaException. + */ +public class WalkerDepthLimitTest extends Assert { + + private static final String NS = "urn:depth"; + + /** Far beyond the default limit, and deep enough to overflow a default stack without it. */ + private static final int DEEP = 5000; + + /** Comfortably within the default limit. */ + private static final int SHALLOW = 100; + + private static void walkRoot(String body) { + XmlSchemaCollection collection = new XmlSchemaCollection(); + collection.read(new StringReader( + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:tns=\"" + NS + "\"" + + " targetNamespace=\"" + NS + "\" elementFormDefault=\"qualified\">" + + body + + "</xs:schema>")); + XmlSchemaElement root = collection.getElementByQName(new QName(NS, "root")); + assertNotNull("the schema under test must declare a 'root' element", root); + new XmlSchemaWalker(collection, new XmlSchemaStateMachineGenerator()).walk(root); + } + + private static void assertRejected(String body) { + try { + walkRoot(body); + fail("expected the deeply nested schema to be rejected"); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage(), + expected.getMessage().contains(XmlSchemaWalker.MAX_DEPTH_PROPERTY)); + } + } + + /** root, with e0 substituting for root, e1 for e0, and so on. */ + private static String substitutionGroupChain(int length) { + StringBuilder body = new StringBuilder("<xs:element name=\"root\" type=\"xs:string\"/>"); + String head = "root"; + for (int i = 0; i < length; i++) { + body.append("<xs:element name=\"e").append(i).append("\" type=\"xs:string\"") + .append(" substitutionGroup=\"tns:").append(head).append("\"/>"); + head = "e" + i; + } + return body.toString(); + } + + /** root contains a reference to e1, which contains a reference to e2, and so on. */ + private static String elementReferenceChain(int length) { + return elementReferenceChain(length, null); + } + + private static String elementReferenceChain(int length, String lastType) { + StringBuilder body = new StringBuilder(); + for (int i = 0; i < length; i++) { + body.append("<xs:element name=\"").append((i == 0) ? "root" : "e" + i).append('"'); + if (i + 1 < length) { + body.append("><xs:complexType><xs:sequence><xs:element ref=\"tns:e").append(i + 1) + .append("\"/></xs:sequence></xs:complexType></xs:element>"); + } else { + body.append(" type=\"").append((lastType == null) ? "xs:string" : lastType).append("\"/>"); + } + } + return body.toString(); + } + + /** root is of type T0, which contains an element of type T1, and so on. */ + private static String namedTypeChain(int length) { + StringBuilder body = new StringBuilder("<xs:element name=\"root\" type=\"tns:T0\"/>"); + for (int i = 0; i < length; i++) { + body.append("<xs:complexType name=\"T").append(i).append("\"><xs:sequence>"); + if (i + 1 < length) { + body.append("<xs:element name=\"c\" type=\"tns:T").append(i + 1).append("\"/>"); + } + body.append("</xs:sequence></xs:complexType>"); + } + return body.toString(); + } + + /** T1 extends T0, T2 extends T1, and so on; T0 references the given attribute group, if any. */ + private static String complexExtensionChain(int length, String attributeGroup) { + StringBuilder body = new StringBuilder("<xs:complexType name=\"T0\"><xs:sequence>" + + "<xs:element name=\"a0\" type=\"xs:string\"/>" + + "</xs:sequence>"); + if (attributeGroup != null) { + body.append("<xs:attributeGroup ref=\"tns:").append(attributeGroup).append("\"/>"); + } + body.append("</xs:complexType>"); + for (int i = 1; i < length; i++) { + body.append("<xs:complexType name=\"T").append(i).append("\"><xs:complexContent>") + .append("<xs:extension base=\"tns:T").append(i - 1).append("\"><xs:sequence>") + .append("<xs:element name=\"a").append(i).append("\" type=\"xs:string\"/>") + .append("</xs:sequence></xs:extension></xs:complexContent></xs:complexType>"); + } + return body.toString(); + } + + /** S1 restricts S0, S2 restricts S1, and so on. */ + private static String simpleRestrictionChain(int length) { + StringBuilder body = new StringBuilder("<xs:element name=\"root\" type=\"tns:S") + .append(length - 1).append("\"/>") + .append("<xs:simpleType name=\"S0\"><xs:restriction base=\"xs:string\"/></xs:simpleType>"); + for (int i = 1; i < length; i++) { + body.append("<xs:simpleType name=\"S").append(i).append("\"><xs:restriction base=\"tns:S") + .append(i - 1).append("\"/></xs:simpleType>"); + } + return body.toString(); + } + + /** g0 references g1, which references g2, and so on. */ + private static String groupReferenceChain(int length) { + StringBuilder body = new StringBuilder("<xs:element name=\"root\"><xs:complexType><xs:sequence>" + + "<xs:group ref=\"tns:g0\"/>" + + "</xs:sequence></xs:complexType></xs:element>"); + for (int i = 0; i < length; i++) { + body.append("<xs:group name=\"g").append(i).append("\"><xs:sequence>"); + if (i + 1 < length) { + body.append("<xs:group ref=\"tns:g").append(i + 1).append("\"/>"); + } else { + body.append("<xs:element name=\"leaf\" type=\"xs:string\"/>"); + } + body.append("</xs:sequence></xs:group>"); + } + return body.toString(); + } + + /** ag0 references ag1, which references ag2, and so on. */ + private static String attributeGroupChain(int length) { + StringBuilder body = new StringBuilder(); + for (int i = 0; i < length; i++) { + body.append("<xs:attributeGroup name=\"ag").append(i).append("\">"); + if (i + 1 < length) { + body.append("<xs:attributeGroup ref=\"tns:ag").append(i + 1).append("\"/>"); + } else { + body.append("<xs:attribute name=\"a\" type=\"xs:string\"/>"); + } + body.append("</xs:attributeGroup>"); + } + return body.toString(); + } + + private static String rootWithAttributeGroupChain(int length) { + return "<xs:element name=\"root\"><xs:complexType><xs:attributeGroup ref=\"tns:ag0\"/>" + + "</xs:complexType></xs:element>" + attributeGroupChain(length); + } + + private static String rootWithComplexExtensionChain(int length) { + return "<xs:element name=\"root\" type=\"tns:T" + (length - 1) + "\"/>" + + complexExtensionChain(length, null); + } + + @Test + public void testDeepSubstitutionGroupChainIsRejected() { + assertRejected(substitutionGroupChain(DEEP)); + } + + @Test + public void testDeepElementReferenceChainIsRejected() { + assertRejected(elementReferenceChain(DEEP)); + } + + @Test + public void testDeepNamedTypeChainIsRejected() { + assertRejected(namedTypeChain(DEEP)); + } + + @Test + public void testDeepGroupReferenceChainIsRejected() { + assertRejected(groupReferenceChain(DEEP)); + } + + @Test + public void testDeepComplexExtensionChainIsRejected() { + assertRejected(rootWithComplexExtensionChain(DEEP)); + } + + @Test + public void testDeepSimpleRestrictionChainIsRejected() { + assertRejected(simpleRestrictionChain(DEEP)); + } + + @Test + public void testDeepAttributeGroupChainIsRejected() { + assertRejected(rootWithAttributeGroupChain(DEEP)); + } + + @Test + public void testShallowChainsStillWalk() { + walkRoot(substitutionGroupChain(SHALLOW)); + walkRoot(elementReferenceChain(SHALLOW)); + walkRoot(namedTypeChain(SHALLOW)); + walkRoot(groupReferenceChain(SHALLOW)); + walkRoot(rootWithComplexExtensionChain(SHALLOW)); + walkRoot(simpleRestrictionChain(SHALLOW)); + walkRoot(rootWithAttributeGroupChain(SHALLOW)); + } + + /* + * Type derivation and attribute group references are resolved while the + * element walk is already at depth, so the stack must hold the element + * walk and either chain at their limits at once. A complex type's + * content is walked as one nested model group per level of extension, + * so the deepest derivation that does not also nest content is a simple + * type's. + */ + @Test + public void testAllChainsJustWithinTheLimitsTogetherStillWalk() { + final int limit = XmlSchemaWalker.DEFAULT_MAX_DEPTH; + // Each element level counts itself and its sequence. + final int elementLevels = limit / 2; + // The leaf type and the built-in xs:string at the base of the + // chain each count one level of derivation too. + final int simpleDerivations = limit - 2; + walkRoot(elementReferenceChain(elementLevels, "tns:Leaf") + + "<xs:complexType name=\"Leaf\"><xs:simpleContent>" + + "<xs:extension base=\"tns:S" + (simpleDerivations - 1) + "\">" + + "<xs:attributeGroup ref=\"tns:ag0\"/>" + + "</xs:extension></xs:simpleContent></xs:complexType>" + + simpleRestrictionChain(simpleDerivations).replaceFirst("<xs:element [^>]*/>", "") + + attributeGroupChain(limit)); + } + + @Test + public void testLimitIsConfigurable() { + final String previous = System.getProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY); + System.setProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY, "10"); + try { + assertRejected(substitutionGroupChain(20)); + assertRejected(rootWithComplexExtensionChain(20)); + assertRejected(rootWithAttributeGroupChain(20)); + walkRoot(substitutionGroupChain(5)); + } finally { + if (previous == null) { + System.clearProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY); + } else { + System.setProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY, previous); + } + } + } +}
