Analyzer with a SimpleVerifier may throw an AnalyzerException on valid java code due to incompatible frame locals
A bug was introduced in commit c3f9957e, where some valid Java code will fail verification with a SimpleVerifier
.
Expected Behavior
Up until ASM 9.6, any such code would pass verification with a SimpleVerifier
with no exceptions thrown.
Current Behavior
Since ASM 9.7, more specifically the commit linked above, some methods with valid bytecode generated by javac
will throw an AnalyzerException
with a reason such as Method owner: expected Ljava/lang/Throwable;, but found Ljava/lang/Object;
Steps to Reproduce
I managed to narrow down the code to reproduce the issue down to this, though at a bytecode level less than half of the compiled instructions are needed:
public Path test(Path p, List<Path> paths) {
Path resolved;
try {
int i = 0;
String s = "foo";
resolved = Files.createDirectory(p.resolve(s));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
try (PrintWriter w = new PrintWriter(System.out)) {
paths.forEach(w::println);
} finally {
for (Path path : paths) {
System.out.println(path);
}
}
return resolved;
}
To reproduce the issue:
- Compile the code above, I used Java 21 (`21.0.2+13-Ubuntu-122.04.1`) though it's likely an issue with earlier versions too
- Read the method into a
MethodNode
- Create a
new Analyzer<>(new SimpleVerifier())
- Verify the method with the created
Analyzer
. - An
AnalyzerException
will be thrown onAnalyzer#analyze
The problem
I spent some hours debugging this issue to find the cause of it. Since commit c3f9957e Analyzer
s will merge the frames before and after an instruction execution into the hanlder of a try-catch block (before this commit just the old frame would be merged). The frame state after execution is calculated on the variable currentFrame
of #analyze
, and during the processing of pseudo-instructions (frames, labels and line numbers), this variable will not be modified.
The problem is that, since currentFrame
is not updated for pseudo-instructions, it's state is the same as the last executable instruction. If the analyzer took a diverging path (ie. a different variable type in the same local slot) to reach the instruction analyzed before, and the new pseudo-instruction is inside a different try-catch block, the different local type will be merged into the try-catch handler, even though it came from a different execution path which would not be reached until later. If the types from this "left-over" frame and the correct frames are incompatible, the final type will be replaced by java/lang/Object
. As such, invoking any method using this variable is likely to fail due to mismatched types.