Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8364789

javax.swing.text.html.parser.ParserDelegator.parse() is not reentrant

XMLWordPrintable

      A DESCRIPTION OF THE PROBLEM :
      When using multiple instances of ParserDelegator and calling their parse() method from multiple threads, the internal DTD object instance can enter a broken state.

      From my analysis this looks like it's originally caused by javax.swing.text.html.parser.ContentModel not being
      thread-safe.
      ContentModel contains a cache, which is lazily initialized in ContentModel.first(). If the initalization code is triggered
      from multiple threads in, this can result in ContentModel having a broken state.

      The relevant code is in https://github.com/openjdk/jdk/blob/master/src/java.desktop/share/classes/javax/swing/text/html/parser/ContentModel.java#L195-L209.


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      See the attached code. It needs to be run with --add-opens java.desktop/javax.swing.text.html.parser=ALL-UNNAMED so we can simulate the code from ParserDelegator .

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      It should run forever without errors
      ACTUAL -
      Observe the output of the program, on my machine it enters broken state after less than 100 runs.

      ---------- BEGIN SOURCE ----------
      // important: this needs to be run with --add-opens java.desktop/javax.swing.text.html.parser=ALL-UNNAMED

      import javax.swing.text.MutableAttributeSet;
      import javax.swing.text.html.HTML;
      import javax.swing.text.html.HTMLEditorKit;
      import javax.swing.text.html.parser.DTD;
      import javax.swing.text.html.parser.DocumentParser;
      import javax.swing.text.html.parser.ParserDelegator;
      import java.io.*;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.concurrent.BrokenBarrierException;
      import java.util.concurrent.CyclicBarrier;

      public class Main {
      static class DtdRef {
      volatile DTD dtd = null;
      }

      static class HtmlCallback extends HTMLEditorKit.ParserCallback {
      private StringBuilder buf = new StringBuilder();

      public String getHtml() {
      return buf.toString();
      }

      @Override public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) {
      buf.append("<" + t.toString() + ">");
      }

      @Override public void handleText(char[] data, int pos) {
      buf.append(data);
      }

      @Override public void handleEndTag(HTML.Tag t, int pos) {
      buf.append("</" + t.toString() + ">");
      }
      }

      static class Worker extends Thread {
      final CyclicBarrier finished;
      final CyclicBarrier start;
      final DtdRef dtdRef;
      volatile boolean success = true;

      Worker(CyclicBarrier start, CyclicBarrier finished, DtdRef dtdRef) {
      this.start = start;
      this.finished = finished;
      this.dtdRef = dtdRef;
      }
      public void run() {
      try {
      while (true) {
      start.await();
      // code from javax.swing.text.html.parser.ParserDelegator.parse(), except we use our custom DTD
      // instance
      String inputHtml = "<p>outer<span>inner</span></p>";
      HtmlCallback callback = new HtmlCallback();
      try {
      new DocumentParser(dtdRef.dtd).parse(new StringReader(inputHtml), callback, true);
      success = callback.getHtml().equals("<html><head></head><body><p>outer<span>inner</span></p></body></html>");
      } catch (NullPointerException e) {
      success = false;
      }
      finished.await();
      }
      } catch (InterruptedException | BrokenBarrierException e) {
      // clean shutdown
      } catch (IOException e) {
      System.err.println(e.getMessage());
      System.err.println(e.getStackTrace());
      }
      }
      }
      public static void main(String[] args) throws IOException, BrokenBarrierException, InterruptedException {
      String inputHtml = "<p>outer<span>inner</span></p>";
      HtmlCallback callback = new HtmlCallback();
      ParserDelegator parser = new ParserDelegator();
      parser.parse(new StringReader(inputHtml), callback, true);


      final int threadCount = 10;
      final CyclicBarrier start = new CyclicBarrier(threadCount + 1);
      final CyclicBarrier finished = new CyclicBarrier(threadCount + 1);
      final DtdRef dtdRef = new DtdRef();
      List<Worker> workers = new ArrayList<>();
      for (int i = 0; i < threadCount; i++) {
      Worker worker = new Worker(start, finished, dtdRef);
      worker.start();
      workers.add(worker);
      }

      boolean triggeredBug = false;
      int runs = 0;
      while (!triggeredBug) {
      dtdRef.dtd = createDtd();
      start.await();
      finished.await();
      triggeredBug = workers.stream().anyMatch((worker) -> !worker.success);
      ++runs;
      System.out.println("Runs: " + runs + ". Triggered bug: " + triggeredBug);
      }
      for (Worker worker : workers) {
      worker.interrupt();
      }
      }

      public static DTD createDtd() throws IOException {
      // logic from javax.swing.text.html.parser.ParserDelegator to initialize the DTD
      String dtdName = "html32";
      DTD instance = DTD.getDTD(dtdName);
      try(InputStream in = ParserDelegator.class.getResourceAsStream(dtdName + ".bdtd")) {
      if (in != null) {
      instance.read(new DataInputStream(new BufferedInputStream(in)));
      instance.putDTDHash(dtdName, instance);
      }
      } catch (IOException e) {
      throw new RuntimeException(e);
      }
      return instance;
      }
      }

      ---------- END SOURCE ----------

            Unassigned Unassigned
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: