-
Bug
-
Resolution: Cannot Reproduce
-
P4
-
None
-
21
-
generic
-
generic
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 ----------
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 ----------