ADDITIONAL SYSTEM INFORMATION :
Reproduced on Mac OS with both Java 11 and Java 15
A DESCRIPTION OF THE PROBLEM :
We observed that the reverse() method of the org.apache.commons.lang.StringUtils class contained in Apache commons-lang version 2.6 started returning incorrect results after thousands of iterations.
The reason we suspect an issue with compact strings and the optimizer is because:
1. The bug only reproduces with strings which can be represented compactly (all code points < 256)
2. The bug only reproduces after calling the method in a loop for an indeterminate number of times (the range we've seen is from a minimum of around 50K iterations to a maximum of around 135K iterations).
3. The bug does not reproduce when compact strings are disabled via -XX:-CompactStrings
4. The bug does not reproduce when the optimizer is disabled via -Djava.compiler=NONE
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached source code. A couple of ways you can reproduce it:
1. Clone the repository I created at https://github.com/jyemin/compact-strings-bug and execute "./gradlew run".
2. Manually download commons-lang jar file from https://repo1.maven.org/maven2/commons-lang/commons-lang/2.6/ and compile the attached source code with this jar in your classpath
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Expected that the following would be printed:
Original string: 123456
Expected reversed string: 654321
Completed normally. Exiting.
ACTUAL -
Instead, the following is printed:
Original string: 123456
Expected reversed string: 654321
Iteration: 118306
Actual: 654326
Iteration: 118307
Actual: 654326
Iteration: 118308
Actual: 654326
Iteration: 118309
Actual: 654326
Iteration: 118310
Actual: 654326
Exiting after 5 failures
A few additional observations:
1. The number of iterations before it starts returning incorrect results varies, but once it starts behaving incorrectly, every subsequent call behaves incorrectly, in exactly the same way
2. It also seems to be the case that there is a single incorrect character in the reversed string. It's always the last character, and instead of being the expected first character from the original string, it's the last character from the original string.
---------- BEGIN SOURCE ----------
import org.apache.commons.lang.StringUtils;
public class CompactStringBug {
protected static final int MAX_FAILURES = 5;
public static void main(String[] args) {
final String id = "123456";
final String expectedReversedId = "654321";
/*
The bug does not reproduce if you include a multibyte character in the string
*/
// final String id = "12345\u1234";
// final String expectedReversedId = "\u123454321";
System.out.println("Original string: " + id);
System.out.println("Expected reversed string: " + expectedReversedId);
System.out.println();
int failures = 0;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String reversedId = StringUtils.reverse(id);
if (!expectedReversedId.equals(reversedId)) {
failures++;
System.out.println("Iteration: " + i);
System.out.println("Actual: " + reversedId);
System.out.println();
if (failures == MAX_FAILURES) {
System.out.println("Exiting after " + MAX_FAILURES + " failures");
System.exit(1);
}
}
}
System.out.println("Completed normally. Exiting.");
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
We've found a number of workarounds. The most straightforward is to replace use of commons-lang StringUtils with java.lang.StringBuilder#reverse.
These are some other ways we've worked around the bug during analysis, but only the first is practicable in production:
1. Disable compact strings, e.g. by starting java with -XX:-CompactStrings
2. Disable the optimizer, e.g. by starting java with -Djava.compiler=NONE
3. Ensure that at least one character in the string is multibyte (i.e. code point > 255)
FREQUENCY : always