No more Java Double Trouble
Lately there has been quite of rumour in the Java community about the endless loop caused by a certain range of values. Any java application parsing floating point values was vulnerable, especially dynamic languages.
Double.parseDouble(s) -> SafeDoubleParser.parseDouble(s)
Double.valueOf(s) -> SafeDoubleParser.valueOf(s)
new Float(s) -> SafeFloatParser.valueOf(s)
Float.parseFloat(s) -> SafeFloatParser.parseFloat(s)
Float.valueOf(s) -> SafeFloatParser.valueOf(s)
bd.doubleValue() -> SafeDoubleParser.doubleValue(bd)
bd.floatValue() -> SafeFloatParser.floatValue(bd)
Now the good news is that Oracle has fixed the issue!
Disclaimer
Only read on if your application needs to run on older, unpatched Java versions.
Analysis
- new Double(String)
- Double.valueOf(String)
- Double.parseDouble(String)
- new Float(String)
- Float.parseFloat(String)
- Float.valueOf(String)
- BigDecimal.doubleValue()
- BigDecimal.floatValue()
These calls are only blocking for a small interval [2.2250738585072011e-308, 2.2250738585072013e-308] of String input values. In fact the interval is even smaller.
Solution
There is a very pragmatic workaround to this problem:
On parsing, use a quite fast String analysis to find out if the input is suspicious. A suspicious value contains the digit sequence "22250738585072". In this case, use the slower BigDecimal parsing to determine if the value really is inside the dangerous interval. Dangerous values are rounded to the most nearby border of the interval, which is a safe value. For neither suspicious nor dangerous values, the standard Java floating decimal parsing is used.
The code looks like:
On parsing, use a quite fast String analysis to find out if the input is suspicious. A suspicious value contains the digit sequence "22250738585072". In this case, use the slower BigDecimal parsing to determine if the value really is inside the dangerous interval. Dangerous values are rounded to the most nearby border of the interval, which is a safe value. For neither suspicious nor dangerous values, the standard Java floating decimal parsing is used.
The code looks like:
final protected static Double decimalValueOf(String s) {
Double result = null;
if (s != null) {
if (isSuspicious(s)) {
// take the slow path
result = parseSafely(s);
} else {
result = Double.valueOf(s);
}
}
return result;
}
final private static Double parseSafely(String s) {
Double result;
BigDecimal bd = new BigDecimal(s);
if (bd.compareTo(UPPER) <> 0) {
if (bd.compareTo(MIDDLE) >= 0) {
result = UPPER_DOUBLE;
} else {
result = LOWER_DOUBLE;
}
} else {
result = Double.valueOf(s);
}
return result;
}
For the BigDecimal.doubleValue() case, we need to make sure that we do not feed dangerous values into the standard Java floating decimal parsing:
final protected static double decimalValue(BigDecimal bigDecimal) {What you see here is the heart of the SafeDecimalParser class. For real usage, there are two convenience classes: SafeDoubleParser and SafeFloatParser. The following table lists the original calls, and how you would replace them:
double result = 0.0;
if (bigDecimal != null) {
if (bigDecimal.compareTo(UPPER) <> 0) {
result = decimalValueOf(bigDecimal.toString()).doubleValue();
} else {
result = bigDecimal.doubleValue();
}
}
return result;
}
String s; BigDecimal bd;
new Double(s) -> SafeDoubleParser.valueOf(s)Double.parseDouble(s) -> SafeDoubleParser.parseDouble(s)
Double.valueOf(s) -> SafeDoubleParser.valueOf(s)
new Float(s) -> SafeFloatParser.valueOf(s)
Float.parseFloat(s) -> SafeFloatParser.parseFloat(s)
Float.valueOf(s) -> SafeFloatParser.valueOf(s)
bd.doubleValue() -> SafeDoubleParser.doubleValue(bd)
bd.floatValue() -> SafeFloatParser.floatValue(bd)
The DoubleTroubleTestSuite.jar contains all the sources for the safe parsers. Running java -jar DoubleTroubleTestSuite.jar executes all the junit tests on your favourite platform. I successfully tested it on:
- Mac OS X 10.5.8
- Windows 7
- Ubuntu 10.4
- Ubuntu 10.10
- Open Solaris
Downsides
- The small performance impact for the detection of suspicious values
- Some values are not exact, however the maximum error is less than 6.2e-325
Nevertheless, this hopefully might be useful.
You find the code inside the .jar file, besides the .class files in the org.python.util package.
It is under the Python Software Foundation License, which means you can freely use and redistribute it.
Happy coding!