8173111: Excessive recursion in EventFilterSupport when filtering over large number of XML events can cause StackOverflow
authordfuchs
Tue, 24 Jan 2017 16:34:23 +0000
changeset 43356 5258cc06c4f4
parent 43355 f8afc2f71a27
child 43357 ef3ea67a2257
8173111: Excessive recursion in EventFilterSupport when filtering over large number of XML events can cause StackOverflow Summary: This fix replaces un unwanted recursion in the XML streams event filtering support by a simple loop (in nextEvent/nextTag). Reviewed-by: aefimov, clanger, lancea, rriggs
jaxp/src/java.xml/share/classes/com/sun/xml/internal/stream/EventFilterSupport.java
jaxp/test/javax/xml/jaxp/unittest/stream/EventsTest/EventFilterSupportTest.java
--- a/jaxp/src/java.xml/share/classes/com/sun/xml/internal/stream/EventFilterSupport.java	Tue Jan 24 11:10:19 2017 +0100
+++ b/jaxp/src/java.xml/share/classes/com/sun/xml/internal/stream/EventFilterSupport.java	Tue Jan 24 16:34:23 2017 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 2017, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -64,7 +64,7 @@
     }
 
     public XMLEvent nextEvent()throws XMLStreamException{
-        if(super.hasNext()){
+        while (super.hasNext()) {
             //get the next event by calling XMLEventReader
             XMLEvent event = super.nextEvent();
 
@@ -72,27 +72,19 @@
             if(fEventFilter.accept(event)){
                 return event;
             }
-            else{
-                return nextEvent();
-            }
-        }else{
-            throw new NoSuchElementException();
         }
+        throw new NoSuchElementException();
     }//nextEvent()
 
      public XMLEvent nextTag() throws XMLStreamException{
-         if(super.hasNext()){
+         while (super.hasNext()) {
              XMLEvent event = super.nextTag();
              //if the filter accepts this event return this event.
              if(fEventFilter.accept(event)){
                 return event;
              }
-             else{
-                return nextTag();
-             }
-         }else{
-             throw new NoSuchElementException();
          }
+         throw new NoSuchElementException();
      }
 
      public XMLEvent peek() throws XMLStreamException{
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jaxp/test/javax/xml/jaxp/unittest/stream/EventsTest/EventFilterSupportTest.java	Tue Jan 24 16:34:23 2017 +0000
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package stream.EventsTest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import javax.xml.stream.EventFilter;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import org.testng.annotations.Test;
+import static org.testng.Assert.assertEquals;
+
+/**
+ * @test
+ * @bug 8173111
+ * @summary tests that filtering out nested elements doesn't end up in
+ *          a StackOverflowException
+ * @run testng/othervm stream.EventsTest.EventFilterSupportTest
+ * @author danielfuchs
+ */
+public class EventFilterSupportTest {
+    static final String ROOT = "xml";
+    static final String NEXT = "foo";
+    static final String SMOKE = "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>"
+            + "<xml><foo><foo><foo></foo></foo></foo></xml>";
+    // A number high enough to trigger StackOverflowException before the fix.
+    static final int MAX = 100_000;
+
+    public static void main(String[] args)
+            throws XMLStreamException, IOException {
+        smokeTest();
+        testNextEvent(MAX);
+        testNextTag(MAX);
+        System.out.println("Tests passed...");
+    }
+
+    // The smoke test just verifies that our TestInputStream works as
+    // expected and produces the expected stream of characters.
+    // Here we test it with 4 nested elements.
+    @Test
+    public static void smokeTest() throws IOException {
+        System.out.println("\nSmoke test...");
+        StringBuilder sb = new StringBuilder();
+        try (InputStream ts = new TestInputStream(4)) {
+            int c;
+            while ((c = ts.read()) != -1) {
+                System.out.print((char)c);
+                sb.append((char)c);
+            }
+        }
+        assertEquals(sb.toString(), SMOKE, "Smoke test failed");
+        System.out.println("\nSmoke test passed\n");
+    }
+
+    // Test calling XMLEventReader.nextEvent()
+    @Test
+    public static void testNextEvent() throws IOException, XMLStreamException {
+        testNextEvent(MAX);
+    }
+
+    // Without the fix, will cause a StackOverflowException if 'max' is high
+    // enough
+    private static void testNextEvent(int max)
+            throws IOException, XMLStreamException {
+        System.out.println("\nTest nextEvent (" + max + ")...");
+        XMLEventReader reader = createXmlReader(max);
+        XMLEvent event;
+        do {
+            event = reader.nextEvent();
+            System.out.println(event);
+        } while (event.getEventType() != XMLEvent.END_DOCUMENT);
+        System.out.println("nextEvent passed\n");
+    }
+
+    // Test calling XMLEventReader.nextTag()
+    @Test
+    public static void testNextTag() throws IOException, XMLStreamException {
+        testNextTag(MAX);
+    }
+
+    // Without the fix, will cause a StackOverflowException if 'max' is high
+    // enough
+    private static void testNextTag(int max)
+            throws IOException, XMLStreamException {
+        System.out.println("\nTest nextTag (" + max + ")...");
+        XMLEventReader reader = createXmlReader(max);
+        XMLEvent event;
+        do {
+            event = reader.nextTag();
+            System.out.println(event);
+            if (event.getEventType() == XMLEvent.END_ELEMENT
+                && event.asEndElement().getName().getLocalPart().equals(ROOT)) {
+                break;
+            }
+        } while (true);
+        System.out.println("nextTag passed\n");
+    }
+
+    private static XMLEventReader createXmlReader(int max)
+            throws XMLStreamException {
+        TestInputStream ts = new TestInputStream(max);
+        XMLInputFactory xif = XMLInputFactory.newInstance();
+        XMLEventReader reader = xif.createXMLEventReader(ts);
+        return xif.createFilteredReader(reader, new TagFilter(max));
+    }
+
+    // An input stream that pretends to contain 'max - 1' nested NEXT tags
+    // within a ROOT element:
+    // <?xml version="1.0" encoding="US-ASCII"?>
+    // <ROOT><NEXT><NEXT>...</NEXT></NEXT></ROOT>
+    // (1 ROOT element + max-1 nested NEXT elements)
+    public static class TestInputStream extends InputStream {
+
+        int open = 0;
+        int i = 0;
+        int n = 0;
+        final int max;
+
+        public TestInputStream(int max) {
+            this.max = max;
+        }
+
+        String tag() {
+            if (n == 0) {
+                // opening first element - includes the XML processing instruction.
+                return "?xml version=\"1.0\" encoding=\"US-ASCII\"?><" + ROOT;
+            }
+            if (n == 2 * max -1) {
+                // closing the first element
+                // we have 'max' opening tags (0..max-1) followed by
+                // 'max' closing tags (max..2*max-1)
+                // for n in [0..max-1] we open the tags,
+                // for n in [max..2*max-1] we close them (in reverse order)
+                return ROOT;
+            }
+            // everything between [1..max-2] is a NEXT element tag (opening
+            // or closing)
+            return NEXT;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (n >= 2 * max) return -1;
+            if (open == 0) {
+                open = 1;
+                return '<';
+            }
+            if (open == 1 && n >= max) {
+                // we have opened the ROOT element + n-1 nested NEXT elements,
+                // so now we need to start closing them all in reverse order.
+                open = 2;
+                return '/';
+            }
+            String tag = tag();
+            if (open > 0 && i < tag.length()) {
+                return tag.charAt(i++);
+            }
+            if (open > 0 && i == tag.length()) {
+                open = 0; i = 0; n++;
+                return '>';
+            }
+            return -1;
+        }
+    }
+
+    public static final class TagFilter implements EventFilter {
+        int count;
+        final int max;
+
+        public TagFilter(int max) {
+            this.max = max;
+        }
+
+        // Filters everything except the ROOT element.
+        @Override
+        public boolean accept(XMLEvent event) {
+            int type = event.getEventType();
+            if (type == XMLEvent.START_ELEMENT) {
+                String loc = event.asStartElement().getName().getLocalPart();
+                if (count == 0 || count == 1) System.out.println("<" + loc + ">");
+                count++;
+                return ROOT.equals(loc);
+            }
+            if (type == XMLEvent.END_ELEMENT) {
+                if (count == max) System.out.println("Got " + count + " elements");
+                String loc = event.asEndElement().getName().getLocalPart();
+                count--;
+                if (count == 0 || count == 1) System.out.println("</" + loc + ">");
+                return ROOT.equals(loc);
+            }
+            if (type == XMLEvent.PROCESSING_INSTRUCTION) return true;
+            if (type == XMLEvent.START_DOCUMENT) return true;
+            if (type == XMLEvent.END_DOCUMENT) return true;
+            return false;
+        }
+    }
+
+}