diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index b08e5aa..f57aeb4 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,10 +1,8 @@ \ No newline at end of file diff --git a/src/dev/w1zzrd/asm/AsmAnnotation.java b/src/dev/w1zzrd/asm/AsmAnnotation.java deleted file mode 100644 index e2fae2f..0000000 --- a/src/dev/w1zzrd/asm/AsmAnnotation.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.w1zzrd.asm; - -import java.lang.annotation.Annotation; -import java.util.Map; - -/** - * Java ASM annotation data representation - * @param Type of the annotation - */ -final class AsmAnnotation { - private final Class annotationType; - private final Map entries; - - public AsmAnnotation(Class annotationType, Map entries) { - this.annotationType = annotationType; - this.entries = entries; - } - - public Class getAnnotationType() { - return annotationType; - } - - public T getEntry(String name) { - if (!hasEntry(name)) - throw new IllegalArgumentException(String.format("No entry \"%s\" in asm annotation!", name)); - return (T)entries.get(name); - } - - public boolean hasEntry(String name) { - return entries.containsKey(name); - } -} diff --git a/src/dev/w1zzrd/asm/Combine.java b/src/dev/w1zzrd/asm/Combine.java new file mode 100644 index 0000000..19201da --- /dev/null +++ b/src/dev/w1zzrd/asm/Combine.java @@ -0,0 +1,283 @@ +package dev.w1zzrd.asm; + +import dev.w1zzrd.asm.analysis.AsmAnnotation; +import dev.w1zzrd.asm.exception.MethodNodeResolutionException; +import dev.w1zzrd.asm.exception.SignatureInstanceMismatchException; +import dev.w1zzrd.asm.signature.MethodSignature; +import jdk.internal.org.objectweb.asm.ClassWriter; +import jdk.internal.org.objectweb.asm.Opcodes; +import jdk.internal.org.objectweb.asm.Type; +import jdk.internal.org.objectweb.asm.tree.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +import static jdk.internal.org.objectweb.asm.ClassWriter.COMPUTE_MAXS; + +public class Combine { + private final ClassNode target; + + + public Combine(ClassNode target) { + this.target = target; + } + + public void inject(MethodNode node, GraftSource source) { + final AsmAnnotation annotation = source.getInjectAnnotation(node); + + final boolean acceptReturn = annotation.getEntry("acceptOriginalReturn"); + + switch ((InPlaceInjection)annotation.getEnumEntry("value")) { + case INSERT: // Explicitly insert a *new* method + insert(node, source); + break; + case REPLACE: // Explicitly replace an *existing* method + replace(node, source, true); + break; + case INJECT: // Insert method by either replacing an existing method or inserting a new method + insertOrReplace(node, source); + break; + case AFTER: // Inject a method's instructions after the original instructions in a given method + append(node, source, acceptReturn); + break; + case BEFORE: // Inject a method's instructions before the original instructions in a given method + prepend(node, source); + break; + } + } + + /** + * Extend implementation of a method past its regular return. This grafts the given method node to the end of the + * targeted method node, such that, instead of returning, the code in the given method node is executed with the + * return value from the original node as the "argument" to the grafted node. + * @param extension Node to extend method with + * @param source The {@link GraftSource} from which the method node will be adapted + * @param acceptReturn Whether or not the grafted method should "receive" the original method's return value as an "argument" + */ + public void append(MethodNode extension, GraftSource source, boolean acceptReturn) { + final MethodNode target = checkMethodExists(source.getMethodTargetName(extension), source.getMethodTargetSignature(extension)); + adaptMethod(extension, source); + + } + + public void prepend(MethodNode extension, GraftSource source) { + final MethodNode target = checkMethodExists(source.getMethodTargetName(extension), source.getMethodTargetSignature(extension)); + adaptMethod(extension, source); + + } + + public void replace(MethodNode inject, GraftSource source, boolean preserveOriginalAccess) { + final MethodNode remove = checkMethodExists(source.getMethodTargetName(inject), source.getMethodTargetSignature(inject)); + ensureMatchingSignatures(remove, inject, Opcodes.ACC_STATIC); + if (preserveOriginalAccess) + copySignatures(remove, inject, Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + + insertOrReplace(inject, source); + } + + public void insert(MethodNode inject, GraftSource source) { + checkMethodNotExists(source.getMethodTargetName(inject), source.getMethodTargetSignature(inject)); + insertOrReplace(inject, source); + } + + protected void insertOrReplace(MethodNode inject, GraftSource source) { + MethodNode replace = findMethodNode(source.getMethodTargetName(inject), source.getMethodTargetSignature(inject)); + + if (replace != null) + this.target.methods.remove(replace); + + adaptMethod(inject, source); + + this.target.methods.add(inject); + } + + + + /** + * Compile target class data to a byte array + * @return Class data + */ + public byte[] toByteArray() { + return toByteArray(COMPUTE_MAXS); + } + + /** + * Compile target class data to a byte array + * @param writerFlags Flags to pass to the {@link ClassWriter} used to compile the target class + * @return Class data + */ + public byte[] toByteArray(int writerFlags) { + ClassWriter writer = new ClassWriter(writerFlags); + //target.methods.forEach(method -> method.localVariables.forEach(var -> var.name = var.name.replace(" ", ""))); + target.accept(writer); + + return writer.toByteArray(); + } + + /** + * Compile target class data to byte array and load with system class loader + * @return Class loaded by the loader + */ + public Class compile() { + return compile(ClassLoader.getSystemClassLoader()); + } + + /** + * Compile target class data to byte array and load with the given class loader + * @param loader Loader to use when loading the class + * @return Class loaded by the loader + */ + public Class compile(ClassLoader loader) { + Method m = null; + try { + m = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + + assert m != null; + m.setAccessible(true); + //ReflectCompat.setAccessible(m, true); + + byte[] data = toByteArray(); + + try { + return (Class) m.invoke(loader, data, 0, data.length); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Prepares a {@link MethodNode} for grafting on to a given method and into the targeted {@link ClassNode} + * @param node Node to adapt + * @param source The {@link GraftSource} from which the node will be adapted + */ + protected void adaptMethod(MethodNode node, GraftSource source) { + final AbstractInsnNode last = node.instructions.getLast(); + for (AbstractInsnNode insn = node.instructions.getFirst(); insn != last; insn = insn.getNext()) { + if (insn instanceof MethodInsnNode) adaptMethodInsn((MethodInsnNode) insn, source); + else if (insn instanceof LdcInsnNode) adaptLdcInsn((LdcInsnNode) insn, source.getTypeName()); + else if (insn instanceof FrameNode) adaptFrameNode((FrameNode) insn, node, source); + } + + node.name = source.getMethodTargetName(node); + } + + + /** + * Adapts a grafted method instruction node to fit its surrogate + * @param node Grafted method instruction node + * @param source The {@link GraftSource} from which the instruction node will be adapted + */ + protected void adaptMethodInsn(MethodInsnNode node, GraftSource source) { + if (node.owner.equals(source.getTypeName())) { + final MethodNode injected = source.getInjectedMethod(node.name, node.desc); + if (injected != null) { + node.owner = this.target.name; + node.name = source.getMethodTargetName(injected); + } + } + } + + /** + * Adapts a grafted constant instruction node to fit its surrogate + * @param node Grafted LDC instruction node + * @param originalOwner Fully-qualified name of the original owner class + */ + protected void adaptLdcInsn(LdcInsnNode node, String originalOwner) { + if (node.cst instanceof Type && ((Type) node.cst).getInternalName().equals(originalOwner)) + node.cst = Type.getType(String.format("L%s;", originalOwner)); + } + + protected void adaptFrameNode(FrameNode node, MethodNode method, GraftSource source) { + + } + + /** + * Ensure that a method node matching the given description does not exist in the targeted class + * @param name Name of the method node + * @param descriptor Descriptor of the method node + * @throws MethodNodeResolutionException If a method matching the given description could be found + */ + protected final void checkMethodNotExists(String name, MethodSignature descriptor) { + final MethodNode target = findMethodNode(name, descriptor); + + if (target != null) + throw new MethodNodeResolutionException(String.format( + "Cannot insert method node \"%s%s\" into class: node with name and signature already exists", + name, + descriptor + )); + } + + /** + * Ensure that a method node matching the given description exists in the targeted class + * @param name Name of the method node + * @param descriptor Descriptor of the method node + * @return The located method node + * @throws MethodNodeResolutionException If no method node matching the given description could be found + */ + protected final @NotNull MethodNode checkMethodExists(String name, MethodSignature descriptor) { + final MethodNode target = findMethodNode(name, descriptor); + + if (target == null) + throw new MethodNodeResolutionException(String.format( + "Cannot replace method node \"%s\" in class: node with name and signature does not exist", + name + )); + + return target; + } + + /** + * Find a method node in the targeted class by name and descriptor + * @param name Name of the method node to find + * @param desc Descriptor of the method node to find + * @return A matching {@link MethodNode} if one exists, else null + */ + protected @Nullable MethodNode findMethodNode(String name, MethodSignature desc) { + return target.methods + .stream() + .filter(it -> it.name.equals(name) && new MethodSignature(it.desc).equals(desc)) + .findFirst() + .orElse(null); + } + + /** + * Ensure that the injection method has matching access flags as the targeted method + * @param target Targeted method + * @param inject Injected method + * @param flags Flags to check equality of (see {@link Opcodes}) + */ + protected static void ensureMatchingSignatures(MethodNode target, MethodNode inject, int flags) { + if ((target.access & flags) != (inject.access & flags)) + throw new SignatureInstanceMismatchException(String.format( + "Access flag mismatch for target method %s (with flags %d) and inject method %s (with flags %d)", + target.name, + target.access & flags, + inject.name, + inject.access & flags + )); + } + + /** + * Copy access flags from the targeted method to the injected method + * @param target Targeted method + * @param inject Injected method + * @param flags Flags to copy (see {@link Opcodes}) + */ + protected static void copySignatures(MethodNode target, MethodNode inject, int flags) { + inject.access ^= (inject.access & flags) ^ (target.access & flags); + } + + + + public static java.util.List dumpInsns(MethodNode node) { + return Arrays.asList(node.instructions.toArray()); + } +} diff --git a/src/dev/w1zzrd/asm/GraftSource.java b/src/dev/w1zzrd/asm/GraftSource.java new file mode 100644 index 0000000..9290cd0 --- /dev/null +++ b/src/dev/w1zzrd/asm/GraftSource.java @@ -0,0 +1,141 @@ +package dev.w1zzrd.asm; + +import dev.w1zzrd.asm.analysis.AsmAnnotation; +import dev.w1zzrd.asm.signature.MethodSignature; +import jdk.internal.org.objectweb.asm.tree.AnnotationNode; +import jdk.internal.org.objectweb.asm.tree.ClassNode; +import jdk.internal.org.objectweb.asm.tree.FieldNode; +import jdk.internal.org.objectweb.asm.tree.MethodNode; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public final class GraftSource { + private final String typeName; + private final HashMap>> methodAnnotations; + private final HashMap>> fieldAnnotations; + + public GraftSource(ClassNode source) { + this.typeName = source.name; + + methodAnnotations = new HashMap<>(); + for (MethodNode mNode : source.methods) + { + List> annotations = parseAnnotations(mNode.visibleAnnotations); + if (hasNoInjectionDirective(annotations)) + continue; + + methodAnnotations.put(mNode, annotations); + } + + fieldAnnotations = new HashMap<>(); + for (FieldNode fNode : source.fields) + { + List> annotations = parseAnnotations(fNode.visibleAnnotations); + if (hasNoInjectionDirective(annotations)) + continue; + + fieldAnnotations.put(fNode, annotations); + } + } + + public String getTypeName() { + return typeName; + } + + public String getMethodTarget(MethodNode node) { + if (methodAnnotations.containsKey(node)) { + String target = getInjectionDirective(methodAnnotations.get(node)).getEntry("target"); + if (target != null && target.length() != 0) + return target; + } + + return node.name + node.desc; + } + + public String getMethodTargetName(MethodNode node) { + String target = getMethodTarget(node); + + if (target.contains("(")) + return target.substring(0, target.indexOf('(')); + + return target; + } + + public MethodSignature getMethodTargetSignature(MethodNode node) { + String target = getMethodTarget(node); + + if (target.contains("(")) + return new MethodSignature(target.substring(target.indexOf('('))); + + return new MethodSignature(node.desc); + } + + public boolean isMethodInjected(String name, String desc) { + return getInjectedMethod(name, desc) != null; + } + + public @Nullable MethodNode getInjectedMethod(String name, String desc) { + return methodAnnotations + .keySet() + .stream() + .filter(it -> it.name.equals(name) && it.desc.equals(desc)) + .findFirst() + .orElse(null); + } + + public String getMethodTargetName(String name, String desc) { + final MethodNode inject = getInjectedMethod(name, desc); + return inject == null ? name : getMethodTargetName(inject); + } + + public String getFieldTargetName(FieldNode node) { + if (fieldAnnotations.containsKey(node)) { + String target = getInjectionDirective(fieldAnnotations.get(node)).getEntry("target"); + if (target != null && target.length() != 0) + return target; + } + + return node.name; + } + + public List> getMethodAnnotations(MethodNode node) { + return methodAnnotations.get(node); + } + + public List> getFieldAnnotations(FieldNode node) { + return fieldAnnotations.get(node); + } + + public Set getInjectMethods() { + return methodAnnotations.keySet(); + } + + public Set getInjectFields() { + return fieldAnnotations.keySet(); + } + + public AsmAnnotation getInjectAnnotation(MethodNode node) { + return getInjectionDirective(methodAnnotations.get(node)); + } + + private static boolean hasNoInjectionDirective(List> annotations) { + return getInjectionDirective(annotations) == null; + } + + private static AsmAnnotation getInjectionDirective(List> annotations) { + for (AsmAnnotation annot : annotations) + if (annot.getAnnotationType() == Inject.class) + return (AsmAnnotation) annot; + + return null; + } + + private static List> parseAnnotations(List annotations) { + return annotations == null ? new ArrayList<>() : annotations.stream().map(AsmAnnotation::getAnnotation).collect(Collectors.toList()); + } +} diff --git a/src/dev/w1zzrd/asm/InPlaceInjection.java b/src/dev/w1zzrd/asm/InPlaceInjection.java index 55fd506..7976516 100644 --- a/src/dev/w1zzrd/asm/InPlaceInjection.java +++ b/src/dev/w1zzrd/asm/InPlaceInjection.java @@ -17,5 +17,15 @@ public enum InPlaceInjection { /** * Replace method instructions in target method */ - REPLACE + REPLACE, + + /** + * Insert method into class. This does not allow overwrites + */ + INSERT, + + /** + * Inserts a method if it does not exist in the class, otherwise replace it + */ + INJECT } diff --git a/src/dev/w1zzrd/asm/Inject.java b/src/dev/w1zzrd/asm/Inject.java index d084539..c18925a 100644 --- a/src/dev/w1zzrd/asm/Inject.java +++ b/src/dev/w1zzrd/asm/Inject.java @@ -4,8 +4,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - -import static dev.w1zzrd.asm.InPlaceInjection.REPLACE; +import static dev.w1zzrd.asm.InPlaceInjection.INJECT; /** * Mark a field or method for injection into a target @@ -17,7 +16,7 @@ public @interface Inject { * How to inject the method. Note: not valid for fields * @return {@link InPlaceInjection} */ - InPlaceInjection value() default REPLACE; + InPlaceInjection value() default INJECT; /** * Explicit method target signature. Note: not valid for fields diff --git a/src/dev/w1zzrd/asm/Merger.java b/src/dev/w1zzrd/asm/Merger.java index 65701b0..8346686 100644 --- a/src/dev/w1zzrd/asm/Merger.java +++ b/src/dev/w1zzrd/asm/Merger.java @@ -1,5 +1,6 @@ package dev.w1zzrd.asm; +import dev.w1zzrd.asm.analysis.AsmAnnotation; import jdk.internal.org.objectweb.asm.*; import jdk.internal.org.objectweb.asm.tree.*; import java.io.*; @@ -27,6 +28,8 @@ public class Merger { protected final ClassNode targetNode; + private final HashMap overrideCount = new HashMap<>(); + /** * Create a merger for the given target class @@ -228,7 +231,9 @@ public class Merger { // Attempt to fix injector ownership for(Field f : node.getClass().getFields()) { try { - if (f.getName().equals("owner") && f.getType().equals(String.class) && f.get(node).equals(injectOwner)) + f.setAccessible(true); + if (f.getName().equals("owner") && f.getType().equals(String.class) && + f.get(node).equals(injectOwner)) f.set(node, getTargetName()); } catch (IllegalAccessException e) { e.printStackTrace(); @@ -457,6 +462,7 @@ public class Merger { assert m != null; m.setAccessible(true); + //ReflectCompat.setAccessible(m, true); byte[] data = toByteArray(); @@ -952,7 +958,7 @@ public class Merger { */ public static ClassNode readClass(byte[] data) { ClassNode node = new ClassNode(); - new ClassReader(data).accept(node, 0); + new ClassReader(data).accept(node, ClassReader.EXPAND_FRAMES); return node; } diff --git a/src/dev/w1zzrd/asm/ReflectCompat.java b/src/dev/w1zzrd/asm/ReflectCompat.java new file mode 100644 index 0000000..944782d --- /dev/null +++ b/src/dev/w1zzrd/asm/ReflectCompat.java @@ -0,0 +1,105 @@ +package dev.w1zzrd.asm; + +import dev.w1zzrd.asm.reflect.BruteForceDummy; +import sun.misc.Unsafe; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; + +/** + * Internal compatibility layer for Java 8+ + */ +final class ReflectCompat { + private static final Unsafe theUnsafe; + + //private static final Field accessibleObject_field_override; + private static final long accessibleObject_fieldOffset_override; + + + static { + long accessibleObject_fieldOffset_override1; + theUnsafe = guarantee(() -> { + Field f1 = Unsafe.class.getDeclaredField("theUnsafe"); + f1.setAccessible(true); + return (Unsafe) f1.get(null); + }); + + // Field guaranteed to fail + AccessibleObject f = guarantee(() -> BruteForceDummy.class.getDeclaredField("inaccessible")); + BruteForceDummy dummy = new BruteForceDummy(); + try { + // Well-defined solution for finding object field offset + accessibleObject_fieldOffset_override1 = theUnsafe.objectFieldOffset(AccessibleObject.class.getDeclaredField("override")); + } catch (NoSuchFieldException e) { + // Brute-force solution when VM hides fields based on exports + long offset = 0; + int temp; + + // Booleans are usually 32 bits large so just search in 4-byte increments + while (true) { + temp = theUnsafe.getInt(f, offset); + + // Ensure we're probably working with a false-value + if (temp == 0) { + theUnsafe.putBoolean(f, offset, true); + boolean fails = fails(() -> ((Field)f).get(dummy)); + + theUnsafe.putInt(f, offset, temp); + + if (!fails) + break; + } + + offset += 4; + } + + accessibleObject_fieldOffset_override1 = offset; + } + accessibleObject_fieldOffset_override = accessibleObject_fieldOffset_override1; + } + + /** + * Forcefully set the override flag of an {@link AccessibleObject} + * @param obj Object to override + * @param access Value of flag + */ + static void setAccessible(AccessibleObject obj, boolean access) { + theUnsafe.putBoolean(obj, accessibleObject_fieldOffset_override, access); + } + + + + + + private interface ExceptionRunnable { + T run() throws Throwable; + } + + /** + * Convenience method for ignoring exceptions where they're guaranteed not to be thrown. + * @param run Interface describing action to be performed + * @param Return type expected from call to {@link ExceptionRunnable#run()} + * @return Expected value + */ + private static T guarantee(ExceptionRunnable run) { + try { + return run.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + /** + * Check if a given code block fails to run all the way through. + * @param run {@link ExceptionRunnable} to check + * @return True if an exception was thrown, otherwise false + */ + private static boolean fails(ExceptionRunnable run) { + try { + run.run(); + } catch (Throwable t) { + return true; + } + return false; + } +} diff --git a/src/dev/w1zzrd/asm/analysis/AsmAnnotation.java b/src/dev/w1zzrd/asm/analysis/AsmAnnotation.java new file mode 100644 index 0000000..660162b --- /dev/null +++ b/src/dev/w1zzrd/asm/analysis/AsmAnnotation.java @@ -0,0 +1,114 @@ +package dev.w1zzrd.asm.analysis; + +import dev.w1zzrd.asm.exception.AnnotationMismatchException; +import jdk.internal.org.objectweb.asm.tree.AnnotationNode; +import sun.reflect.annotation.AnnotationParser; +import sun.reflect.annotation.AnnotationType; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +/** + * Java ASM annotation data representation + * @param Type of the annotation + */ +public final class AsmAnnotation { + private final Class annotationType; + private final Map entries; + + public AsmAnnotation(Class annotationType, Map entries) { + this.annotationType = annotationType; + this.entries = entries; + } + + public Class getAnnotationType() { + return annotationType; + } + + public T getEntry(String name) { + if (!hasEntry(name)) + throw new IllegalArgumentException(String.format("No entry \"%s\" in asm annotation!", name)); + if (hasExplicitEntry(name)) + return (T)entries.get(name); + return (T)getValueMethod(name).getDefaultValue(); + } + + public > T getEnumEntry(String entryName) { + if (!hasExplicitEntry(entryName)) { + if (hasDefaultEntry(entryName)) + return (T)getValueMethod(entryName).getDefaultValue(); + + throw new IllegalArgumentException(String.format("No entry \"%s\" in annotation!", entryName)); + } + + final String[] value = getEntry(entryName); + final String typeName = value[0]; + final String enumName = value[1]; + + Class type; + try { + type = (Class) Class.forName(typeName.substring(1, typeName.length() - 1).replace('/', '.')); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + try { + T[] values = (T[]) type.getDeclaredMethod("values").invoke(null); + + for (T declaredValue : values) + if (declaredValue.name().equals(enumName)) + return declaredValue; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + throw new AnnotationMismatchException(String.format( + "Could not find an enum of type %s with name \"%s\"", + typeName, + enumName + )); + } + + protected Method getValueMethod(String name) { + for (Method m : annotationType.getDeclaredMethods()) + if (m.getName().equals(name)) { + m.setAccessible(true); + return m; + } + + return null; + } + + protected boolean hasDefaultEntry(String name) { + return getValueMethod(name) != null; + } + + protected boolean hasExplicitEntry(String name) { + return entries.containsKey(name); + } + + public boolean hasEntry(String name) { + return hasDefaultEntry(name) || hasExplicitEntry(name); + } + + + public static AsmAnnotation getAnnotation(AnnotationNode node) { + Class cls; + try { + cls = (Class) Class.forName(node.desc.substring(1, node.desc.length() - 1).replace('/', '.')); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + + HashMap entries = new HashMap<>(); + if (node.values != null) + for (int i = 0; i < node.values.size(); i += 2) + entries.put((String)node.values.get(i), node.values.get(i + 1)); + + return new AsmAnnotation(cls, entries); + } +} diff --git a/src/dev/w1zzrd/asm/analysis/FrameState.java b/src/dev/w1zzrd/asm/analysis/FrameState.java new file mode 100644 index 0000000..7b3b3bd --- /dev/null +++ b/src/dev/w1zzrd/asm/analysis/FrameState.java @@ -0,0 +1,624 @@ +package dev.w1zzrd.asm.analysis; + +import dev.w1zzrd.asm.exception.StateAnalysisException; +import dev.w1zzrd.asm.signature.MethodSignature; +import dev.w1zzrd.asm.signature.TypeSignature; +import jdk.internal.org.objectweb.asm.Label; +import jdk.internal.org.objectweb.asm.Opcodes; +import jdk.internal.org.objectweb.asm.Type; +import jdk.internal.org.objectweb.asm.tree.*; +import org.jetbrains.annotations.Nullable; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; +import java.util.function.Predicate; + +/** + * Method frame state analysis class. + * Ideally, this should allow for instruction optimization when weaving methods. + * Additionally, this could theoretically enable code to be woven in-between original instructions + */ +public class FrameState { + /** + * Stack clobbering pushed values after an instruction is invoked. + * (See {@link jdk.internal.org.objectweb.asm.Frame#SIZE})
+ * Key:
+ * ? No change
+ * X Requires special attention
+ * L Object
+ * I int
+ * J long
+ * F float
+ * D double
+ */ + private static final String STACK_CLOBBER_PUSH = + "?LIIIIIIIJJFFFDDIIXXXIJFDLIIIIJJJJFFFFDDDDLLLLIJFDLIII???????????????????????????????????XXXXXX?IJFDIJFDIJFDIJFDIJFDIJFDIJIJIJIJIJIJXJFDIFDIJDIJFIIIIIIII?????????????????????????X?X?XXXXXLLLI?LI??XL????"; + + /** + * Stack clobbering popped values when an instruction is invoked. + * (See {@link jdk.internal.org.objectweb.asm.Frame#SIZE})
+ * Key:
+ * ? None
+ * X Requires special attention
+ * $ Cat1 computational type
+ * L Object
+ * I int
+ * J long
+ * F float
+ * D double
+ * S int/float
+ * W long/double
+ * C int, int
+ * V long, long
+ * B float, float
+ * N double, double
+ * M object, int
+ * 0 object, object
+ * 1 object, int, int
+ * 2 object, int, long
+ * 3 object, int, float
+ * 4 object, int, double
+ * 5 object, int, object
+ * K Cat1, Cat1
+ *
+ * Cat1 computational types are, according to the JVM8 spec, essentially all 32-bit types (any type that occupies 1 stack slot) + */ + private static final String STACK_CLOBBER_POP = + "??????????????????????????????????????????????MMMMMMMMIJFDLIIIIJJJJFFFFDDDDLLLL12345111$K$$$XXXKCVBNCVBNCVBNCVBNCVBNCVBNCVCVCVCVCVCV?IIIJJJFFFDDDIIIVBBNNIIIIIICCCCCC00?????IJFDL??XLXXXXXX?IILLLLLLXXLL??"; + + + private final Stack stack = new Stack<>(); + private final ArrayList locals = new ArrayList<>(); + private final int stackSize; + + private FrameState(AbstractInsnNode targetNode, List constants) { + AbstractInsnNode first = targetNode, tmp; + + // Computation is already O(n), no need to accept first instruction as an argument + while ((tmp = first.getPrevious()) != null) + first = tmp; + + // Now we traverse backward to select ONE possible sequence of instructions that may be executed + // This lets us simulate the stack for this sequence. This only works because we don't consider conditionals + // Because we ignore conditionals and because we have assisting FrameNodes, we sidestep the halting problem + // Since we're traversing backward, the latest instruction to be read will be the earliest to be executed + Stack simulate = new Stack<>(); + for (AbstractInsnNode check = targetNode; check != null; check = check.getPrevious()) { + // If the node we're checking is a label, find the earliest jump to it + // This assumes that no compiler optimizations have been made based on multiple values at compile time + // since we can't trivially predict branches, but I don't think the java compiler does either, so meh + if (check instanceof LabelNode) { + // Labels don't affect the stack, so we can safely ignore them + JumpInsnNode jump = findEarliestJump((LabelNode) check); + if (jump == null) + continue; + + check = jump; + } + + // No need to check line numbers in a simulation + if (check instanceof LineNumberNode) + continue; + + // Add instruction to simulation list + simulate.add(check); + + // No need to simulate the state before a full frame: this is kinda like a "checkpoint" in the frame state + if (check instanceof FrameNode && ((FrameNode) check).type == Opcodes.F_FULL) + break; + } + + int stackSize = 0; + + // We now have a proposed set of instructions that might run if the instructions were to be called + // Next, we analyse the stack and locals throughout the execution of these instructions + while (!simulate.isEmpty()) { + if (simulate.peek() instanceof FrameNode) + stackSize = ((FrameNode) simulate.peek()).stack.size(); + + updateFrameState(simulate.pop(), stack, locals, constants); + } + + this.stackSize = stackSize; + + // The stack and locals are now in the state they would be in after the target instruction is hit + // QED or something... + + + /* + * NOTE: This code analysis strategy assumes that the program analyzed follows the behaviour of any regular JVM + * program. This will, for example, fail to predict the state of the following set of (paraphrased) + * instructions: + * + * + * METHOD START: + * 1: LOAD 1 + * 2: JUMP TO LABEL "X" IF LOADED VALUE == 0 // if(1 == 0) + * 3: PUSH 2 + * 4: PUSH 69 + * 5: PUSH 420 + * 6: LABEL "X" + * 7: POP <----- Analysis requested of this node + * + * + * This FrameState method will (falsely) predict that the stack is empty and that the POP will fail, since it + * will trace execution back to the jump at line (2), since it cannot determine that the jump will always fail + * and simply observes the jump as popping the value loaded at (1), thus meaning that the stack would be empty + * at line (7). Whereas in actuality, the jump will fail and the stack will therefore contain three values. + * + * This is because a normal Java program would not allow this arrangement of instructions, since it would entail + * a split in the state of the stack based on the outcome of the conditional jump. I don't know of any JVM + * language that *would* produce this behaviour (and I'm 90% sure the JVM would complain about the lack of + * FrameNodes in the above example), but if the FrameState *does* encounter code compiled by such a language, + * this code CANNOT predict the state of such a stack and incorrect assumptions about the state of the stack may + * occur. + * + * Also note that this kind of behaviour cannot be predicted due to the halting problem, so any program which + * exhibits the aforementioned behaviour is inherently unpredictable. + */ + } + + /** + * Attempts to find the earliest jump label referencing the given node in the instruction list + * @param node {@link LabelNode} to find jumps referencing + * @return A jump instruction node earlier in the instruction list or null if none could be found + */ + private static @Nullable JumpInsnNode findEarliestJump(LabelNode node) { + JumpInsnNode jump = null; + + // Traverse backward until we hit the beginning of the list + for (AbstractInsnNode prev = node; prev != null; prev = prev.getPrevious()) + if (prev instanceof JumpInsnNode && ((JumpInsnNode) prev).label.equals(node)) + jump = (JumpInsnNode) prev; + + return jump; + } + + /** + * Updates the state of a simulated stack frame based on the effects of a given instruction. Effectively simulates + * the instruction to a certain degree + * @param instruction Instruction to "simulate" + * @param stack Frame stack values + * @param locals Frame local variables + * @param constants Method constant pool types + */ + private static void updateFrameState( + AbstractInsnNode instruction, + Stack stack, + ArrayList locals, + List constants + ) { + if (instruction instanceof FrameNode) { + // Stack values are always updated at a FrameNode + stack.clear(); + + switch (instruction.getType()) { + case Opcodes.F_NEW: + case Opcodes.F_FULL: + // Since this is a full frame, we start anew + locals.clear(); + + // Ascertain stack types + appendTypes(((FrameNode) instruction).stack, stack, true); + + // Ascertain local types + appendTypes(((FrameNode) instruction).local, locals, false); + break; + + case Opcodes.F_APPEND: + appendTypes(((FrameNode) instruction).local, locals, false); + break; + + case Opcodes.F_SAME1: + appendTypes(((FrameNode) instruction).stack, stack, true); + break; + + case Opcodes.F_CHOP: + List local = ((FrameNode) instruction).local; + if (local != null) + while (local.size() > locals.size()) + locals.remove(locals.size() - 1); + break; + } + } else clobberStack(instruction, stack, locals, constants); + } + + /** + * Parse and append raw frame type declarations to the end of the given collection + * @param types Raw frame types to parse + * @param appendTo Collection to append types to + * @param skipNulls Whether or not to short-circuit parsing when a null-valued type is found + */ + private static void appendTypes(List types, List appendTo, boolean skipNulls) { + if (types == null) return; + + for (Object o : types) + if (o == null && skipNulls) break; + else appendTo.add(o == null ? null : parseFrameSignature(o)); + } + + /** + * Determine the {@link TypeSignature} of stack/local type declaration + * @param o Type to parse + * @return {@link TypeSignature} representing the given type declaration + */ + private static TypeSignature parseFrameSignature(Object o) { + if (o instanceof String) // Fully qualified type + return new TypeSignature("L"+o+";"); + else if (o instanceof Integer) { // Primitive + switch ((int)o) { + case 0: // Top + return new TypeSignature('V', true); + case 1: // Int + return new TypeSignature("I"); + case 2: // Float + return new TypeSignature("F"); + case 3: // Double + return new TypeSignature("D"); + case 4: // Long + return new TypeSignature("J"); + case 5: // Null + return new TypeSignature(); + } + } else if (o instanceof Label) { + return new TypeSignature("Ljava/lang/Object;", 0, true); + } + + throw new StateAnalysisException(String.format("Could not determine type signature for object %s", o)); + } + + /** + * Simulate stack-clobbering effects of invoking a given instruction with a given frame state + * @param insn Instruction to simulate + * @param stack Frame stack values + * @param locals Frame local variables + */ + private static void clobberStack( + AbstractInsnNode insn, + List stack, + List locals, + List constants + ) { + // Look, before you go ahead and roast my code, just know that I have a "code first, think later" mentality, + // so this entire method was essentially throw together and structured this way before I realised what I was + // doing. If things look like they're implemented in a dumb way, it's probably because it is. There was + // virtually no thought behind the implementation of this method. Now... let the roasting commence + + final int opcode = insn.getOpcode(); + if (opcode >= 0 && opcode < STACK_CLOBBER_POP.length()) { + // We have an instruction + char pushType = STACK_CLOBBER_PUSH.charAt(opcode); + char popType = STACK_CLOBBER_POP.charAt(opcode); + + // Yes, the switches in the conditional statements can be collapsed, but this keeps it clean (for now) + // TODO: Collapse switch statements + if (pushType == 'X' && popType == 'X') { + // Complex argument and result + // This behaviour is exhibited by 11 instructions in the JVM 8 spec + int argCount = 0; + switch (opcode) { + case Opcodes.DUP2: + case Opcodes.DUP2_X1: + case Opcodes.DUP2_X2: + // Actually just operates on Cat2 values, but whatever + stack.add(stack.size() - (opcode - 90), stack.get(stack.size() - 2)); + stack.add(stack.size() - (opcode - 90), stack.get(stack.size() - 2)); + break; + + case Opcodes.INVOKEVIRTUAL: + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKEINTERFACE: + argCount = 1; + case Opcodes.INVOKESTATIC: { + MethodSignature msig = new MethodSignature(((MethodInsnNode)insn).desc); + argCount += msig.getArgCount(); + for (int i = 0; i < argCount; ++i) { + // Longs and doubles pop 2 values from the stack + if (i < msig.getArgCount() && msig.getArg(i).stackFrameElementWith() == 2) + stack.remove(stack.size() - 1); + + // All args pop at least 1 value + stack.remove(stack.size() - 1); + } + + // For non-void methods, push return to stack + if (!msig.getRet().isVoidType()) + stack.add(msig.getRet()); + + break; + } + + case Opcodes.INVOKEDYNAMIC: + // TODO: Implement: this requires dynamic call-site resolution and injection + //InvokeDynamicInsnNode dyn = (InvokeDynamicInsnNode) insn; + break; + + case 196: // WIDE + // WIDE instruction not expected in normal Java programs + // TODO: Implement? + throw new NotImplementedException(); + } + + } else if (pushType == 'X') { + // Complex result + // Technically IINC is classified here, but it can be ignored because this isn't a verification tool; + // this just checks clobbering, which IINC does not do + switch (opcode) { + case Opcodes.DUP: + case Opcodes.DUP_X1: + case Opcodes.DUP_X2: + stack.add(stack.size() - (opcode - 88), stack.get(stack.size() - 1)); + break; + + case Opcodes.LDC: + case 19: // LDC_W + case 20: // LDC2_W + { + // I'm not 100% sure this actually works for LDC_W and LDC2_W + LdcInsnNode ldc = (LdcInsnNode) insn; + if (ldc.cst instanceof Type) { + // Type objects in in context will always refer to method references, class literals or + // array literals + int sort = ((Type) ldc.cst).getSort(); + switch (sort) { + case Type.OBJECT: + stack.add(new TypeSignature(((Type) ldc.cst).getDescriptor())); + break; + + case Type.METHOD: + stack.add(new TypeSignature(new MethodSignature(((Type) ldc.cst).getDescriptor()))); + break; + } + } else if (ldc.cst instanceof String){ + // Loading a string constant, I think + stack.add(new TypeSignature("Ljava/lang/String;")); + } else { + // Some primitive boxed value + // All the boxed primitives have a public static final field TYPE declaring their unboxed + // type, so we just get the internal name of that field reflectively because I'm lazy + // TODO: Un-reflect-ify this because it can literally be solved with if-elses instead + try { + stack.add(new TypeSignature( + ((Class)ldc.cst.getClass().getField("TYPE").get(null)).getName() + )); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + break; + } + + case Opcodes.GETFIELD: + stack.remove(stack.size() - 1); + case Opcodes.GETSTATIC: + stack.add(new TypeSignature(((FieldInsnNode) insn).desc)); + break; + } + } else if (popType == 'X') { + // Complex argument encompasses 3 instructions + switch (opcode) { + case Opcodes.PUTFIELD: + case Opcodes.PUTSTATIC: { + FieldInsnNode put = (FieldInsnNode) insn; + + // Get type signature + TypeSignature sig = new TypeSignature(put.desc); + + // If type is Long or Double, we need to pop 2 elements + if (sig.stackFrameElementWith() == 2) + stack.remove(stack.size() - 1); + + // Pop element from stack + stack.remove(stack.size() - 1); + + // If this was a non-static instruction, pop object reference too + if (opcode == Opcodes.PUTFIELD) + stack.remove(stack.size() - 1); + + break; + } + + case Opcodes.MULTIANEWARRAY: { + MultiANewArrayInsnNode marray = (MultiANewArrayInsnNode) insn; + + // Pop a value for each dimension + for (int i = 0; i < marray.dims; ++i) + stack.remove(stack.size() - 1); + + stack.add(new TypeSignature(marray.desc)); + break; + } + } + } else { + // Trivial-ish argument and result + trivialPop(insn, popType, stack, locals, constants); + trivialPush(insn, pushType, stack, locals, constants); + } + } + } + + + /** + * Simulate a "trivial" instruction which pops values from the operand stack + * @param insn Instruction to simulate pushing for + * @param type Classification of push type + * @param stack Simulated operand stand types + * @param locals Simulated frame local types + * @param constants Method constant pool + */ + private static void trivialPop(AbstractInsnNode insn, char type, List stack, List locals, List constants) { + // TODO: Fix type naming scheme; this is actually going to make me cry + // Yes, the fall-throughs are very intentional + switch (type) { + // Pops 4 values + case 'V': + case 'N': + case '2': + case '4': + stack.remove(stack.size() - 1); + + // Pops 3 values + case '1': + case '3': + case '5': + stack.remove(stack.size() - 1); + + // Pops 2 values + case 'D': + case 'J': + case 'W': + case 'C': + case 'B': + case 'M': + case '0': + case 'K': + stack.remove(stack.size() - 1); + + // Pops 1 value + case 'I': + case 'F': + case 'L': + case 'S': + case '$': + stack.remove(stack.size() - 1); + break; + } + } + + /** + * Simulate a "trivial" instruction which pushes values to the operand stack + * @param insn Instruction to simulate pushing for + * @param type Classification of push type + * @param stack Simulated operand stand types + * @param locals Simulated frame local types + * @param constants Method constant pool + */ + private static void trivialPush(AbstractInsnNode insn, char type, List stack, List locals, List constants) { + // Pushing is a bit more tricky than popping because we have to resolve types (kind of) + switch (type) { + case 'I': + case 'F': + // Push single-entry primitive + stack.add(new TypeSignature(Character.toString(type))); + break; + + case 'D': + case 'J': + // Push two-entry primitive (value + top) + stack.add(new TypeSignature(Character.toString(type))); + stack.add(new TypeSignature(type, true)); + break; + + case 'L': + // Push an object type to the stack + switch (insn.getOpcode()) { + case Opcodes.ACONST_NULL: + // Null type, I guess + stack.add(new TypeSignature()); + break; + + case Opcodes.ALOAD: + case 42: // ALOAD_0 + case 43: // ALOAD_1 + case 44: // ALOAD_2 + case 45: // ALOAD_3 + // Push a local variable to the stack + stack.add(locals.get(((VarInsnNode) insn).var)); + break; + + case Opcodes.AALOAD: + // Read an array element to the stack + stack.remove(stack.size() - 1); // Pop array index + + // Pop array and push value + // This assumes that the popped value is an array (as it should be) + stack.add(stack.remove(stack.size() - 1).getArrayElementType()); + break; + + case Opcodes.NEW: + // Allocate a new object (should really be marked as uninitialized, but meh) + // We'll burn that bridge when we get to it or something... + stack.add(new TypeSignature(((TypeInsnNode) insn).desc)); + break; + + case Opcodes.NEWARRAY: + // Allocate a new, 1-dimensional, primitive array + stack.remove(stack.size() - 1); + stack.add(new TypeSignature( + Character.toString("ZCFDBSIJ".charAt(((IntInsnNode) insn).operand - 4)), + 1, + false + )); + break; + + case Opcodes.ANEWARRAY: + // Allocate a new, 1-dimensional, object array + stack.remove(stack.size() - 1); + stack.add(new TypeSignature(((TypeInsnNode) insn).desc, 1, false)); + break; + + case Opcodes.CHECKCAST: + // Cast an object to another type + stack.remove(stack.size() - 1); + stack.add(new TypeSignature(((TypeInsnNode) insn).desc)); + break; + } + } + } + + + /** + * Purely for debugging purposes. This method generates a collection of instruction names that match the given + * functional stack-clobbering properties.
+ *
+ * For example:
+ * The WIDE instruction is classified as both a complex-push and complex-pop because determining how it clobbers + * the stack requires determining which instruction it is wrapping and thereby what types are expected. + * Depending on the bytecode, the wide instruction can pop between 0 (like WIDE ILOAD) and 2 (like WIDE LSTORE) + * operands and may push between 0 (like WIDE ISTORE) and 2 (like WIDE DLOAD) operands or not touch the operand + * stack at all (like WIDE IINC). + * + * @param complexPush Whether or not the instructions should have non-trivial results generated by execution + * @param complexPop Whether or not the instructions should have non-trivial argument requirements for execution + * @param insnP An instruction-code specific predicate for fine-tuned filtering + * @return A collection of instruction names matching the given functional properties. For instructions named + * "Opcode<...>", please refer to the comments in {@link Opcodes} as well as the official JVM specification + * @see JVM8 instructions spec + */ + private static List getOpsByComplexity(boolean complexPush, boolean complexPop, @Nullable Predicate insnP) { + ArrayList opcodes = new ArrayList<>(); + + for (int i = 0; i < FrameState.STACK_CLOBBER_PUSH.length(); ++i) + if ((FrameState.STACK_CLOBBER_PUSH.charAt(i) == 'X' == complexPush) && + (FrameState.STACK_CLOBBER_POP.charAt(i) == 'X' == complexPop)) + opcodes.add(i); + + return opcodes.stream().filter(insnP == null ? it -> true : insnP).map(instrID -> { + try { + return java.util.Arrays + .stream(Opcodes.class.getFields()) + .filter(field -> { + try { + return java.lang.reflect.Modifier.isStatic(field.getModifiers()) && + !field.getName().startsWith("ACC_") && + !field.getName().startsWith("T_") && + !field.getName().startsWith("H_") && + !field.getName().startsWith("F_") && + !field.getName().startsWith("V1_") && + field.getType().equals(int.class) && + field.get(null).equals(instrID); + } catch (Throwable t) { + throw new RuntimeException(t); + } + }) + .map(Field::getName) + .findFirst() + .orElse(String.format("Opcode<%d>", instrID)); + } catch(Throwable t) { + throw new RuntimeException(t); + } + }).collect(java.util.stream.Collectors.toList()); + } +} diff --git a/src/dev/w1zzrd/asm/exception/AnnotationMismatchException.java b/src/dev/w1zzrd/asm/exception/AnnotationMismatchException.java new file mode 100644 index 0000000..d185fb5 --- /dev/null +++ b/src/dev/w1zzrd/asm/exception/AnnotationMismatchException.java @@ -0,0 +1,22 @@ +package dev.w1zzrd.asm.exception; + +public class AnnotationMismatchException extends RuntimeException { + public AnnotationMismatchException() { + } + + public AnnotationMismatchException(String message) { + super(message); + } + + public AnnotationMismatchException(String message, Throwable cause) { + super(message, cause); + } + + public AnnotationMismatchException(Throwable cause) { + super(cause); + } + + public AnnotationMismatchException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/dev/w1zzrd/asm/exception/MethodNodeResolutionException.java b/src/dev/w1zzrd/asm/exception/MethodNodeResolutionException.java new file mode 100644 index 0000000..704899c --- /dev/null +++ b/src/dev/w1zzrd/asm/exception/MethodNodeResolutionException.java @@ -0,0 +1,22 @@ +package dev.w1zzrd.asm.exception; + +public class MethodNodeResolutionException extends RuntimeException { + public MethodNodeResolutionException() { + } + + public MethodNodeResolutionException(String message) { + super(message); + } + + public MethodNodeResolutionException(String message, Throwable cause) { + super(message, cause); + } + + public MethodNodeResolutionException(Throwable cause) { + super(cause); + } + + public MethodNodeResolutionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/dev/w1zzrd/asm/exception/SignatureInstanceMismatchException.java b/src/dev/w1zzrd/asm/exception/SignatureInstanceMismatchException.java new file mode 100644 index 0000000..cd273f1 --- /dev/null +++ b/src/dev/w1zzrd/asm/exception/SignatureInstanceMismatchException.java @@ -0,0 +1,22 @@ +package dev.w1zzrd.asm.exception; + +public class SignatureInstanceMismatchException extends RuntimeException { + public SignatureInstanceMismatchException() { + } + + public SignatureInstanceMismatchException(String message) { + super(message); + } + + public SignatureInstanceMismatchException(String message, Throwable cause) { + super(message, cause); + } + + public SignatureInstanceMismatchException(Throwable cause) { + super(cause); + } + + public SignatureInstanceMismatchException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/dev/w1zzrd/asm/exception/StateAnalysisException.java b/src/dev/w1zzrd/asm/exception/StateAnalysisException.java new file mode 100644 index 0000000..aa5bcc6 --- /dev/null +++ b/src/dev/w1zzrd/asm/exception/StateAnalysisException.java @@ -0,0 +1,22 @@ +package dev.w1zzrd.asm.exception; + +public class StateAnalysisException extends RuntimeException { + public StateAnalysisException() { + } + + public StateAnalysisException(String message) { + super(message); + } + + public StateAnalysisException(String message, Throwable cause) { + super(message, cause); + } + + public StateAnalysisException(Throwable cause) { + super(cause); + } + + public StateAnalysisException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/dev/w1zzrd/asm/exception/TypeSignatureParseException.java b/src/dev/w1zzrd/asm/exception/TypeSignatureParseException.java new file mode 100644 index 0000000..75e88f9 --- /dev/null +++ b/src/dev/w1zzrd/asm/exception/TypeSignatureParseException.java @@ -0,0 +1,18 @@ +package dev.w1zzrd.asm.exception; + +public class TypeSignatureParseException extends IllegalArgumentException { + public TypeSignatureParseException() { + } + + public TypeSignatureParseException(String s) { + super(s); + } + + public TypeSignatureParseException(String message, Throwable cause) { + super(message, cause); + } + + public TypeSignatureParseException(Throwable cause) { + super(cause); + } +} diff --git a/src/dev/w1zzrd/asm/reflect/BruteForceDummy.java b/src/dev/w1zzrd/asm/reflect/BruteForceDummy.java new file mode 100644 index 0000000..5c041f7 --- /dev/null +++ b/src/dev/w1zzrd/asm/reflect/BruteForceDummy.java @@ -0,0 +1,9 @@ +package dev.w1zzrd.asm.reflect; + +/** + * Dummy class used as a canary for brute-forcing the object field offset + * of AccessibleObject override flag. + */ +public class BruteForceDummy { + private final int inaccessible = 0; +} diff --git a/src/dev/w1zzrd/asm/signature/MethodSignature.java b/src/dev/w1zzrd/asm/signature/MethodSignature.java new file mode 100644 index 0000000..d1ea553 --- /dev/null +++ b/src/dev/w1zzrd/asm/signature/MethodSignature.java @@ -0,0 +1,131 @@ +package dev.w1zzrd.asm.signature; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +public class MethodSignature { + private final TypeSignature[] args; + private final TypeSignature ret; + + public MethodSignature(String sig) { + // Minimal signature size is 3. For example: "()V". With name, minimal length is 4: "a()V" + if (sig.length() < 3 || (sig.charAt(0) != '(' && sig.length() < 4)) + throw new IllegalArgumentException(String.format("Invalid method signature \"%s\"", sig)); + + final int len = sig.length(); + + int namelen = 0; + while (sig.charAt(namelen) != '(') + ++namelen; + + // NOTE: namelen now points to the opening parenthesis + + + // Find closing parenthesis + int endParen = namelen + 1; + while (sig.charAt(endParen) != ')') + if (++endParen == len) + throw new IllegalArgumentException(String.format("No end of argument list: \"%s\"", sig)); + + // Parse argument type list + ArrayList args = new ArrayList<>(); + int parseLen = namelen + 1; + while (parseLen < endParen) { + TypeSignature parsed = parseOneSignature(sig, parseLen); + args.add(parsed); + parseLen += parsed.getSig().length(); + } + + // Parse return type + TypeSignature ret = parseOneSignature(sig, endParen + 1); + if (ret.getSig().length() != len - endParen - 1) + throw new IllegalArgumentException(String.format("Trailing characters in method signature return type: %s", sig)); + + + this.args = args.toArray(new TypeSignature[0]); + this.ret = ret; + } + + private static TypeSignature parseOneSignature(String sig, int startAt) { + final int len = sig.length(); + switch (sig.charAt(startAt)) { + case 'Z': + case 'B': + case 'C': + case 'S': + case 'I': + case 'J': + case 'F': + case 'D': + case 'V': { + return new TypeSignature(sig.substring(startAt, startAt + 1)); + } + case '[': { + for (int i = startAt + 1; i < len; ++i) + if (sig.charAt(i) != '[') { + TypeSignature nestedSig = parseOneSignature(sig, i); + return new TypeSignature(nestedSig.getSig(), i - startAt, false); + } + break; + } + // Object type + case 'L': { + for (int i = startAt + 1; i < len; ++i) + if (sig.charAt(i) == ')' || sig.charAt(i) == '(') + throw new IllegalArgumentException("Bad type termination!"); + else if (sig.charAt(i) == ';') + return new TypeSignature(sig.substring(startAt, i + 1)); + break; + } + + case ')': + case '(': throw new IllegalArgumentException(String.format("Unexpected token in signature \"%s\"", sig.substring(startAt))); + } + + throw new IllegalArgumentException(String.format("Invalid type/method signature \"%s\"", sig.substring(startAt))); + } + + public int getArgCount() { + return args.length; + } + + public TypeSignature getArg(int index) { + return args[index]; + } + + public TypeSignature getRet() { + return ret; + } + + @Override + public String toString() { + int size = 2; + for (int i = 0; i < args.length; ++i) + size += args[i].getSig().length(); + size += ret.getSig().length(); + + StringBuilder builder = new StringBuilder(size); + builder.append('('); + + for (int i = 0; i < args.length; ++i) + builder.append(args[i].getSig()); + + return builder.append(')').append(ret.getSig()).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MethodSignature that = (MethodSignature) o; + return Arrays.equals(args, that.args) && ret.equals(that.ret); + } + + @Override + public int hashCode() { + int result = Objects.hash(ret); + result = 31 * result + Arrays.hashCode(args); + return result; + } +} diff --git a/src/dev/w1zzrd/asm/signature/TypeSignature.java b/src/dev/w1zzrd/asm/signature/TypeSignature.java new file mode 100644 index 0000000..426e95e --- /dev/null +++ b/src/dev/w1zzrd/asm/signature/TypeSignature.java @@ -0,0 +1,244 @@ +package dev.w1zzrd.asm.signature; + +import dev.w1zzrd.asm.exception.SignatureInstanceMismatchException; +import dev.w1zzrd.asm.exception.TypeSignatureParseException; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Objects; + +public class TypeSignature { + private final String sig; + private final int arrayDepth; + + private final TypeModifier modifier; + private final MethodSignature dynamicRef; + + public TypeSignature(String sig, int reportedArrayDepth, boolean isUninitialized) { + // Signature cannot be an empty string + if (sig.length() == 0) + throw new TypeSignatureParseException("Signature cannot be blank!"); + + modifier = TypeModifier.UNINITIALIZED.iff(isUninitialized); + dynamicRef = null; + + StringBuilder builder = new StringBuilder(reportedArrayDepth + 1); + char[] fill = new char[reportedArrayDepth]; + Arrays.fill(fill, '['); + builder.append(fill); + + final int len = sig.length(); + + + // Compute the nesting depth of array types + int arrayDepth = 0; + while (sig.charAt(arrayDepth) == '[') + if (++arrayDepth == len) + throw new TypeSignatureParseException("Array signature of blank type is not valid!"); + + this.arrayDepth = arrayDepth + reportedArrayDepth; + + // Resolve signature from type identifier + switch (Character.toUpperCase(sig.charAt(arrayDepth))) { + // Primitive type + case 'Z': + case 'B': + case 'C': + case 'S': + case 'I': + case 'J': + case 'F': + case 'D': { + this.sig = builder.append(sig.toUpperCase()).toString(); + return; + } + + // Special void return type + case 'V': { + // Type "[V" cannot exist, for example + if (reportedArrayDepth + arrayDepth > 0) + throw new TypeSignatureParseException("Void type cannot have an array depth!"); + + this.sig = sig.toUpperCase(); + return; + } + + // Object type + case 'L': { + // Unterminated type signature + if (sig.charAt(sig.length() - 1) != ';') + break; + + this.sig = builder.append(sig).toString(); + return; + } + } + + throw new TypeSignatureParseException(String.format("Unknown type signature \"%s\"", sig)); + } + + public TypeSignature(String sig) { + this(sig, 0, false); + } + + /** + * Create a Top value signature + * @param primitive Primitive type internal name (V, J or D for Top types) + * @param isTop Whether or not this is a Top type (only valid for 64-bit types J and D or as delimiter type V) + */ + public TypeSignature(@Nullable Character primitive, boolean isTop) { + if (primitive != null) { + switch (primitive) { + case 'J': + case 'D': + case 'V': + break; + + default: + throw new TypeSignatureParseException(String.format( + "Primitive type signature %s cannot have a Top value. To declare a Top delimiter, use 'V'", + primitive.toString() + )); + } + } + this.sig = primitive == null ? "V" : primitive.toString(); + modifier = TypeModifier.TOP.iff(isTop); + dynamicRef = null; + this.arrayDepth = 0; + } + + public TypeSignature(MethodSignature dynamicRef) { + modifier = TypeModifier.METHOD; + arrayDepth = 0; + sig = dynamicRef.toString(); + this.dynamicRef = dynamicRef; + } + + /** + * Represents a type signature of the JVM null verification type + */ + public TypeSignature() { + this.sig = "null"; + modifier = TypeModifier.NULL; + dynamicRef = null; + this.arrayDepth = 0; + } + + /** + * Get the actual signature represented by this object + * @return The fully qualified type signature of the represented type + */ + public String getSig() { + return sig; + } + + /** + * The contained type (in the case that this is an array type). If the type represented by this object + * is not an array, this is equivalent to calling {@link TypeSignature#getSig()} + * @return Signature of the type contained in the array + */ + public String getType() { + return sig.substring(arrayDepth); + } + + /** + * Whether or not the type represented by this object is an array type + * @return True if the type has an array depth greater than 0, else false + */ + public boolean isArray() { + return arrayDepth > 0; + } + + /** + * Get the type signature of the elements contained in an array type + * @return The element type signature of the current array type signature + */ + public TypeSignature getArrayElementType() { + if (!isArray()) + throw new SignatureInstanceMismatchException("Attempt to get element type of non-array!"); + + return new TypeSignature(sig.substring(1)); + } + + /** + * Check whether or not this type represents a Top type. + * @return True if it is a Top, else false + */ + public boolean isTop() { + return modifier == TypeModifier.TOP; + } + + /** + * Check if the currently represented type is a void return type + * @return True if the signature is "V", else false + */ + public boolean isVoidType() { + return sig.length() == 1 && sig.charAt(0) == 'V'; + } + + /** + * Whether or not this type signature represents a primitive type in the JVM + * Primitive types are: Z, B, C, S, I, J, F, D, V + * @return True if this signature is primitive, false if it represents a reference-type (object) + */ + public boolean isPrimitive() { + return sig.length() == 1; + } + + /** + * The array depth of the currently represented object. Primitives and objects have a depth of 0. + * Array types have a depth grater than 0 dependent on the depth of the nesting. + * @return 0 for primitives and object and 1+ for all other types + */ + public int getArrayDepth() { + return arrayDepth; + } + + /** + * Gets the amount of slots the represented type occupies in a stack frame local variable list and operand stack + * @return 2 for (non-array) Double and Long types, 1 for everything else + */ + public int stackFrameElementWith() { + return isPrimitive() && (sig.charAt(0) == 'J' || sig.charAt(0) == 'D') ? 2 : 1; + } + + /** + * Get a string representation of the represented type + * @return The exact internal string representation of the signature + */ + @Override + public String toString() { + return sig; + } + + /** + * Checks if this object is semantically equivalent to the given object + * @param other The object to compare to + * @return True iff {@code other} is an instance of {@link TypeSignature} and the signatures are identical + */ + @Override + public boolean equals(Object other) { + return other instanceof TypeSignature && + ((TypeSignature)other).sig.equals(sig) && + ((TypeSignature) other).modifier == modifier; + } + + /** + * Computes the hashcode of this object. This mimics the equivalence specified by + * {@link TypeSignature#equals(Object)} by simply being the hashcode of the signature string + * @return The hashcode of the represented signature string + */ + @Override + public int hashCode() { + return Objects.hash(sig, modifier); + } + + + private enum TypeModifier { + NONE, TOP, UNINITIALIZED, NULL, METHOD; + + public TypeModifier iff(boolean condition) { + return condition ? this : NONE; + } + } +} diff --git a/test/MergeTest.java b/test/MergeTest.java index 2f09556..6817a17 100644 --- a/test/MergeTest.java +++ b/test/MergeTest.java @@ -1,6 +1,8 @@ +import dev.w1zzrd.asm.Merger; + public class MergeTest { - private final String s; + String s; public MergeTest(){ @@ -12,10 +14,44 @@ public class MergeTest { } public String test(){ + Class c = Merger.class; + Runnable r = () -> { + System.out.println(c.getName()); + }; + + System.out.println(r); + r.run(); return s + "Test"; } public String test1(){ return s; } + + public void stackTest() { + String str = Integer.toString(getNumber() * 23); + + if ("69".equals(str)) { + int k = Integer.getInteger(str); + + System.out.println(k + str + (k * k)); + getNumber(); + } + + float f = getNumber() * 2.5f; + + System.out.println(f + str + (f * f)); + + multiArg(str, f, f == 5f, "69".equals(str) ? f * f : (f + 1.0), f < 6f ? (int)f : 7); + } + + + private static int getNumber() { + return 3; + } + + private static void multiArg(String k, float a, boolean bool, double b, int i) { + if (bool) + System.out.println(k + a + b * getNumber() + i); + } } diff --git a/test/Test.java b/test/Test.java index 13ccc6c..a75286e 100644 --- a/test/Test.java +++ b/test/Test.java @@ -1,11 +1,28 @@ +import dev.w1zzrd.asm.Combine; +import dev.w1zzrd.asm.GraftSource; import dev.w1zzrd.asm.Merger; +import jdk.internal.org.objectweb.asm.tree.ClassNode; +import jdk.internal.org.objectweb.asm.tree.MethodNode; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; public class Test { public static void main(String... args) throws IOException { + + ClassNode target = Merger.getClassNode("MergeTest"); + ClassNode inject = Merger.getClassNode("MergeInject"); + GraftSource source = new GraftSource(target); + + Combine combine = new Combine(target); + for (MethodNode method : source.getInjectMethods()) { + combine.inject(method, source); + } + + combine.compile(); + + System.out.println("Asdf"); + + /* Merger m = new Merger("MergeTest"); m.inject("MergeInject"); @@ -22,5 +39,7 @@ public class Test { Runnable r = (Runnable)new MergeTest("Constructor message"); r.run(); + */ } + }