// Copyright (c) 2011, David J. Pearce (djp@ecs.vuw.ac.nz)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of the <organization> 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 DAVID J. PEARCE 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 jasm.verifier;
import jasm.attributes.Code;
import jasm.attributes.StackMapTable;
import jasm.lang.Bytecode;
import jasm.lang.ClassFile;
import jasm.lang.JvmType;
import jasm.lang.JvmTypes;
import jasm.lang.Bytecode.ArrayLength;
import jasm.lang.Bytecode.ArrayLoad;
import jasm.lang.Bytecode.ArrayStore;
import jasm.lang.Bytecode.BinOp;
import jasm.lang.Bytecode.CheckCast;
import jasm.lang.Bytecode.Cmp;
import jasm.lang.Bytecode.Conversion;
import jasm.lang.Bytecode.Dup;
import jasm.lang.Bytecode.DupX1;
import jasm.lang.Bytecode.DupX2;
import jasm.lang.Bytecode.GetField;
import jasm.lang.Bytecode.If;
import jasm.lang.Bytecode.IfCmp;
import jasm.lang.Bytecode.Iinc;
import jasm.lang.Bytecode.InstanceOf;
import jasm.lang.Bytecode.Invoke;
import jasm.lang.Bytecode.Load;
import jasm.lang.Bytecode.LoadConst;
import jasm.lang.Bytecode.MonitorEnter;
import jasm.lang.Bytecode.MonitorExit;
import jasm.lang.Bytecode.Neg;
import jasm.lang.Bytecode.Nop;
import jasm.lang.Bytecode.Pop;
import jasm.lang.Bytecode.PutField;
import jasm.lang.Bytecode.Return;
import jasm.lang.Bytecode.Swap;
import jasm.lang.Bytecode.Throw;
import jasm.lang.ClassFile.Method;
import jasm.util.dfa.ForwardFlowAnalysis;
import java.util.List;
/**
* <p>
* A forward flow analysis which determines the type of each variable and stack
* location in a given <code>ClassFile.Method</code>. In the case of a method
* which is not well-typed, a verification error is reported. For valid methods,
* a <code>StackMapTable</code> attribute is added to the <code>Code</code>
* attribute (and any existing one replaced).
* </p>
*
* @author David J. Pearce
*
*/
public class TypeAnalysis extends ForwardFlowAnalysis<TypeAnalysis.Store>{
private ClassFile clazz; // currently being analysed
private ClassFile.Method method; // currently being analysed
/**
* Apply the analysis to every method in a classfile, creating the necessary
* <code>StackMapTable</code> attributes.
*
* @param cf
*/
public void apply(ClassFile cf) {
this.clazz = cf;
for (ClassFile.Method method : cf.methods()) {
// FIXME: this feels like a hack.
if(method.attribute(Code.class) != null) {
Store[] stores = apply(method);
addStackMapTable(method,stores);
}
}
}
/**
* Apply the analysis to a given method in a classfile.
*
* @param method
* The method to apply analysis to
*/
public Store[] apply(ClassFile.Method method) {
this.method = method;
return super.apply(method);
}
private void addStackMapTable(ClassFile.Method method, Store[] stores) {
Code attr = method.attribute(Code.class);
if (attr == null) {
// sanity check
throw new IllegalArgumentException(
"cannot apply forward flow analysis on method without code attribute");
}
StackMapTable existing = attr.attribute(StackMapTable.class);
if(existing != null) {
attr.attributes().remove(existing);
}
StackMapTable.Frame[] frames = new StackMapTable.Frame[stores.length];
for (int i = 0; i != frames.length; ++i) {
Store store = stores[i];
if(store != null) {
frames[i] = new StackMapTable.Frame(store.maxLocals, store.stack - store.maxLocals,
store.types);
} else {
// dead code
}
}
attr.attributes().add(new StackMapTable(frames));
}
@Override
public Store[] initialise(Code attr, Method method) {
// First, create the initial store from the parameter types.
List<JvmType> paramTypes = method.type().parameterTypes();
JvmType[] types = new JvmType[attr.maxLocals() + attr.maxStack()];
int index = 0;
if(!method.isStatic()) {
// Non-static methods have receiver at index 0
types[index++] = clazz.type();
}
// All methods have parameters in the local variable array on entry.
for (JvmType t : paramTypes) {
// Normalise types because of the discrepancy between declared
// types, and the types considered in the local variable array (i.e.
// declared type bool is int in local varaible array).
types[index] = normalise(t);
if (t instanceof JvmType.Long || t instanceof JvmType.Double) {
// for some reason, longs and doubles occupy two slots.
index = index + 2;
} else {
index = index + 1;
}
}
// set all remaining local variables to have void type
for(int i=index;i!=attr.maxLocals();++i) {
types[i] = JvmTypes.VOID;
}
// Now, create the stores array (one element for each bytecode);
Store[] stores = new Store[attr.bytecodes().size()];
stores[0] = new Store(types, attr.maxLocals());
return stores;
}
@Override
public Store transfer(int index, Bytecode.Store code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
checkIsSubtype(normalise(code.type),type,index,orig);
store.set(code.slot,type);
return store;
}
@Override
public Store transfer(int index, Load code, Store store) {
Store orig = store;
store = store.clone();
checkMaxStack(1,index,orig);
JvmType type = store.get(code.slot);
checkIsSubtype(normalise(code.type),type,index,orig);
store.push(type);
return store;
}
@Override
public Store transfer(int index, LoadConst code, Store store) {
Store orig = store;
store = store.clone();
checkMaxStack(1,index,orig);
Object constant = code.constant;
if (constant instanceof Boolean || constant instanceof Byte
|| constant instanceof Short || constant instanceof Character
|| constant instanceof Integer) {
store.push(JvmTypes.INT);
} else if(constant instanceof Long) {
store.push(JvmTypes.LONG);
} else if(constant instanceof Float) {
store.push(JvmTypes.FLOAT);
} else if(constant instanceof Double) {
store.push(JvmTypes.DOUBLE);
} else if(constant instanceof String) {
store.push(JvmTypes.JAVA_LANG_STRING);
} else if(constant == null) {
store.push(JvmTypes.NULL);
} else if(constant instanceof JvmType.Clazz) {
store.push(JvmTypes.JAVA_LANG_CLASS);
} else {
throw new RuntimeException("unknown constant encountered ("
+ constant + "," + constant.getClass().getName() + ")");
}
return store;
}
@Override
public Store transfer(int index, ArrayLoad code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(2,index,orig);
JvmType i = store.pop();
checkIsSubtype(JvmTypes.INT,i,index,orig);
JvmType type = store.pop();
checkIsSubtype(code.type,type,index,orig);
if (type instanceof JvmType.Array) {
JvmType.Array arrType = (JvmType.Array) type;
store.push(normalise(arrType.element()));
} else {
// This is a fall back, since it is permitted to perform an array
// load / store on the null type.
store.push(JvmTypes.VOID);
}
return store;
}
@Override
public Store transfer(int index, ArrayStore code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(3,index,orig);
JvmType item = store.pop();
checkIsSubtype(normalise(code.type.element()), item, index, orig);
JvmType i = store.pop();
checkIsSubtype(JvmTypes.INT,i,index,orig);
JvmType type = store.pop();
checkIsSubtype(code.type,type,index,orig);
return store;
}
@Override
public void transfer(int index, Throw code, Store store) {
checkMinStack(1,index,store);
JvmType type = store.top();
checkIsSubtype(JvmTypes.JAVA_LANG_THROWABLE, type, index, store);
}
@Override
public void transfer(int index, Return code, Store store) {
if(code.type != null) {
checkMinStack(1,index,store);
checkIsSubtype(normalise(code.type),store.top(),index,store);
checkIsSubtype(normalise(method.type().returnType()),store.top(),index,store);
}
}
@Override
public Store transfer(int index, Iinc code, Store store) {
checkIsSubtype(JvmTypes.INT, store.get(code.slot), index, store);
return store;
}
@Override
public Store transfer(int index, BinOp code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(2,index,orig);
JvmType rhs = store.pop();
JvmType lhs = store.pop();
checkIsSubtype(code.type,lhs,index,orig);
switch(code.op) {
case Bytecode.BinOp.SHL:
case Bytecode.BinOp.SHR:
case Bytecode.BinOp.USHR:
// These bytecodes are non-symmetric, and always require an int on
// the right-hand side.
checkIsSubtype(JvmTypes.INT,rhs,index,orig);
break;
default:
// These bytecodes are symmetric and always require the same type on
// both the left- and right-hand sides.
checkIsSubtype(code.type,rhs,index,orig);
}
store.push(code.type);
return store;
}
@Override
public Store transfer(int index, Neg code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
JvmType mhs = store.pop();
checkIsSubtype(code.type, mhs, index, orig);
store.push(code.type);
return store;
}
@Override
public Store transfer(int index, Bytecode.New code, Store store) {
Store orig = store;
store = store.clone();
if (code.type instanceof JvmType.Array) {
int dims = Math.max(1, code.dims);
checkMinStack(dims,index,orig);
// In the case of an array construction, there will be one or more
// dimensions provided for the array.
for (int i = 0; i != dims; ++i) {
checkIsSubtype(JvmTypes.INT, store.pop(), index, orig);
}
}
checkMaxStack(1,index,orig);
store.push(code.type);
return store;
}
@Override
public Store transfer(int index, boolean branch, If code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1, index, orig);
JvmType mhs = store.pop();
switch (code.cond) {
case NONNULL:
case NULL:
checkIsSubtype(JvmTypes.JAVA_LANG_OBJECT, mhs, index, orig);
break;
default:
checkIsSubtype(JvmTypes.INT, mhs, index, orig);
}
return store;
}
@Override
public Store transfer(int index, boolean branch, IfCmp code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(2,index,orig);
JvmType rhs = store.pop();
JvmType lhs = store.pop();
checkIsSubtype(normalise(code.type),lhs,index,orig);
checkIsSubtype(normalise(code.type),rhs,index,orig);
return store;
}
@Override
public Store transfer(int index, GetField code, Store store) {
Store orig = store;
store = store.clone();
if(code.mode != Bytecode.FieldMode.STATIC) {
checkMinStack(1,index,orig);
JvmType owner = store.pop();
checkIsSubtype(code.owner, owner, index, orig);
}
checkMaxStack(1,index,store);
store.push(normalise(code.type));
return store;
}
@Override
public Store transfer(int index, PutField code, Store store) {
Store orig = store;
store = store.clone();
JvmType type = store.pop();
checkIsSubtype(normalise(code.type), type, index, orig);
if (code.mode != Bytecode.FieldMode.STATIC) {
checkMinStack(2,index,orig);
JvmType owner = store.pop();
checkIsSubtype(code.owner, owner, index, orig);
} else {
checkMinStack(1,index,orig);
}
return store;
}
@Override
public Store transfer(int index, ArrayLength code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
if (!(type instanceof JvmType.Array || type instanceof JvmType.Null)) {
throw new VerificationException(method, index, orig,
"arraylength requires array type, found " + type);
}
store.push(JvmTypes.INT);
return store;
}
@Override
public Store transfer(int index, Invoke code, Store store) {
Store orig = store;
store = store.clone();
JvmType.Function ftype = code.type;
List<JvmType> parameters = ftype.parameterTypes();
if(code.mode != Bytecode.InvokeMode.STATIC) {
checkMinStack(parameters.size()+1,index,orig);
} else {
checkMinStack(parameters.size(),index,orig);
}
checkMinStack(parameters.size(),index,orig);
for (int i = parameters.size() - 1; i >= 0; --i) {
JvmType type = store.pop();
checkIsSubtype(normalise(parameters.get(i)), type, index, orig);
}
if (code.mode != Bytecode.InvokeMode.STATIC) {
JvmType type = store.pop();
checkIsSubtype(code.owner, type, index, orig);
}
JvmType rtype = ftype.returnType();
if(!rtype.equals(JvmTypes.VOID)) {
checkMaxStack(1,index,store);
store.push(normalise(rtype));
}
return store;
}
@Override
public Store transfer(int index, CheckCast code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
checkIsSubtype(code.type,type,index,orig);
store.push(code.type);
return store;
}
@Override
public Store transfer(int index, Conversion code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
if(!type.equals(code.from)) {
throw new VerificationException(method, index, orig,
"conversion expected " + code.from + ", found " + type);
}
store.push(code.to);
return store;
}
@Override
public Store transfer(int index, InstanceOf code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
checkIsSubtype(code.type,type,index,orig);
store.push(JvmTypes.INT);
return store;
}
@Override
public Store transfer(int index, Pop code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(1,index,orig);
store.pop();
return store;
}
@Override
public Store transfer(int index, Dup code, Store store) {
Store orig = store;
store = store.clone();
checkMaxStack(1,index,orig);
JvmType type = store.top();
store.push(type);
return store;
}
@Override
public Store transfer(int index, DupX1 code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(2,index,orig);
checkMaxStack(1,index,orig);
// Duplicate the top operand stack value and insert two values down
JvmType type = store.pop();
JvmType gate = store.pop();
store.push(type);
store.push(gate);
store.push(type);
return store;
}
@Override
public Store transfer(int index, DupX2 code, Store store) {
// Duplicate the top operand stack value and insert two or three values
// down
Store orig = store;
store = store.clone();
checkMinStack(3,index,orig);
checkMaxStack(1,index,orig);
JvmType type = store.pop();
JvmType gate1 = store.pop();
JvmType gate2 = store.pop();
store.push(type);
store.push(gate2);
store.push(gate1);
store.push(type);
return store;
}
@Override
public Store transfer(int index, Swap code, Store store) {
Store orig = store;
store = store.clone();
checkMinStack(2,index,orig);
JvmType first = store.pop();
JvmType second = store.pop();
store.push(first);
store.push(second);
return store;
}
@Override
public Store transfer(int index, Cmp code, Store store) {
Store orig = store; // saved
store = store.clone();
checkMinStack(2,index,orig);
JvmType lhs = store.pop();
JvmType rhs = store.pop();
checkIsSubtype(code.type,lhs,index,orig);
checkIsSubtype(code.type,rhs,index,orig);
store.push(JvmTypes.INT);
return store;
}
@Override
public Store transfer(int index, Nop code, Store store) {
// does what it says on the tin ;)
return store;
}
@Override
public Store transfer(int index, MonitorEnter code, Store store) {
Store orig = store; // saved
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
if (type instanceof JvmType.Primitive) {
throw new VerificationException(method, index, orig,
"monitorenter bytecode requires Object type");
}
return store;
}
@Override
public Store transfer(int index, MonitorExit code, Store store) {
Store orig = store; // saved
store = store.clone();
checkMinStack(1,index,orig);
JvmType type = store.pop();
if (type instanceof JvmType.Primitive) {
throw new VerificationException(method, index, orig,
"monitorexit bytecode requires Object type");
}
return store;
}
@Override
public boolean merge(int index, Store original, Store update) {
if (original.stack != update.stack) {
throw new VerificationException(method, index, original,
"incompatible stack heights");
}
// first check whether any changes are needed without allocating any more memory.
JvmType[] original_types = original.types;
JvmType[] update_types = update.types;
boolean changed = false;
for(int i=0;i!=original.stack;++i) {
JvmType ot = original_types[i];
JvmType ut = update_types[i];
if(ot != null && ut != null) {
changed |= !isSubtype(ot,ut);
original_types[i] = join(ot,ut);
} else {
changed |= ot != null || ut != null;
original_types[i] = null;
}
}
return changed;
}
private JvmType join(JvmType t1, JvmType t2) {
if (t1.equals(t2)) {
return t1;
} else if (t1 instanceof JvmType.Array && t2 instanceof JvmType.Array) {
JvmType.Array a1 = (JvmType.Array) t1;
JvmType.Array a2 = (JvmType.Array) t2;
// FIXME: can we do better here?
if (a1.element().equals(a2.element())) {
return a1;
}
} else if (t1 instanceof JvmType.Reference
&& t2 instanceof JvmType.Reference) {
// FIXME: could do a lot better here.
return JvmTypes.JAVA_LANG_OBJECT;
}
return JvmTypes.VOID;
}
/**
* Convert types into their stack based representation.
*
* @param type
* @return
*/
private static JvmType normalise(JvmType type) {
if (type.equals(JvmTypes.BOOL) || type.equals(JvmTypes.CHAR)
|| type.equals(JvmTypes.BYTE)
|| type.equals(JvmTypes.SHORT)) {
return JvmTypes.INT;
}
return type;
}
/**
* Check that there are at least min items on the stack.
*
* @param min
* The number of spaces to check for
* @param index
* Index position of respective bytecode
* @param store
* Store at this point
*/
private void checkMinStack(int min, int index, Store store) {
int stackSize = store.stack();
if (stackSize < min) {
throw new VerificationException(method, index, store,
"bytecode requires " + min + " stack items, found only "
+ stackSize + " items.");
}
}
/**
* Check that there are at least max free spaces on the stack.
*
* @param max
* The number of spaces to check for
* @param index
* Index position of respective bytecode
* @param store
* Store at this point
*/
private void checkMaxStack(int max, int index, Store store) {
int spaces = store.maxStack() - store.stack();
if (max > spaces) {
throw new VerificationException(method, index, store,
"bytecode requires space for " + max + " stack items, found only "
+ spaces + " spaces.");
}
}
/**
* Check t1 is a supertype of t2 (i.e. t1 :> t2). If not, throw a
* VerificationException.
*/
private void checkIsSubtype(JvmType t1, JvmType t2, int index, Store store) {
if(isSubtype(t1,t2)) {
return;
} else {
Code code = method.attribute(Code.class);
List<Bytecode> bytecodes = code.bytecodes();
// return
throw new VerificationException(method, index, store,
"expected type " + t1 + ", found type " + t2 + " (index "
+ index + ", " + bytecodes.get(index) + ")");
}
}
/**
* Determine whether t1 is a supertype of t2 (i.e. t1 :> t2).
*/
private boolean isSubtype(JvmType t1, JvmType t2) {
if(t1.equals(t2)) {
return true;
} else if(t1 instanceof JvmType.Array && t2 instanceof JvmType.Array) {
JvmType.Array a1 = (JvmType.Array) t1;
JvmType.Array a2 = (JvmType.Array) t2;
// NOTE: the following is technically unsound. But, it matches how
// the JVM actually handles subtyping of arrays.
return isSubtype(a1.element(),a2.element());
} else if (t1.equals(JvmTypes.JAVA_LANG_OBJECT)
&& t2 instanceof JvmType.Array) {
return true;
} else if (t1 instanceof JvmType.Reference
&& t2 instanceof JvmType.Null) {
return true;
} else if(t1 instanceof JvmType.Clazz && t2 instanceof JvmType.Clazz) {
// FIXME: could do a lot better here.
return true;
}
return false;
}
/**
* Indicates that the bytecode being analysis is malformed in some manner.
*
* @author David J. Pearce
*
*/
public static class VerificationException extends RuntimeException {
/**
* Classfile method which is malformed.
*/
private ClassFile.Method method;
/**
* Bytecode index where the problem originated
*/
private int index;
/**
* Current state of the abstract store when the problem originated.
*/
private Store store;
public VerificationException(ClassFile.Method method, int index, Store store, String msg) {
super(msg);
this.method = method;
this.index = index;
this.store = store;
}
}
/**
* An abstract representation of the typing environment used in the JVM
* bytecode verifier.
*
* @author David J. Pearce
*
*/
static class Store {
private JvmType[] types;
private int stack; // stack pointer
private int maxLocals;
public Store(JvmType[] types, int maxLocals) {
this.types = types;
this.stack = maxLocals;
this.maxLocals = maxLocals;
}
private Store(Store store) {
this.types = store.types.clone();
this.stack = store.stack;
this.maxLocals = store.maxLocals;
}
public Store clone() {
return new Store(this);
}
public int stack() {
return stack - maxLocals;
}
public int maxLocals() {
return maxLocals;
}
public int maxStack() {
return types.length - maxLocals;
}
public JvmType get(int index) {
return types[index];
}
public void set(int slot, JvmType type) {
types[slot] = type;
}
public JvmType top() {
return types[stack-1];
}
public void push(JvmType type) {
types[stack++] = type;
}
public JvmType pop() {
stack = stack-1;
return types[stack];
}
public String toString() {
String r = "[";
for(int i=0;i!=stack;++i) {
if(i == maxLocals) {
r = r + " | ";
} else if(i != 0) {
r = r + ", ";
}
r = r + types[i];
}
return r + "]";
}
}
}