This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/budget-resolution
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git

commit 666ddc4c89ffb7e3aa0b73c3f96d5ef98471cf76
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 17 07:52:42 2026 +0100

    Fix scchema resolution budget bug
---
 .../ws/commons/schema/XmlSchemaCollection.java     |  77 ++++++++++----
 .../java/tests/SchemaResolutionBudgetTest.java     | 115 +++++++++++++++++++++
 2 files changed, 174 insertions(+), 18 deletions(-)

diff --git 
a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
 
b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
index 0a367c25..0c3b0507 100644
--- 
a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
+++ 
b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
@@ -209,6 +209,14 @@ public final class XmlSchemaCollection {
 
     private int resolutionsSinceTopLevelRead;
 
+    /**
+     * Depth of nested {@link #read} calls. A schema resolved through
+     * <code>xs:import</code>/<code>xs:include</code>/<code>xs:redefine</code> 
is built by a
+     * re-entrant read, so this distinguishes the outermost read - where the 
per-read resolution
+     * budget starts - from the nested ones that spend it.
+     */
+    private int activeReads;
+
     /**
      * Creates new XmlSchemaCollection
      */
@@ -539,10 +547,6 @@ public final class XmlSchemaCollection {
      * @param pKey the schema key.
      */
     public void push(SchemaKey pKey) {
-        if (stack.isEmpty()) {
-            // A new top-level read is starting.
-            resolutionsSinceTopLevelRead = 0;
-        }
         if (stack.size() >= MAX_IMPORT_DEPTH) {
             throw new XmlSchemaException("Maximum schema import/include depth 
of " + MAX_IMPORT_DEPTH
                                          + " exceeded while resolving " + pKey
@@ -584,10 +588,15 @@ public final class XmlSchemaCollection {
      * @return the schema object.
      */
     public XmlSchema read(Document doc, String systemId, 
TargetNamespaceValidator validator) {
-        SchemaBuilder builder = new SchemaBuilder(this, validator);
-        XmlSchema schema = builder.build(doc, systemId);
-        schema.setInputEncoding(doc.getInputEncoding());
-        return schema;
+        enterRead();
+        try {
+            SchemaBuilder builder = new SchemaBuilder(this, validator);
+            XmlSchema schema = builder.build(doc, systemId);
+            schema.setInputEncoding(doc.getInputEncoding());
+            return schema;
+        } finally {
+            exitRead();
+        }
     }
 
     /**
@@ -599,8 +608,13 @@ public final class XmlSchemaCollection {
      * @return the XML schema object.
      */
     public XmlSchema read(Document doc) {
-        SchemaBuilder builder = new SchemaBuilder(this, null);
-        return builder.build(doc, null);
+        enterRead();
+        try {
+            SchemaBuilder builder = new SchemaBuilder(this, null);
+            return builder.build(doc, null);
+        } finally {
+            exitRead();
+        }
     }
 
     /**
@@ -611,10 +625,15 @@ public final class XmlSchemaCollection {
      * @return the XmlSchema
      */
     public XmlSchema read(Element elem) {
-        SchemaBuilder builder = new SchemaBuilder(this, null);
-        XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, null);
-        xmlSchema.setInputEncoding(elem.getOwnerDocument().getXmlEncoding());
-        return xmlSchema;
+        enterRead();
+        try {
+            SchemaBuilder builder = new SchemaBuilder(this, null);
+            XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, null);
+            
xmlSchema.setInputEncoding(elem.getOwnerDocument().getXmlEncoding());
+            return xmlSchema;
+        } finally {
+            exitRead();
+        }
     }
 
     /**
@@ -626,10 +645,15 @@ public final class XmlSchemaCollection {
      * @return the schema object.
      */
     public XmlSchema read(Element elem, String systemId) {
-        SchemaBuilder builder = new SchemaBuilder(this, null);
-        XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, systemId);
-        xmlSchema.setInputEncoding(elem.getOwnerDocument().getInputEncoding());
-        return xmlSchema;
+        enterRead();
+        try {
+            SchemaBuilder builder = new SchemaBuilder(this, null);
+            XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, 
systemId);
+            
xmlSchema.setInputEncoding(elem.getOwnerDocument().getInputEncoding());
+            return xmlSchema;
+        } finally {
+            exitRead();
+        }
     }
 
     /**
@@ -818,6 +842,23 @@ public final class XmlSchemaCollection {
         return schemas.get(pKey);
     }
 
+    /**
+     * Marks the start of a read. The per-read resolution budget is reset only 
for the outermost
+     * one: the stack of in-progress resolutions returns to empty between two 
sibling imports of
+     * the same document, so resetting whenever it is empty charged each 
branch of the import
+     * graph separately and left the total unbounded.
+     */
+    private void enterRead() {
+        if (activeReads == 0) {
+            resolutionsSinceTopLevelRead = 0;
+        }
+        activeReads++;
+    }
+
+    private void exitRead() {
+        activeReads--;
+    }
+
     XmlSchema read(InputSource inputSource, TargetNamespaceValidator 
namespaceValidator) {
         try {
             DocumentBuilderFactory docFac = 
DocumentBuilderFactory.newInstance();
diff --git a/xmlschema-core/src/test/java/tests/SchemaResolutionBudgetTest.java 
b/xmlschema-core/src/test/java/tests/SchemaResolutionBudgetTest.java
new file mode 100644
index 00000000..66c4a442
--- /dev/null
+++ b/xmlschema-core/src/test/java/tests/SchemaResolutionBudgetTest.java
@@ -0,0 +1,115 @@
+/**
+ * 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 tests;
+
+import java.io.StringReader;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.apache.ws.commons.schema.XmlSchemaException;
+import org.apache.ws.commons.schema.resolver.URIResolver;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+
+/**
+ * maxSchemaResolutions is meant to bound the number of documents one read may 
pull in. The
+ * bound has to hold however the import graph is shaped: a deep chain is 
already caught by
+ * maxImportDepth, so the case that needs this budget is the shallow, wide one.
+ */
+public class SchemaResolutionBudgetTest extends Assert {
+
+    /** The documented default of 
org.apache.ws.commons.schema.maxSchemaResolutions. */
+    private static final int MAX_RESOLUTIONS = 1000;
+
+    /** Counts how many documents a read actually pulled in. */
+    private static final class CountingResolver implements URIResolver {
+        private final AtomicInteger count = new AtomicInteger();
+
+        public InputSource resolveEntity(String namespace, String 
schemaLocation, String baseUri) {
+            int id = count.incrementAndGet();
+            InputSource source = new InputSource(new StringReader(
+                "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"; 
targetNamespace=\""
+                + namespace + "\"><xs:element name=\"e\" 
type=\"xs:string\"/></xs:schema>"));
+            source.setSystemId("http://example.invalid/leaf"; + id + ".xsd");
+            return source;
+        }
+    }
+
+    /** Each read needs its own namespaces, or the second one collides in the 
collection. */
+    private static String rootImporting(String tag, int leaves) {
+        StringBuilder root = new StringBuilder(
+            "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"; 
targetNamespace=\"urn:root"
+            + tag + "\">");
+        for (int i = 0; i < leaves; i++) {
+            root.append("<xs:import 
namespace=\"urn:leaf").append(tag).append('_').append(i)
+                .append("\" 
schemaLocation=\"http://example.invalid/leaf";).append(tag)
+                .append('_').append(i).append(".xsd\"/>");
+        }
+        return root.append("</xs:schema>").toString();
+    }
+
+    private static CountingResolver read(int leaves) {
+        XmlSchemaCollection collection = new XmlSchemaCollection();
+        CountingResolver resolver = new CountingResolver();
+        collection.setSchemaResolver(resolver);
+        collection.read(new StringReader(rootImporting("a", leaves)));
+        return resolver;
+    }
+
+    /**
+     * XMLSCHEMA-XXX: the budget was reset whenever the in-progress stack was 
empty, which it is
+     * between two sibling imports of the top-level document, so a wide graph 
never reached it.
+     */
+    @Test
+    public void testWideImportGraphIsBounded() {
+        CountingResolver resolver;
+        try {
+            resolver = read(MAX_RESOLUTIONS * 5);
+            fail("expected the resolution budget to stop a graph of "
+                 + (MAX_RESOLUTIONS * 5) + " imports, but it resolved "
+                 + resolver.count.get());
+        } catch (XmlSchemaException expected) {
+            assertTrue(expected.getMessage(),
+                       expected.getMessage().contains("schema documents were 
resolved"));
+        }
+    }
+
+    /** A graph inside the budget must still resolve in full. */
+    @Test
+    public void testGraphWithinBudgetStillResolves() {
+        CountingResolver resolver = read(MAX_RESOLUTIONS / 2);
+        assertEquals(MAX_RESOLUTIONS / 2, resolver.count.get());
+    }
+
+    /** The budget is per top-level read, so a second read starts from zero. */
+    @Test
+    public void testBudgetIsPerTopLevelRead() {
+        XmlSchemaCollection collection = new XmlSchemaCollection();
+        CountingResolver resolver = new CountingResolver();
+        collection.setSchemaResolver(resolver);
+        for (int i = 0; i < 3; i++) {
+            collection.read(new StringReader(rootImporting("r" + i, 
MAX_RESOLUTIONS / 2)));
+        }
+        assertTrue("a later read must not inherit an earlier read's spend",
+                   resolver.count.get() > MAX_RESOLUTIONS);
+    }
+}

Reply via email to