This special case is an issue with Exception handlers dealing with a particular conflict case differently than everywhere else. Most Exception handlers start with an astore instruction that puts the Exception class onto the LocalVariables array over top of an existing type. Normally, a circumstance like that would be a conflict, but Sun's preverifier seems to resolve it by setting the type to a java.lang.Object type rather than null.
A solution is in place for this issue, but at least one case has been found where it makes an otherwise passing test case fail.
Package | Class | Method | Method # | Effect | Source | com.markcrocker.purifier.testcases | ProblemCases | exceptionHandlerObjectConflict() | 6 | Would Fail without fix | Purifier1 project |
---|---|---|---|---|---|
nullObjectNeverInit() | 7 | Won't fail unless fix is faulty | |||
nestedExceptionHandlersConflict(Integer[]) | 11 | Fails partly because of fix! | |||
UniqueProblemCases | nestedExceptionHandlersConflictWithUnknown(Integer[]) | 6 | |||
catchesNestedExceptions(Integer[]) | 7 | ||||
ExceptionCases | 13 | examples.stock | StockMidlet | viewAlerts() | 14 | Wireless Toolkit 1.04 |
checkAlerts(String) | 8 | ||||
calc() | 4 | ||||
destroyApp(boolean) | 3 | ||||
StockMidlet$StockRefreshTask | run() | 1 | |||
example.http | HttpTest | readHeaders(String) | 7 |
This case involves an Exception handler that starts with an astore instruction that puts the Exception class onto the LocalVariables array in the same slot as an existing type from another, competing branch source. Normally, when the Frame is pushed onto the BranchStack the LocalVariables.resolveConflicts method detects a conflict between the two types and sets the slot to null. The resolveConflicts method does this because it assumes that when two different Frames from different branch sources that lead to the same branch target have a LocalVariables slot that holds incompatible types, then the types represent junk data that can be discarded. However, Sun's preverifier seems to resolve this particular case by setting the type to a java.lang.Object type rather than null. It only does this if both LocalVariables slots hold ITEM_Object entries. If either is of any other type, then it reverts to the standard behaviour, which is to set the entry to null.
ProblemCases.exceptionHandlerObjectConflict provides an example of this issue and is shown below.
1: public void exceptionHandlerObjectConflict() { 2: try { 3: Integer[] eye = new Integer[5]; 4: eye[9] = new Integer(13); 5: } catch (ArrayIndexOutOfBoundsException aioob ) { 6: } 7: }
The following byte code is from the compiled and preverified class file created from the source in the previous section. Unrelated attributes have been removed for brevity and the StackMap reformatted for readability.
0: iconst_5 1: anewarray <java.lang.Integer> (14) 4: astore_1 5: aload_1 6: bipush 9 8: new <java.lang.Integer> (14) 11: dup 12: bipush 13 14: invokespecial java.lang.Integer.<init> (I)V (15) 17: aastore 18: goto #22 21: astore_1 22: return Exception handler(s) = From To Handler Type 0 18 21 java.lang.ArrayIndexOutOfBoundsException(13)
At offset 22, there is a conflict between the ArrayIndexOutOfBoundsException which is put into LocalVariables slot 1 by the astore_1 instruction at offset 21 and the Integer array that is stored in the same slot by the astore_1 instruction at offset 4.
These Frames conflict because the branch source from the goto instruction at offset 18 and the fall through from the instruction at offset 21 both lead to the return instruction at offset 22. The first branch source has an Integer array pushed onto the stack by the anewarray instruction at offset 1 and stored on the local variables at offset 4. At the goto branch instruction at offset 18, the Frame is:
Local Variables: 0: (type=Object, class=com.markcrocker.purifier.testcases.ProblemCases) 1: (type=Object, class=[Ljava.lang.Integer;) OperandStack: Slots used: 0 MaxStack: 5.
The second branch source is an exception handler, which starts at offset 21 and is automatically initialized with the ArrayIndexOutOfBoundsException on the stack. This type is then stored in the same slot as the Integer array from the last branch by the astore_1 instruction at offset 21, which falls through to instruction 22. Resulting in a Frame of:
Local Variables: 0: (type=Object, class=com.markcrocker.purifier.testcases.ProblemCases) 1: (type=Object, class=java.lang.ArrayIndexOutOfBoundsException) OperandStack: Slots used: 0 MaxStack: 5.
Normally, the LocalVariables.resolveConflicts method, which is invoked at the return instruction at offset 22, would see the [Ljava.lang.Integer; and java.lang.ArrayIndexOutOfBoundsException in LocalVariables slot 1 as incompatible and would set the slot to null. In this case, however, the slot is set to java.lang.Object. The resulting StackMap is:
StackMap( ( offset = 21, locals = { (type=Object, (20)class=com.markcrocker.purifier.testcases.ProblemCases) }, stack items = { (type=Object, (13)class=java.lang.ArrayIndexOutOfBoundsException) } ) ( offset = 22, locals = { (type=Object, (20)class=com.markcrocker.purifier.testcases.ProblemCases), (type=Object, (104)class=java.lang.Object) } ) )
Note that despite the locals at offset 22 have a java.lang.Object in the second record.
The ProblemCases.exceptionHandlerPrimitiveConflict test case tests an opposing situation where the conflict really should be resolved by setting the LocalVariables slot to null. This helps ensure that the solutions to this problem don't remove ITEM_Nulls when they actually do belong.
One case that shows where the solution breaks down is UniqueProblemCases.nestedExceptionHandlersConflictWithUnknown(Integer[]), where the expected StackMap entry at offset 24 is:
offset = 24, locals = { (type=Object, (17)class=com.markcrocker.purifier.testcases.UniqueProblemCases), (type=Object, (85)class=[Ljava.lang.Integer;), (type=Integer), (type=Object, (15)class=java.lang.Integer), (type=Object, (86)class=java.lang.Object) }
However, the Purifier1 produces:
offset = 24, locals = { (type=Object, (17)class=com.markcrocker.purifier.testcases.UniqueProblemCases), (type=Object, (85)class=[Ljava.lang.Integer;), (type=Integer), (type=Object, (86)class=java.lang.Object), (type=Object, (12)class=java.lang.NullPointerException) }
This shows that sometimes the Purifier1 misses cases that it should catch and also catches some that should be left alone.
A LocalVariables.setAstoreOverride method and supporting methods and fields have been added so that the LocalVariables has a state indicating that a particular LocalVariables slot should resolve any conflicts between ITEM_Object types as java.lang.Object instead of null. Some detection code has been added to the Data FLow Analysis code of the StackMapGen.generateStackMap method to use the setAstoreOverride of the current Frame whenever an astore instruction in first instruction of an Exception handler is found. Unfortunately, a case has been found where this actually causes an otherwise passing test case to fail, so this solution is not adequate.