/* * Apache License * Version 2.0, January 2004 * http://www.apache.org/licenses/ * * TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION * * 1. Definitions. * * "License" shall mean the terms and conditions for use, reproduction, * and distribution as defined by Sections 1 through 9 of this document. * * "Licensor" shall mean the copyright owner or entity authorized by * the copyright owner that is granting the License. * * "Legal Entity" shall mean the union of the acting entity and all * other entities that control, are controlled by, or are under common * control with that entity. For the purposes of this definition, * "control" means (i) the power, direct or indirect, to cause the * direction or management of such entity, whether by contract or * otherwise, or (ii) ownership of fifty percent (50%) or more of the * outstanding shares, or (iii) beneficial ownership of such entity. * * "You" (or "Your") shall mean an individual or Legal Entity * exercising permissions granted by this License. * * "Source" form shall mean the preferred form for making modifications, * including but not limited to software source code, documentation * source, and configuration files. * * "Object" form shall mean any form resulting from mechanical * transformation or translation of a Source form, including but * not limited to compiled object code, generated documentation, * and conversions to other media types. * * "Work" shall mean the work of authorship, whether in Source or * Object form, made available under the License, as indicated by a * copyright notice that is included in or attached to the work * (an example is provided in the Appendix below). * * "Derivative Works" shall mean any work, whether in Source or Object * form, that is based on (or derived from) the Work and for which the * editorial revisions, annotations, elaborations, or other modifications * represent, as a whole, an original work of authorship. For the purposes * of this License, Derivative Works shall not include works that remain * separable from, or merely link (or bind by name) to the interfaces of, * the Work and Derivative Works thereof. * * "Contribution" shall mean any work of authorship, including * the original version of the Work and any modifications or additions * to that Work or Derivative Works thereof, that is intentionally * submitted to Licensor for inclusion in the Work by the copyright owner * or by an individual or Legal Entity authorized to submit on behalf of * the copyright owner. For the purposes of this definition, "submitted" * means any form of electronic, verbal, or written communication sent * to the Licensor or its representatives, including but not limited to * communication on electronic mailing lists, source code control systems, * and issue tracking systems that are managed by, or on behalf of, the * Licensor for the purpose of discussing and improving the Work, but * excluding communication that is conspicuously marked or otherwise * designated in writing by the copyright owner as "Not a Contribution." * * "Contributor" shall mean Licensor and any individual or Legal Entity * on behalf of whom a Contribution has been received by Licensor and * subsequently incorporated within the Work. * * 2. Grant of Copyright License. Subject to the terms and conditions of * this License, each Contributor hereby grants to You a perpetual, * worldwide, non-exclusive, no-charge, royalty-free, irrevocable * copyright license to reproduce, prepare Derivative Works of, * publicly display, publicly perform, sublicense, and distribute the * Work and such Derivative Works in Source or Object form. * * 3. Grant of Patent License. Subject to the terms and conditions of * this License, each Contributor hereby grants to You a perpetual, * worldwide, non-exclusive, no-charge, royalty-free, irrevocable * (except as stated in this section) patent license to make, have made, * use, offer to sell, sell, import, and otherwise transfer the Work, * where such license applies only to those patent claims licensable * by such Contributor that are necessarily infringed by their * Contribution(s) alone or by combination of their Contribution(s) * with the Work to which such Contribution(s) was submitted. If You * institute patent litigation against any entity (including a * cross-claim or counterclaim in a lawsuit) alleging that the Work * or a Contribution incorporated within the Work constitutes direct * or contributory patent infringement, then any patent licenses * granted to You under this License for that Work shall terminate * as of the date such litigation is filed. * * 4. Redistribution. You may reproduce and distribute copies of the * Work or Derivative Works thereof in any medium, with or without * modifications, and in Source or Object form, provided that You * meet the following conditions: * * (a) You must give any other recipients of the Work or * Derivative Works a copy of this License and * * (b) You must cause any modified files to carry prominent notices * stating that You changed the files and * * (c) You must retain, in the Source form of any Derivative Works * that You distribute, all copyright, patent, trademark, and * attribution notices from the Source form of the Work, * excluding those notices that do not pertain to any part of * the Derivative Works and * * (d) If the Work includes a "NOTICE" text file as part of its * distribution, then any Derivative Works that You distribute must * include a readable copy of the attribution notices contained * within such NOTICE file, excluding those notices that do not * pertain to any part of the Derivative Works, in at least one * of the following places: within a NOTICE text file distributed * as part of the Derivative Works within the Source form or * documentation, if provided along with the Derivative Works or, * within a display generated by the Derivative Works, if and * wherever such third-party notices normally appear. The contents * of the NOTICE file are for informational purposes only and * do not modify the License. You may add Your own attribution * notices within Derivative Works that You distribute, alongside * or as an addendum to the NOTICE text from the Work, provided * that such additional attribution notices cannot be construed * as modifying the License. * * You may add Your own copyright statement to Your modifications and * may provide additional or different license terms and conditions * for use, reproduction, or distribution of Your modifications, or * for any such Derivative Works as a whole, provided Your use, * reproduction, and distribution of the Work otherwise complies with * the conditions stated in this License. * * 5. Submission of Contributions. Unless You explicitly state otherwise, * any Contribution intentionally submitted for inclusion in the Work * by You to the Licensor shall be under the terms and conditions of * this License, without any additional terms or conditions. * Notwithstanding the above, nothing herein shall supersede or modify * the terms of any separate license agreement you may have executed * with Licensor regarding such Contributions. * * 6. Trademarks. This License does not grant permission to use the trade * names, trademarks, service marks, or product names of the Licensor, * except as required for reasonable and customary use in describing the * origin of the Work and reproducing the content of the NOTICE file. * * 7. Disclaimer of Warranty. Unless required by applicable law or * agreed to in writing, Licensor provides the Work (and each * Contributor provides its Contributions) on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied, including, without limitation, any warranties or conditions * of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A * PARTICULAR PURPOSE. You are solely responsible for determining the * appropriateness of using or redistributing the Work and assume any * risks associated with Your exercise of permissions under this License. * * 8. Limitation of Liability. In no event and under no legal theory, * whether in tort (including negligence), contract, or otherwise, * unless required by applicable law (such as deliberate and grossly * negligent acts) or agreed to in writing, shall any Contributor be * liable to You for damages, including any direct, indirect, special, * incidental, or consequential damages of any character arising as a * result of this License or out of the use or inability to use the * Work (including but not limited to damages for loss of goodwill, * work stoppage, computer failure or malfunction, or any and all * other commercial damages or losses), even if such Contributor * has been advised of the possibility of such damages. * * 9. Accepting Warranty or Additional Liability. While redistributing * the Work or Derivative Works thereof, You may choose to offer, * and charge a fee for, acceptance of support, warranty, indemnity, * or other liability obligations and/or rights consistent with this * License. However, in accepting such obligations, You may act only * on Your own behalf and on Your sole responsibility, not on behalf * of any other Contributor, and only if You agree to indemnify, * defend, and hold each Contributor harmless for any liability * incurred by, or claims asserted against, such Contributor by reason * of your accepting any such warranty or additional liability. * * END OF TERMS AND CONDITIONS * * APPENDIX: How to apply the Apache License to your work. * * To apply the Apache License to your work, attach the following * boilerplate notice, with the fields enclosed by brackets "[]" * replaced with your own identifying information. (Don't include * the brackets!) The text should be enclosed in the appropriate * comment syntax for the file format. We also recommend that a * file or class name and description of purpose be included on the * same "printed page" as the copyright notice for easier * identification within third-party archives. * * Copyright 2018 pengfengwang * * 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 com.ximsfei.stark.gradle.asm.monitor; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.google.common.collect.ImmutableList; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.JSRInlinerAdapter; import org.objectweb.asm.commons.Method; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.MethodNode; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static com.google.common.base.Preconditions.checkNotNull; public class RedirectionVisitor extends MonitorVisitor { private boolean disableRedirectionForClass = false; private boolean isInterface = false; private boolean classInitializerAdded = false; private static final class VisitorBuilder implements MonitorVisitor.VisitorBuilder { @Override public MonitorVisitor build(@NonNull AsmClassNode classNode, @NonNull ClassVisitor classVisitor) { return new RedirectionVisitor(classNode, classVisitor); } @Override public String getMangledRelativeClassFilePath(@NonNull String originalClassFilePath) { return originalClassFilePath; } } @NonNull public static final MonitorVisitor.VisitorBuilder VISITOR_BUILDER = new VisitorBuilder(); public RedirectionVisitor( @NonNull AsmClassNode classAndInterfaceNode, @NonNull ClassVisitor classVisitor) { super(classAndInterfaceNode, classVisitor); } /** * Ensures that the class contains a $starkChange field used for referencing the IncrementalChange * dispatcher. * <p> * <p>Also updates package_private visibility to public so we can call into this class from * outside the package. */ @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { visitedClassName = name; visitedSuperName = superName; isInterface = (access & Opcodes.ACC_INTERFACE) != 0; int fieldAccess = isInterface ? Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_FINAL : Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_VOLATILE | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_TRANSIENT; // when dealing with interfaces, the $starkChange field is an AtomicReference to the CHANGE_TYPE // since fields in interface must be final. For classes, it's the CHANGE_TYPE directly. if (isInterface) { super.visitField( fieldAccess, "$starkChange", getRuntimeTypeName(Type.getType(AtomicReference.class)), null, null); } else { super.visitField(fieldAccess, "$starkChange", getRuntimeTypeName(CHANGE_TYPE), null, null); } access = transformClassAccessForStark(access); super.visit(version, access, name, signature, superName, interfaces); } @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { int newAccess = access & (~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) | Opcodes.ACC_PUBLIC; super.visitInnerClass(name, outerName, innerName, newAccess); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { // if (desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) { // disableRedirectionForClass = true; // } return super.visitAnnotation(desc, visible); } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { access = transformAccessForStark(access); return super.visitField(access, name, desc, signature, value); } /** * Insert Constructor specific logic({@link ConstructorRedirection} and * {@link ConstructorBuilder}) for constructor redirecting or * normal method redirecting ({@link MethodRedirection}) for other methods. */ @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { access = transformAccessForStark(access); MethodVisitor defaultVisitor = super.visitMethod(access, name, desc, signature, exceptions); MethodNode method = checkNotNull( getMethodByNameInClass(name, desc, classAndInterfaceNode), "Method found by visitor but not in the pre-parsed class node."); // does the method use blacklisted APIs. boolean hasIncompatibleChange = StarkMethodVerifier.verifyMethod(method) != StarkVerifierStatus.COMPATIBLE; if (hasIncompatibleChange || disableRedirectionForClass || !isAccessCompatibleWithStark(access)) { return defaultVisitor; } if (name.equals(ByteCodeUtils.CLASS_INITIALIZER)) { classInitializerAdded = true; return isInterface ? new ISInterfaceStaticInitializerMethodVisitor( defaultVisitor, access, name, desc) : defaultVisitor; } ArrayList<Type> args = new ArrayList<>(Arrays.asList(Type.getArgumentTypes(desc))); boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; if (!isStatic) { args.add(0, Type.getType(Object.class)); } // Install the Jsr/Ret inliner adapter, we have had reports of code still using the // Jsr/Ret deprecated byte codes. // see https://code.google.com/p/android/issues/detail?id=220019 JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(defaultVisitor, access, name, desc, signature, exceptions); ISAbstractMethodVisitor mv = isInterface ? new ISDefaultMethodVisitor(jsrInlinerAdapter, access, name, desc) : new ISMethodVisitor(jsrInlinerAdapter, access, name, desc); if (name.equals(ByteCodeUtils.CONSTRUCTOR)) { if ((access & Opcodes.ACC_SYNTHETIC) != 0 || ByteCodeUtils.isAnnotatedWith(method, "Lkotlin/jvm/JvmOverloads;")) { return defaultVisitor; } Constructor constructor = ConstructorBuilder.build(visitedClassName, method); LabelNode start = new LabelNode(); method.instructions.insert(constructor.loadThis, start); if (constructor.lineForLoad != -1) { // Record the line number from the start of LOAD_0 for uninitialized 'this'. // This allows a breakpoint to be set at the line with this(...) or super(...) // call in the constructor. method.instructions.insert( constructor.loadThis, new LineNumberNode(constructor.lineForLoad, start)); } mv.addRedirection(new ConstructorRedirection(start, constructor, args)); } else { mv.addRedirection( new MethodRedirection( new LabelNode(mv.getStartLabel()), name + "." + desc, args, Type.getReturnType(desc))); } method.accept(mv); return null; } /** * If a class is package private, make it public so instrumented code living in a different * class loader can instantiate them. * * @param access the original class/method/field access. * @return the new access or the same one depending on the original access rights. */ private static int transformClassAccessForStark(int access) { AccessRight accessRight = AccessRight.fromNodeAccess(access); return accessRight == AccessRight.PACKAGE_PRIVATE ? access | Opcodes.ACC_PUBLIC : access; } /** * If a method/field is not private, make it public. This is to workaround the fact * <p> * <ul> * reload.dex are loaded from a different class loader but private methods/fields are accessed * through reflection, therefore you need visibility. * </ul> * <p> * remember that in Java, protected methods or fields can be accessed by classes in the same * package : {@see https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html} * * @param access the original class/method/field access. * @return the new access or the same one depending on the original access rights. */ private static int transformAccessForStark(int access) { AccessRight accessRight = AccessRight.fromNodeAccess(access); if (accessRight != AccessRight.PRIVATE) { access &= ~Opcodes.ACC_PROTECTED; access &= ~Opcodes.ACC_PRIVATE; return access | Opcodes.ACC_PUBLIC; } return access; } private abstract static class ISAbstractMethodVisitor extends GeneratorAdapter { protected boolean disableRedirection = false; protected int change; protected final List<Type> args; protected final List<Redirection> redirections; protected final Map<Label, Redirection> resolvedRedirections; protected final Label start; public ISAbstractMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(Opcodes.ASM5, mv, access, name, desc); this.change = -1; this.redirections = new ArrayList<>(); this.resolvedRedirections = new HashMap<>(); this.args = new ArrayList<>(Arrays.asList(Type.getArgumentTypes(desc))); this.start = new Label(); boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; // if this is not a static, we add a fictional first parameter what will contain the // "this" reference which can be loaded with ILOAD_0 bytecode. if (!isStatic) { args.add(0, Type.getType(Object.class)); } } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { // if (desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) { // disableRedirection = true; // } return super.visitAnnotation(desc, visible); } /** * inserts a new local '$starkChange' in each method that contains a reference to the type's * IncrementalChange dispatcher, this is done to avoid threading issues. * <p> * Pseudo code: * <code> * $package/IncrementalChange $local1 = $className$.$starkChange; * </code> */ @Override public void visitCode() { if (!disableRedirection) { // Labels cannot be used directly as they are volatile between different visits, // so we must use LabelNode and resolve before visiting for better performance. for (Redirection redirection : redirections) { resolvedRedirections.put(redirection.getPosition().getLabel(), redirection); } super.visitLabel(start); change = newLocal(CHANGE_TYPE); visitChangeField(); storeLocal(change); redirectAt(start); } super.visitCode(); } protected abstract void visitChangeField(); @Override public void visitLabel(Label label) { super.visitLabel(label); redirectAt(label); } protected void redirectAt(Label label) { if (disableRedirection) { return; } Redirection redirection = resolvedRedirections.get(label); if (redirection != null) { // A special line number to mark this area of code. super.visitLineNumber(0, label); redirection.redirect(this, change); } } public void addRedirection(@NonNull Redirection redirection) { redirections.add(redirection); } @Override public void visitLocalVariable( String name, String desc, String signature, Label start, Label end, int index) { // In dex format, the argument names are separated from the local variable names. It // seems to be needed to declare the local argument variables from the beginning of // the methods for dex to pick that up. By inserting code before the first label we // break that. In Java this is fine, and the debugger shows the right thing. However // if we don't readjust the local variables, we just don't see the arguments. if (!disableRedirection && index < args.size()) { start = this.start; } super.visitLocalVariable(name, desc, signature, start, end, index); } public Label getStartLabel() { return start; } } private class ISMethodVisitor extends ISAbstractMethodVisitor { public ISMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(mv, access, name, desc); } @Override protected void visitChangeField() { visitFieldInsn( Opcodes.GETSTATIC, visitedClassName, "$starkChange", getRuntimeTypeName(CHANGE_TYPE)); } } private class ISDefaultMethodVisitor extends ISAbstractMethodVisitor { public ISDefaultMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(mv, access, name, desc); } @Override protected void visitChangeField() { visitFieldInsn( Opcodes.GETSTATIC, visitedClassName, "$starkChange", getRuntimeTypeName(Type.getType(AtomicReference.class))); mv.visitMethodInsn( Opcodes.INVOKEVIRTUAL, "java/util/concurrent/atomic/AtomicReference", "get", "()Ljava/lang/Object;", false); } } private class ISInterfaceStaticInitializerMethodVisitor extends GeneratorAdapter { public ISInterfaceStaticInitializerMethodVisitor( MethodVisitor mv, int access, String name, String desc) { super(Opcodes.ASM5, mv, access, name, desc); } @Override public void visitCode() { addInterfaceClassInitializerCode(this); super.visitCode(); } } /** * Decorated {@link MethodNode} that maintains a reference to the class declaring the method. */ private static class MethodReference { final MethodNode method; final ClassNode owner; private MethodReference(MethodNode method, ClassNode owner) { this.method = method; this.owner = owner; } private String getDefautlDispatchName() { return getDefaultDispatchName(method); } private static String getDefaultDispatchName(MethodNode method) { return method.name + "." + method.desc; } } /*** * Inserts a trampoline to this class so that the updated methods can make calls to super * class methods. * <p> * Pseudo code for this trampoline: * <code> * Object access$super($classType instance, String name, object[] args) { * switch(name) { * case "firstMethod.(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;": * return super~instance.firstMethod((String)arg[0], arg[1]); * case "secondMethod.(Ljava/lang/String;I)V": * return super~instance.firstMethod((String)arg[0], arg[1]); * * default: * StringBuilder $local1 = new StringBuilder(); * $local1.append("Method not found "); * $local1.append(name); * $local1.append(" in " $classType $super implementation"); * throw new $package/StarkReloadException($local1.toString()); * } * </code> */ private void createAccessSuper() { int access = Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_VARARGS; Method m = new Method("access$super", "(L" + visitedClassName + ";Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;"); MethodVisitor visitor = super.visitMethod(access, m.getName(), m.getDescriptor(), null, null); final GeneratorAdapter mv = new GeneratorAdapter(access, m, visitor); // Gather all methods from itself and its superclasses to generate a giant access$super // implementation. // This will work fine as long as we don't support adding methods to a class. final Map<String, MethodReference> uniqueMethods = new HashMap<>(); if (classAndInterfaceNode.hasParent()) { addAllNewMethods( classAndInterfaceNode.getClassNode(), classAndInterfaceNode.getParent(), uniqueMethods); } else { // if we cannot determine the parents for this class, let's blindly add all the // method of the current class as a gateway to a possible parent version. addAllNewMethods( classAndInterfaceNode.getClassNode(), classAndInterfaceNode.getClassNode(), uniqueMethods); } // and gather all default methods of all directly/inherited implemented interfaces. for (AsmInterfaceNode implementedInterface : classAndInterfaceNode.getInterfaces()) { addDefaultMethods( classAndInterfaceNode.getClassNode(), implementedInterface, uniqueMethods); implementedInterface.onAll( interfaceNode -> { addAllNewMethods( classAndInterfaceNode.getClassNode(), interfaceNode, uniqueMethods); return null; }); } new StringSwitch() { @Override void visitString() { mv.visitVarInsn(Opcodes.ALOAD, 1); } @Override void visitCase(String methodName) { MethodReference methodRef = uniqueMethods.get(methodName); mv.visitVarInsn(Opcodes.ALOAD, 0); Type[] args = Type.getArgumentTypes(methodRef.method.desc); int argc = 0; for (Type t : args) { mv.visitVarInsn(Opcodes.ALOAD, 2); mv.push(argc); mv.visitInsn(Opcodes.AALOAD); ByteCodeUtils.unbox(mv, t); argc++; } // if (TRACING_ENABLED) { // trace(mv, "super selected ", methodRef.owner.name, // methodRef.method.name, methodRef.method.desc); // } String parentName = findParentClassForMethod(methodRef); // logger.verbose( // "Generating access$super for %1$s recev %2$s", // methodRef.method.name, // parentName); // Call super on the other object, yup this works cos we are on the right place to // call from. mv.visitMethodInsn(Opcodes.INVOKESPECIAL, parentName, methodRef.method.name, methodRef.method.desc, false); Type ret = Type.getReturnType(methodRef.method.desc); if (ret.getSort() == Type.VOID) { mv.visitInsn(Opcodes.ACONST_NULL); } else { mv.box(ret); } mv.visitInsn(Opcodes.ARETURN); } @Override void visitDefault() { writeMissingMessageWithHash(mv, visitedClassName); } }.visit(mv, uniqueMethods.keySet()); mv.visitMaxs(0, 0); mv.visitEnd(); } /*** * Inserts a trampoline to this class so that the updated methods can make calls to * constructors. * * <p> * Pseudo code for this trampoline: * <code> * ClassName(Object[] args, Marker unused) { * String name = (String) args[0]; * if (name.equals( * "java/lang/ClassName.(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;")) { * this((String)arg[1], arg[2]); * return * } * if (name.equals("SuperClassName.(Ljava/lang/String;I)V")) { * super((String)arg[1], (int)arg[2]); * return; * } * ... * StringBuilder $local1 = new StringBuilder(); * $local1.append("Method not found "); * $local1.append(name); * $local1.append(" in " $classType $super implementation"); * throw new $package/StarkReloadException($local1.toString()); * } * </code> */ private void createDispatchingThis() { // Gather all methods from itself and its superclasses to generate a giant constructor // implementation. // This will work fine as long as we don't support adding constructors to classes. final Map<String, MethodNode> uniqueMethods = new HashMap<>(); addAllNewConstructors( uniqueMethods, classAndInterfaceNode.getClassNode(), true /*keepPrivateConstructors*/); classAndInterfaceNode.onParents( parentClassNode -> { addAllNewConstructors( uniqueMethods, parentClassNode, false /*keepPrivateConstructors*/); return null; }); int access = Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC; Method m = new Method(ByteCodeUtils.CONSTRUCTOR, ConstructorRedirection.DISPATCHING_THIS_SIGNATURE); MethodVisitor visitor = super.visitMethod(0, m.getName(), m.getDescriptor(), null, null); final GeneratorAdapter mv = new GeneratorAdapter(access, m, visitor); mv.visitCode(); // Mark this code as redirection code Label label = new Label(); mv.visitLineNumber(0, label); // Get and store the constructor canonical name. mv.visitVarInsn(Opcodes.ALOAD, 1); mv.push(1); mv.visitInsn(Opcodes.AALOAD); mv.unbox(Type.getType("Ljava/lang/String;")); final int constructorCanonicalName = mv.newLocal(Type.getType("Ljava/lang/String;")); mv.storeLocal(constructorCanonicalName); new StringSwitch() { @Override void visitString() { mv.loadLocal(constructorCanonicalName); } @Override void visitCase(String canonicalName) { MethodNode methodNode = uniqueMethods.get(canonicalName); String owner = canonicalName.split("\\.")[0]; // Parse method arguments and mv.visitVarInsn(Opcodes.ALOAD, 0); Type[] args = Type.getArgumentTypes(methodNode.desc); int argc = 1; for (Type t : args) { mv.visitVarInsn(Opcodes.ALOAD, 1); mv.push(argc + 1); mv.visitInsn(Opcodes.AALOAD); ByteCodeUtils.unbox(mv, t); argc++; } mv.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, ByteCodeUtils.CONSTRUCTOR, methodNode.desc, false); mv.visitInsn(Opcodes.RETURN); } @Override void visitDefault() { writeMissingMessageWithHash(mv, visitedClassName); } }.visit(mv, uniqueMethods.keySet()); mv.visitMaxs(1, 3); mv.visitEnd(); } /** * add a static initializer to java8 interfaces (this visitor will only be called when default * methods are present). */ private void addInterfaceClassInitializer() { if (classInitializerAdded) { return; } MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null); mv.visitCode(); addInterfaceClassInitializerCode(mv); mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(3, 0); mv.visitEnd(); } private void addInterfaceClassInitializerCode(MethodVisitor mv) { mv.visitTypeInsn(Opcodes.NEW, "java/util/concurrent/atomic/AtomicReference"); mv.visitInsn(Opcodes.DUP); mv.visitInsn(Opcodes.ACONST_NULL); mv.visitMethodInsn( Opcodes.INVOKESPECIAL, "java/util/concurrent/atomic/AtomicReference", "<init>", "(Ljava/lang/Object;)V", false); mv.visitFieldInsn( Opcodes.PUTSTATIC, visitedClassName, "$starkChange", "Ljava/util/concurrent/atomic/AtomicReference;"); } @Override public void visitEnd() { createAccessSuper(); if (isInterface) { addInterfaceClassInitializer(); } else { createDispatchingThis(); } super.visitEnd(); } /** * Find a suitable parent for a method reference. The method owner is not always a valid * parent to dispatch to. For instance, take the following example : * <code> * package a; * public class A { * public void publicMethod(); * } * <p> * package a; * class B extends A { * public void publicMethod(); * } * <p> * package b; * public class C extends B { * ... * } * </code> * when instrumenting C, the first method reference for "publicMethod" is on class B which we * cannot invoke directly since it's present on a private package B which is not located in the * same package as C. However C can still call the "publicMethod" since it's defined on A which * is a public class. * <p> * We cannot just blindly take the top most definition of "publicMethod" hoping this is the * accessible one since you can very well do : * <code> * package a; * class A { * public void publicMethod(); * } * <p> * package a; * public class B extends A { * public void publicMethod(); * } * <p> * package b; * public class C extends B { * ... * } * </code> * <p> * In that case, the top most parent class is the one defined the unaccessible method reference. * <p> * Therefore, the solution is to walk up the hierarchy until we find the same method defined on * an accessible class, if we cannot find such a method, the suitable parent is the parent class * of the visited class which is legal (but might consume a DEX id). * * @param methodReference the method reference to find a suitable parent for. * @return the parent class name */ @NonNull String findParentClassForMethod(@NonNull MethodReference methodReference) { // logger.verbose( // "MethodRef %1$s access(%2$s) -> owner %3$s access(%4$s)", // methodReference.method.name, methodReference.method.access, // methodReference.owner.name, methodReference.owner.access); // if the method owner class is accessible from the visited class, just use that. if (isParentClassVisible(methodReference.owner, classAndInterfaceNode.getClassNode())) { if (!classAndInterfaceNode.isInterface(methodReference.owner)) { return methodReference.owner.name; } } // logger.verbose("Found an inaccessible methodReference %1$s", methodReference.method.name); // walk up the hierarchy, starting at the method reference owner. if (classAndInterfaceNode.hasParent()) { AsmClassNode asmClassNode = classAndInterfaceNode.getParent(); while (!asmClassNode.getClassNode().name.equals(methodReference.owner.name) && asmClassNode.hasParent()) { asmClassNode = asmClassNode.getParent(); } if (asmClassNode.hasParent()) { String selectedParent = asmClassNode.onParents( parentClassNode -> { // check that this parent is visible, there might be several layers of package // private classes. if (isParentClassVisible( parentClassNode, classAndInterfaceNode.getClassNode())) { //noinspection unchecked: ASM API for (MethodNode methodNode : (List<MethodNode>) parentClassNode.methods) { // do not reference bridge methods, they might not be translated into dex, or // might disappear in the next javac compiler for that use case. if (methodNode.name.equals(methodReference.method.name) && methodNode.desc.equals( methodReference.method.desc) && (methodNode.access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT)) == 0) { // logger.verbose( // "Using class %1$s for dispatching %2$s:%3$s", // parentClassNode.name, // methodReference.method.name, // methodReference.method.desc); return parentClassNode.name; } } } return null; }); if (selectedParent != null) { return selectedParent; } } } // logger.verbose("Using immediate parent for dispatching %1$s", methodReference.method.desc); return classAndInterfaceNode.getClassNode().superName; } private static boolean isParentClassVisible(@NonNull ClassNode parent, @NonNull ClassNode child) { return ((parent.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0 || (ByteCodeUtils.getPackageName(parent.name).equals( ByteCodeUtils.getPackageName(child.name)))); } /** * Add all unseen method from the passed ClassNode's methods and implemented interfaces. * * @param instrumentedClass class that is being visited * @param superClass the class to save all directly implemented methods as well as default * methods from implemented interfaces from * @param methods the methods already encountered in the ClassNode hierarchy * @see ClassNode#methods */ private void addAllNewMethods( ClassNode instrumentedClass, AsmClassNode superClass, Map<String, MethodReference> methods) { superClass.onAll( classNode -> { addAllNewMethods(instrumentedClass, classNode, methods); return null; }); } /** * Add all unseen methods from the passed ClassNode's methods. * * @param instrumentedClass class that is being visited * @param superClass the class to save all new methods from * @param methods the methods already encountered in the ClassNode hierarchy * @see ClassNode#methods */ private List<MethodReference> addAllNewMethods( ClassNode instrumentedClass, ClassNode superClass, Map<String, MethodReference> methods) { ImmutableList.Builder<MethodReference> methodRefs = ImmutableList.builder(); //noinspection unchecked for (MethodNode method : (List<MethodNode>) superClass.methods) { MethodReference methodRef = addNewMethod(instrumentedClass, superClass, method, methods); if (methodRef != null) { methodRefs.add(methodRef); } } return methodRefs.build(); } /** * Adds all default method for the passed implemented interface of a class as well as all * default methods from any interface extended by the passed implemented interface. * * @param instrumentedClass the class we are bytecode instrumenting. * @param implementedInterface the interface that might contain default methods. * @param methods map of methods we already encountered on the instrumentedClass implementation. * @return nothing */ private Void addDefaultMethods( ClassNode instrumentedClass, AsmInterfaceNode implementedInterface, Map<String, MethodReference> methods) { Map<String, MethodReference> visitedInterfaceMethods = new HashMap<>(); implementedInterface.onAll( interfaceNode -> { addAllNewMethods(instrumentedClass, interfaceNode, methods) .forEach( methodRef -> { visitedInterfaceMethods.put( methodRef.getDefautlDispatchName(), methodRef); }); return null; }); // now make sure we have a gateway for inherited default methods that are not present // on the derived interface as calls can be made using the derived interface as the owner. for (MethodNode methodNode : (List<MethodNode>) implementedInterface.getClassNode().methods) { visitedInterfaceMethods.remove(MethodReference.getDefaultDispatchName(methodNode)); } // all the remaining methods should be added under this interface to make sure we calling // all possible owners for this method. for (MethodReference methodReference : visitedInterfaceMethods.values()) { addNewMethod( implementedInterface.getClassNode().name + "." + methodReference.getDefautlDispatchName(), instrumentedClass, implementedInterface.getClassNode(), methodReference.method, methods); } return null; } /** * Add a new method in the list of unseen methods in the instrumentedClass hierarchy. * * @param instrumentedClass class that is being visited * @param superClass the class or interface defining the passed method. * @param method the method to study * @param methods the methods arlready encountered in the ClassNode hierarchy * @return the newly added {@link MethodReference} of null if the method was not added for any * reason. */ @Nullable private static MethodReference addNewMethod( ClassNode instrumentedClass, ClassNode superClass, MethodNode method, Map<String, MethodReference> methods) { if (method.name.equals(ByteCodeUtils.CONSTRUCTOR) || method.name.equals(ByteCodeUtils.CLASS_INITIALIZER)) { return null; } String name = MethodReference.getDefaultDispatchName(method); // if we are dealing with an interface default method we should always provide a // way to invoke it through the access$super. It is possible that this default method // is overriden by a super class but since several interfaces can implement the same // method name, you can easily end up with a partial override (overriding some of the // implemented interfaces methods but not all of them). if ((superClass.access & Opcodes.ACC_INTERFACE) != 0 && isParentClassVisible(superClass, instrumentedClass)) { // all default methods name will use the defining interface name for the hashcode // to avoid name collision when dealing with 2 methods with the same names coming // from 2 distinct interfaces. name = superClass.name + "." + name; } return addNewMethod(name, instrumentedClass, superClass, method, methods); } /** * Add a new method in the list of unseen methods in the instrumentedClass hierarchy. * * @param name the dispatching name that will be used for matching callers to the passed method. * @param instrumentedClass class that is being visited * @param superClass the class or interface defining the passed method. * @param method the method to study * @param methods the methods arlready encountered in the ClassNode hierarchy * @return the newly added {@link MethodReference} of null if the method was not added for any * reason. */ @Nullable private static MethodReference addNewMethod( String name, ClassNode instrumentedClass, ClassNode superClass, MethodNode method, Map<String, MethodReference> methods) { if (isAccessCompatibleWithStark(method.access) && !methods.containsKey(name) && (method.access & Opcodes.ACC_STATIC) == 0 && isCallableFromSubclass(method, superClass, instrumentedClass)) { MethodReference methodReference = new MethodReference(method, superClass); methods.put(name, methodReference); return methodReference; } return null; } @SuppressWarnings("SimplifiableIfStatement") private static boolean isCallableFromSubclass( @NonNull MethodNode method, @NonNull ClassNode superClass, @NonNull ClassNode subclass) { if ((method.access & Opcodes.ACC_PRIVATE) != 0) { return false; } else if ((method.access & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) != 0) { return true; } else { // "package private" access modifier. return ByteCodeUtils.getPackageName(superClass.name).equals(ByteCodeUtils.getPackageName(subclass.name)); } } /** * Add all constructors from the passed ClassNode's methods. {@see ClassNode#methods} * * @param methods the constructors already encountered in the ClassNode hierarchy * @param classNode the class to save all new methods from. * @param keepPrivateConstructors whether to keep the private constructors. */ private void addAllNewConstructors(Map<String, MethodNode> methods, ClassNode classNode, boolean keepPrivateConstructors) { //noinspection unchecked for (MethodNode method : (List<MethodNode>) classNode.methods) { if (!method.name.equals(ByteCodeUtils.CONSTRUCTOR)) { continue; } if (!isAccessCompatibleWithStark(method.access)) { continue; } if (!keepPrivateConstructors && (method.access & Opcodes.ACC_PRIVATE) != 0) { continue; } if (!classNode.name.equals(visitedClassName) && !classNode.name.equals(visitedSuperName)) { continue; } String key = classNode.name + "." + method.desc; if (methods.containsKey(key)) { continue; } methods.put(key, method); } } }