org.caffinitas.bcverify.BCVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of caffinitas-bcverify Show documentation
Show all versions of caffinitas-bcverify Show documentation
Bytecode verifier that checks if a method/class/jar only uses "permitted" code
The newest version!
/*
* Copyright (C) 2014 Robert Stupp, Koeln, Germany, robert-stupp.de
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.caffinitas.bcverify;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BCVerifier
{
static final Logger LOGGER = LoggerFactory.getLogger(BCVerifier.class);
private final List allow = new ArrayList();
private final List reject = new ArrayList();
private boolean acceptDefault = true;
private static final Pattern DOT = Pattern.compile("\\.");
public void setAcceptDefault(boolean acceptDefault)
{
this.acceptDefault = acceptDefault;
}
public void addAllow(Pattern... patterns)
{
Collections.addAll(allow, patterns);
}
public void addAllow(String... patterns)
{
for (String pattern : patterns)
{
allow.add(Pattern.compile(pattern));
}
}
public void addAllowPackages(String... packages)
{
for (String pkg : packages)
{
allow.add(Pattern.compile(DOT.matcher(pkg).replaceAll("\\.")+"\\..+"));
}
}
public void addAllowClasses(String... classes)
{
for (String cls : classes)
{
allow.add(Pattern.compile(DOT.matcher(cls).replaceAll("\\.")));
}
}
public void addReject(Pattern... patterns)
{
Collections.addAll(reject, patterns);
}
public void addReject(String... patterns)
{
for (String pattern : patterns)
{
reject.add(Pattern.compile(pattern));
}
}
public void addRejectPackages(String... packages)
{
for (String pkg : packages)
{
reject.add(Pattern.compile(DOT.matcher(pkg).replaceAll("\\\\.")+"\\..+"));
}
}
public void addRejectClasses(String... classes)
{
for (String cls : classes)
{
reject.add(Pattern.compile(DOT.matcher(cls).replaceAll("\\\\.")));
}
}
public VerifyResult scan(File file) throws IOException
{
if (file == null)
{
return null;
}
LOGGER.debug("scan({})", file);
VerifyResult result = new VerifyResult();
scan(result, new StringBuilder(), file);
return result;
}
private void scan(VerifyResult result, StringBuilder path, File file) throws IOException
{
if (file == null)
{
return;
}
int l = path.length();
if (path.length() > 0)
{
path.append('.');
}
if (file.isDirectory())
{
path.append(file.getName());
File[] files = file.listFiles();
if (files != null)
{
for (File f : files)
{
scan(result, path, f);
}
}
} else if (file.isFile())
{
if (file.getName().endsWith(".class"))
{
path.append(file.getName(), 0, file.getName().length() - 6);
scanClass(result, path.toString(), file);
}
}
path.setLength(l);
}
public VerifyResult scan(URL url) throws IOException
{
if (url == null)
{
return null;
}
LOGGER.debug("scan({})", url);
VerifyResult result = new VerifyResult();
try (InputStream input = url.openConnection().getInputStream())
{
JarInputStream jarInputStream = new JarInputStream(input);
for (JarEntry entry; (entry = jarInputStream.getNextJarEntry()) != null; )
{
String name = entry.getName();
if (name.endsWith(".class"))
{
scanClass(result, name.substring(0, name.length() - 6).replace('/', '.'), jarInputStream);
}
}
}
return result;
}
public void scanClass(VerifyResult result, String clazz, File file) throws IOException
{
if (file == null)
{
return;
}
LOGGER.debug("scanClass({}, {})", clazz, file);
try (FileInputStream fis = new FileInputStream(file))
{
scanClass(result, clazz, fis);
}
}
public void scanClass(VerifyResult result, String clazz, URL url) throws IOException
{
if (url == null)
{
return;
}
LOGGER.debug("scanClass({}, {})", clazz, url);
try (InputStream input = url.openConnection().getInputStream())
{
scanClass(result, clazz, input);
}
}
public void scanClass(VerifyResult result, String clazz, byte[] classData)
{
if (classData == null)
{
return;
}
LOGGER.debug("scanClass(clazz, byte[])", clazz);
ClassReader classReader = new ClassReader(classData);
scanClass(result, clazz, classReader);
}
public void scanClass(VerifyResult result, String clazz, InputStream classData) throws IOException
{
if (classData == null)
{
return;
}
LOGGER.debug("scanClass({}, InputStream)", clazz);
ClassReader classReader = new ClassReader(classData);
scanClass(result, clazz, classReader);
}
public void scanClass(final VerifyResult result, final String clazz, ClassReader classReader)
{
if (classReader == null)
{
return;
}
LOGGER.debug("scanClass({}, ClassReader)", clazz);
final StringBuilder currentSource = new StringBuilder();
final StringBuilder currentClass = new StringBuilder();
final StringBuilder currentMethod = new StringBuilder();
final AtomicInteger currentLine = new AtomicInteger();
final MethodVisitor methodVisitor = new MethodVisitor(Opcodes.ASM5)
{
@Override public void visitLineNumber(int line, Label start)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitLineNumber line={} start={}", line, start);
}
currentLine.set(line);
super.visitLineNumber(line, start);
}
@Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitMethodInsn opcode={} owner={} name={} desc={} itf={}", opcode, owner, name, desc, itf);
}
Type t = Type.getObjectType(owner);
checkType(result, clazz, t, "method call against %s.%s%s in method %s.%s (%s:%s)", t.getClassName(), name, desc, currentClass, currentMethod,
currentSource, currentLine);
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
@Override public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitInvokeDynamicInsn name={} desc={} bsm={} bsmArgs={}", name, desc, bsm, bsmArgs);
}
Type t = Type.getType(desc);
checkType(result, clazz, t, "method call against %s.%s%s in method %s.%s (%s:%s)", t.getClassName(), name, desc, currentClass, currentMethod,
currentSource, currentLine);
super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
}
@Override public void visitTypeInsn(int opcode, String type)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitTypeInsn opcode={} type={}", opcode, type);
}
Type t = Type.getObjectType(type);
checkType(result, clazz, t, "type in method %s.%s (%s:%s)", currentClass, currentMethod, currentSource, currentLine);
super.visitTypeInsn(opcode, type);
}
@Override public void visitFieldInsn(int opcode, String owner, String name, String desc)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitFieldInsn opcode={} owner={} name={} desc={}", opcode, owner, name, desc);
}
Type type = Type.getType(desc);
checkType(result, clazz, type, "field %s in method %s.%s (%s:%s)", name, currentClass, currentMethod, currentSource, currentLine);
super.visitFieldInsn(opcode, owner, name, desc);
}
@Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index)
{
if (LOGGER.isTraceEnabled())
{
LOGGER
.trace("visitLocalVariable name={} desc={} signature={} start={} end={} index={}", name, desc, signature, start, end, index);
}
Type type = Type.getType(desc);
checkType(result, clazz, type, "local variable %s in method %s.%s (%s:%s)", name, currentClass, currentMethod, currentSource, currentLine);
super.visitLocalVariable(name, desc, signature, start, end, index);
}
@Override public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index,
String desc, boolean visible)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitLocalVariableAnnotation typeRef={} typePath={} start={} end={} index={} desc={} visible={}", typeRef, typePath,
start, end, index, desc, visible);
}
return super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, desc, visible);
}
};
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5)
{
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visit version={} access={} name={} signature={} superName={} interfaces={}", version, access, name, signature,
superName, interfaces);
}
currentClass.setLength(0);
currentClass.append(Type.getObjectType(name).getClassName());
super.visit(version, access, name, signature, superName, interfaces);
}
@Override public void visitSource(String source, String debug)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitSource source={} debug={}", source, debug);
}
currentSource.setLength(0);
currentSource.append(source);
super.visitSource(source, debug);
}
@Override public void visitOuterClass(String owner, String name, String desc)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitOuterClass owner={} name={} desc={}", owner, name, desc);
}
super.visitOuterClass(owner, name, desc);
// TODO maybe resolve outer class and scan it starting here
}
@Override public void visitInnerClass(String name, String outerName, String innerName, int access)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitInnerClass name={} outerName={} innerName={} access={}", name, outerName, innerName, access);
}
super.visitInnerClass(name, outerName, innerName, access);
// TODO maybe resolve inner class and scan it starting here
}
@Override public void visitAttribute(Attribute attr)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitAttribute attr={}", attr);
}
Type type = Type.getType(attr.type);
checkType(result, clazz, type, "attr type %s", currentClass);
super.visitAttribute(attr);
}
@Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitField access={} name={} desc={} signature={} value={}", access, name, desc, signature, value);
}
Type type = Type.getType(desc);
checkType(result, clazz, type, "field type %s.%s", currentClass, name);
return super.visitField(access, name, desc, signature, value);
}
@Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitMethod access={} name={} desc={} signature={} exceptions={}", access, name, desc, signature, exceptions);
}
Type returnType = Type.getReturnType(desc);
Type[] argTypes = Type.getArgumentTypes(desc);
checkType(result, clazz, returnType, "return type %s.%s%s", currentClass, name, desc);
for (Type argType : argTypes)
{
checkType(result, clazz, argType, "method parameter type %s.%s%s", currentClass, name, desc);
}
super.visitMethod(access, name, desc, signature, exceptions);
currentMethod.setLength(0);
currentMethod.append(name);
currentMethod.append(desc);
return methodVisitor;
}
@Override public void visitEnd()
{
if (LOGGER.isTraceEnabled())
{
LOGGER.trace("visitEnd");
}
super.visitEnd();
}
};
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
}
private void checkType(VerifyResult result, String clazz, Type type, String fmt, Object... params)
{
if (type.getSort() == Type.ARRAY)
{
type = type.getElementType();
}
if (type.getSort() == Type.OBJECT)
{
checkClass(result, type.getClassName(), type, fmt, params);
}
}
public void checkClass(VerifyResult result, String className, Type type, String fmt, Object... params)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("{}: checkClass {}: {}", className, type.getClassName(), String.format(fmt, params));
}
VerifyResult.ClassResult r = result.checked(className);
if (r != null)
{
if (!r.isAccept())
r.addReason(String.format(fmt, params));
return;
}
Pattern bestMatch = null;
int bestMatchLen = 0;
Boolean accept = null;
for (Pattern pattern : allow)
{
Matcher m = pattern.matcher(className);
if (m.matches())
{
int ml = pattern.toString().length();
if (ml > bestMatchLen)
{
bestMatch = pattern;
bestMatchLen = ml;
accept = true;
}
}
}
for (Pattern pattern : reject)
{
Matcher m = pattern.matcher(className);
if (m.matches())
{
int ml = pattern.toString().length();
if (ml > bestMatchLen)
{
bestMatch = pattern;
bestMatchLen = ml;
accept = false;
}
}
}
if (accept == null)
{
accept = acceptDefault;
}
String reason = accept ? "" : String.format(fmt, params);
result.add(new VerifyResult.ClassResult(className, accept, reason));
LOGGER.debug("{} => {} {}", className, accept, bestMatch);
}
private static int matchLength(Matcher m)
{
return m.end() - m.start();
}
}