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);
+            }
+        }
+    }
+}

Reply via email to