Commit de38696f authored by Eric Bruneton's avatar Eric Bruneton
Browse files

Add a CheckFrameAnalyzer class in asm-util. This will be used in...

Add a CheckFrameAnalyzer class in asm-util. This will be used in CheckClassAdapter to check frames if they are required and not computed with COMPUTE_FRAMES.
parent d51146fc
Pipeline #20601 passed with stage
in 6 minutes
......@@ -48,7 +48,7 @@ import org.objectweb.asm.tree.VarInsnNode;
* A semantic bytecode analyzer. <i>This class does not fully check that JSR and RET instructions
* are valid.</i>
*
* @param <V> type of the Value used for the analysis.
* @param <V> type of the {@link Value} used for the analysis.
* @author Eric Bruneton
*/
public class Analyzer<V extends Value> implements Opcodes {
......
// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holders nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.util;
import java.util.Collections;
import java.util.List;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TableSwitchInsnNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.Interpreter;
import org.objectweb.asm.tree.analysis.Value;
/**
* An {@link Analyzer} subclass which checks that methods provide stack map frames where expected
* (i.e. at jump target and after instructions without immediate successor), and that these stack
* map frames are valid (for the provided interpreter; they may still be invalid for the JVM, if the
* {@link Interpreter} uses a simplified type system compared to the JVM verifier).
*
* @author Eric Bruneton
* @param <V> type of the {@link Value} used for the analysis.
*/
class CheckFrameAnalyzer<V extends Value> extends Analyzer<V> {
/** The interpreter to use to symbolically interpret the bytecode instructions. */
private final Interpreter<V> interpreter;
/** The instructions of the currently analyzed method. */
private InsnList insnList;
/**
* The number of locals in the last stack map frame processed by {@link expandFrame}. Long and
* double values are represented with two elements.
*/
private int currentLocals;
CheckFrameAnalyzer(final Interpreter<V> interpreter) {
super(interpreter);
this.interpreter = interpreter;
}
@Override
protected void init(final String owner, final MethodNode method) throws AnalyzerException {
insnList = method.instructions;
currentLocals = Type.getArgumentsAndReturnSizes(method.desc) >> 2;
Frame<V>[] frames = getFrames();
Frame<V> currentFrame = frames[0];
expandFrames(owner, method, currentFrame);
for (int insnIndex = 0; insnIndex < insnList.size(); ++insnIndex) {
Frame<V> oldFrame = frames[insnIndex];
// Simulate the execution of this instruction.
AbstractInsnNode insnNode = null;
try {
insnNode = method.instructions.get(insnIndex);
int insnOpcode = insnNode.getOpcode();
int insnType = insnNode.getType();
if (insnType == AbstractInsnNode.LABEL
|| insnType == AbstractInsnNode.LINE
|| insnType == AbstractInsnNode.FRAME) {
checkFrame(insnIndex + 1, oldFrame, /* requireFrame = */ false);
} else {
currentFrame.init(oldFrame).execute(insnNode, interpreter);
if (insnNode instanceof JumpInsnNode) {
JumpInsnNode jumpInsn = (JumpInsnNode) insnNode;
if (insnOpcode != GOTO && insnOpcode != JSR) {
checkFrame(insnIndex + 1, currentFrame, /* requireFrame = */ false);
} else if (insnOpcode == GOTO) {
endControlFlow(insnIndex);
}
int jumpInsnIndex = insnList.indexOf(jumpInsn.label);
if (insnOpcode == JSR) {
throw new AnalyzerException(insnNode, "JSR instructions are unsupported");
} else {
checkFrame(jumpInsnIndex, currentFrame, /* requireFrame = */ true);
}
} else if (insnNode instanceof LookupSwitchInsnNode) {
LookupSwitchInsnNode lookupSwitchInsn = (LookupSwitchInsnNode) insnNode;
int targetInsnIndex = insnList.indexOf(lookupSwitchInsn.dflt);
checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
for (int i = 0; i < lookupSwitchInsn.labels.size(); ++i) {
LabelNode label = lookupSwitchInsn.labels.get(i);
targetInsnIndex = insnList.indexOf(label);
currentFrame.initJumpTarget(insnOpcode, label);
checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
}
endControlFlow(insnIndex);
} else if (insnNode instanceof TableSwitchInsnNode) {
TableSwitchInsnNode tableSwitchInsn = (TableSwitchInsnNode) insnNode;
int targetInsnIndex = insnList.indexOf(tableSwitchInsn.dflt);
currentFrame.initJumpTarget(insnOpcode, tableSwitchInsn.dflt);
checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
newControlFlowEdge(insnIndex, targetInsnIndex);
for (int i = 0; i < tableSwitchInsn.labels.size(); ++i) {
LabelNode label = tableSwitchInsn.labels.get(i);
currentFrame.initJumpTarget(insnOpcode, label);
targetInsnIndex = insnList.indexOf(label);
checkFrame(targetInsnIndex, currentFrame, /* requireFrame = */ true);
}
endControlFlow(insnIndex);
} else if (insnOpcode == RET) {
throw new AnalyzerException(insnNode, "RET instructions are unsupported");
} else if (insnOpcode != ATHROW && (insnOpcode < IRETURN || insnOpcode > RETURN)) {
checkFrame(insnIndex + 1, currentFrame, /* requireFrame = */ false);
} else {
endControlFlow(insnIndex);
}
}
List<TryCatchBlockNode> insnHandlers = getHandlers(insnIndex);
if (insnHandlers != null) {
for (TryCatchBlockNode tryCatchBlock : insnHandlers) {
Type catchType;
if (tryCatchBlock.type == null) {
catchType = Type.getObjectType("java/lang/Throwable");
} else {
catchType = Type.getObjectType(tryCatchBlock.type);
}
Frame<V> handler = newFrame(oldFrame);
handler.clearStack();
handler.push(interpreter.newExceptionValue(tryCatchBlock, handler, catchType));
checkFrame(insnList.indexOf(tryCatchBlock.handler), handler, /* requireFrame = */ true);
}
}
if (!hasNextJvmInsnOrFrame(insnIndex)) {
break;
}
} catch (AnalyzerException e) {
throw new AnalyzerException(
e.node, "Error at instruction " + insnIndex + ": " + e.getMessage(), e);
} catch (RuntimeException e) {
// DontCheck(IllegalCatch): can't be fixed, for backward compatibility.
throw new AnalyzerException(
insnNode, "Error at instruction " + insnIndex + ": " + e.getMessage(), e);
}
}
}
/**
* Expands the {@link FrameNode} "instructions" of the given method into {@link Frame} objects and
* stores them at the corresponding indices of the {@link #frames} array. The expanded frames are
* also associated with the label and line number nodes immediately preceding each frame node.
*
* @param owner the internal name of the class to which 'method' belongs.
* @param method the method whose frames must be expanded.
* @param initialFrame the implicit initial frame of 'method'.
* @throws AnalyzerException if the stack map frames of 'method', i.e. its FrameNode
* "instructions", are invalid.
*/
private void expandFrames(
final String owner, final MethodNode method, final Frame<V> initialFrame)
throws AnalyzerException {
int lastJvmOrFrameInsnIndex = -1;
Frame<V> currentFrame = initialFrame;
int currentInsnIndex = 0;
for (AbstractInsnNode insnNode : method.instructions) {
if (insnNode instanceof FrameNode) {
try {
currentFrame = expandFrame(owner, currentFrame, (FrameNode) insnNode);
} catch (AnalyzerException e) {
throw new AnalyzerException(
e.node, "Error at instruction " + currentInsnIndex + ": " + e.getMessage(), e);
}
for (int index = lastJvmOrFrameInsnIndex + 1; index <= currentInsnIndex; ++index) {
getFrames()[index] = currentFrame;
}
}
if (isJvmInsnNode(insnNode) || insnNode instanceof FrameNode) {
lastJvmOrFrameInsnIndex = currentInsnIndex;
}
currentInsnIndex += 1;
}
}
/**
* Returns the expanded representation of the given FrameNode.
*
* @param owner the internal name of the class to which 'frameNode' belongs.
* @param previousFrame the frame before 'frameNode', in expanded form.
* @param frameNode a possibly compressed stack map frame.
* @return the expanded version of 'frameNode'.
* @throws AnalyzerException if 'frameNode' is invalid.
*/
private Frame<V> expandFrame(
final String owner, final Frame<V> previousFrame, final FrameNode frameNode)
throws AnalyzerException {
Frame<V> frame = newFrame(previousFrame);
List<Object> locals = frameNode.local == null ? Collections.emptyList() : frameNode.local;
int currentLocal = currentLocals;
switch (frameNode.type) {
case Opcodes.F_NEW:
case Opcodes.F_FULL:
currentLocal = 0;
// fall through
case Opcodes.F_APPEND:
for (Object type : locals) {
V value = newFrameValue(owner, frameNode, type);
if (currentLocal + value.getSize() > frame.getLocals()) {
throw new AnalyzerException(frameNode, "Cannot append more locals than maxLocals");
}
frame.setLocal(currentLocal++, value);
if (value.getSize() == 2) {
frame.setLocal(currentLocal++, interpreter.newValue(null));
}
}
break;
case Opcodes.F_CHOP:
for (Object unusedType : locals) {
if (currentLocal <= 0) {
throw new AnalyzerException(frameNode, "Cannot chop more locals than defined");
}
if (currentLocal > 1 && frame.getLocal(currentLocal - 2).getSize() == 2) {
currentLocal -= 2;
} else {
currentLocal -= 1;
}
}
break;
case Opcodes.F_SAME:
case Opcodes.F_SAME1:
break;
default:
throw new AnalyzerException(frameNode, "Illegal frame type " + frameNode.type);
}
currentLocals = currentLocal;
while (currentLocal < frame.getLocals()) {
frame.setLocal(currentLocal++, interpreter.newValue(null));
}
List<Object> stack = frameNode.stack == null ? Collections.emptyList() : frameNode.stack;
frame.clearStack();
for (Object type : stack) {
frame.push(newFrameValue(owner, frameNode, type));
}
return frame;
}
/**
* Creates a new value that represents the given stack map frame type.
*
* @param owner the internal name of the class to which 'frameNode' belongs.
* @param frameNode the stack map frame to which 'type' belongs.
* @param type an Integer, String or LabelNode object representing a primitive, reference or
* uninitialized a stack map frame type, respectively. See {@link FrameNode}.
* @return a value that represents the given type.
* @throws AnalyzerException if 'type' is an invalid stack map frame type.
*/
private V newFrameValue(final String owner, final FrameNode frameNode, final Object type)
throws AnalyzerException {
if (type == Opcodes.TOP) {
return interpreter.newValue(null);
} else if (type == Opcodes.INTEGER) {
return interpreter.newValue(Type.INT_TYPE);
} else if (type == Opcodes.FLOAT) {
return interpreter.newValue(Type.FLOAT_TYPE);
} else if (type == Opcodes.LONG) {
return interpreter.newValue(Type.LONG_TYPE);
} else if (type == Opcodes.DOUBLE) {
return interpreter.newValue(Type.DOUBLE_TYPE);
} else if (type == Opcodes.NULL) {
return interpreter.newOperation(new InsnNode(Opcodes.ACONST_NULL));
} else if (type == Opcodes.UNINITIALIZED_THIS) {
return interpreter.newValue(Type.getObjectType(owner));
} else if (type instanceof String) {
return interpreter.newValue(Type.getObjectType((String) type));
} else if (type instanceof LabelNode) {
AbstractInsnNode referencedNode = (LabelNode) type;
while (referencedNode != null && !isJvmInsnNode(referencedNode)) {
referencedNode = referencedNode.getNext();
}
if (referencedNode == null || referencedNode.getOpcode() != Opcodes.NEW) {
throw new AnalyzerException(frameNode, "LabelNode does not designate a NEW instruction");
}
return interpreter.newValue(Type.getObjectType(((TypeInsnNode) referencedNode).desc));
}
throw new AnalyzerException(frameNode, "Illegal stack map frame value " + type);
}
/**
* Merges (or checks) the given frame into (or with respect to) the frame at the given
* instruction. If the frame or the subroutine at the given instruction index changes as a result
* of this merge, the instruction index is added to the list of instructions to process (if it is
* not already the case).
*
* @param insnIndex an instruction index.
* @param frame a frame. This frame is left unchanged by this method.
* @param requireFrame whether a frame must already exist or not in {@link #frames} at
* 'insnIndex'.
* @throws AnalyzerException if the frames have incompatible sizes or if the frame at 'insnIndex'
* is missing (if required) or not compatible with 'frame'.
*/
private void checkFrame(final int insnIndex, final Frame<V> frame, final boolean requireFrame)
throws AnalyzerException {
Frame<V> oldFrame = getFrames()[insnIndex];
if (oldFrame == null) {
if (requireFrame) {
throw new AnalyzerException(null, "Expected stack map frame at instruction " + insnIndex);
}
getFrames()[insnIndex] = newFrame(frame);
} else {
String error = checkMerge(frame, oldFrame);
if (error != null) {
throw new AnalyzerException(
null,
"Stack map frame incompatible with frame at instruction "
+ insnIndex
+ " ("
+ error
+ ")");
}
}
}
/**
* Checks that merging the two given frame would not produce any change, i.e. that the types in
* the source frame are super types of the corresponding types in the destination frame.
*
* @param srcFrame a source frame. This frame is left unchanged by this method.
* @param dstFrame a destination frame. This frame is left unchanged by this method.
* @return an error message if the frames have incompatible sizes, or if a type in the source
* frame is not a super type of the corresponding type in the destination frame. Returns
* {@literal null} otherwise.
*/
private String checkMerge(final Frame<V> srcFrame, final Frame<V> dstFrame) {
int numLocals = srcFrame.getLocals();
if (numLocals != dstFrame.getLocals()) {
throw new AssertionError();
}
for (int i = 0; i < numLocals; ++i) {
V v = interpreter.merge(srcFrame.getLocal(i), dstFrame.getLocal(i));
if (!v.equals(dstFrame.getLocal(i))) {
return "incompatible types at local "
+ i
+ ": "
+ srcFrame.getLocal(i)
+ " and "
+ dstFrame.getLocal(i);
}
}
int numStack = srcFrame.getStackSize();
if (numStack != dstFrame.getStackSize()) {
return "incompatible stack heights";
}
for (int i = 0; i < numStack; ++i) {
V v = interpreter.merge(srcFrame.getStack(i), dstFrame.getStack(i));
if (!v.equals(dstFrame.getStack(i))) {
return "incompatible types at stack item "
+ i
+ ": "
+ srcFrame.getStack(i)
+ " and "
+ dstFrame.getStack(i);
}
}
return null;
}
/**
* Ends the control flow graph at the given instruction.
*
* @param insnIndex an instruction index.
* @throws AnalyzerException if 'insnIndex' is not the last instruction and there is no frame at
* 'insnIndex' + 1 in {@link #getFrames}.
*/
private void endControlFlow(final int insnIndex) throws AnalyzerException {
if (hasNextJvmInsnOrFrame(insnIndex) && getFrames()[insnIndex + 1] == null) {
throw new AnalyzerException(
null, "Expected stack map frame at instruction " + (insnIndex + 1));
}
}
/**
* Returns true if the given instruction is followed by a JVM instruction or a by stack map frame.
*
* @param insnIndex an instruction index.
* @return true if 'insnIndex' is followed by a JVM instruction or a by stack map frame.
*/
private boolean hasNextJvmInsnOrFrame(final int insnIndex) {
AbstractInsnNode insn = insnList.get(insnIndex).getNext();
while (insn != null) {
if (isJvmInsnNode(insn) || insn instanceof FrameNode) {
return true;
}
insn = insn.getNext();
}
return false;
}
/**
* Returns true if the given instruction node corresponds to a real JVM instruction.
*
* @param insnNode an instruction node.
* @return true except for label, line number and stack map frame nodes.
*/
private static boolean isJvmInsnNode(final AbstractInsnNode insnNode) {
return insnNode.getOpcode() >= 0;
}
}
// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holders nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.util;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import java.util.ArrayList;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.test.AsmTest;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.BasicVerifier;
import org.objectweb.asm.tree.analysis.Frame;
/**
* Unit tests for {@link CheckFrameanalyzer}.
*
* @author Eric Bruneton
*/
class CheckFrameAnalyzerTest extends AsmTest implements Opcodes {
private static final String CLASS_NAME = "C";
// Labels used to generate test cases.
private final Label label0 = new Label();
@Test
void testAnalyze_invalidJsr() {
MethodNode methodNode = new MethodNodeBuilder().jsr(label0).label(label0).vreturn().build();
Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
String message = assertThrows(AnalyzerException.class, analyze).getMessage();
assertTrue(message.contains("Error at instruction 0: JSR instructions are unsupported"));
}
@Test
void testAnalyze_invalidRet() {
MethodNode methodNode = new MethodNodeBuilder().ret(0).vreturn().build();
Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
String message = assertThrows(AnalyzerException.class, analyze).getMessage();
assertTrue(message.contains("Error at instruction 0: RET instructions are unsupported"));
}
@Test
void testAnalyze_missingFrameAtJumpTarget() {
MethodNode methodNode =
new MethodNodeBuilder().iconst_0().ifne(label0).iconst_0().label(label0).vreturn().build();
Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
String message = assertThrows(AnalyzerException.class, analyze).getMessage();
assertTrue(
message.contains("Error at instruction 1: Expected stack map frame at instruction 3"));
}
@Test
void testAnalyze_missingFrameAfterGoto() {
MethodNode methodNode =
new MethodNodeBuilder().nop().go(label0).nop().label(label0).vreturn().build();
Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
String message = assertThrows(AnalyzerException.class, analyze).getMessage();
assertTrue(
message.contains("Error at instruction 1: Expected stack map frame at instruction 2"));
}
@Test
void testAnalyze_illegalFrameType() {
MethodNode methodNode =
new MethodNodeBuilder()
.nop()
.go(label0)
.frame(123456, null, null)
.nop()
.label(label0)
.vreturn()
.build();
Executable analyze = () -> newAnalyzer().analyze(CLASS_NAME, methodNode);
String message = assertThrows(AnalyzerException.class, analyze).getMessage();