1 /* 2 * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 25 package org.graalvm.compiler.core.test; 26 27 import static java.lang.String.format; 28 29 import java.io.IOException; 30 import java.lang.reflect.Constructor; 31 import java.lang.reflect.Executable; 32 import java.lang.reflect.Method; 33 import java.util.Arrays; 34 import java.util.HashSet; 35 import java.util.Set; 36 37 import org.graalvm.compiler.options.OptionDescriptor; 38 import org.graalvm.compiler.options.OptionDescriptors; 39 import org.graalvm.compiler.options.OptionKey; 40 import org.graalvm.compiler.options.OptionsParser; 41 import org.graalvm.compiler.serviceprovider.GraalServices; 42 import org.junit.Test; 43 import org.objectweb.asm.ClassReader; 44 import org.objectweb.asm.ClassVisitor; 45 import org.objectweb.asm.Label; 46 import org.objectweb.asm.MethodVisitor; 47 import org.objectweb.asm.Opcodes; 48 import org.objectweb.asm.Type; 49 50 /** 51 * Verifies a class declaring one or more {@linkplain OptionKey options} has a class initializer 52 * that only initializes the option(s). This sanity check mitigates the possibility of an option 53 * value being used before being set. 54 */ 55 public class OptionsVerifierTest { 56 57 @Test 58 public void verifyOptions() throws IOException { 59 HashSet<Class<?>> checked = new HashSet<>(); 60 for (OptionDescriptors opts : OptionsParser.getOptionsLoader()) { 61 for (OptionDescriptor desc : opts) { 62 OptionsVerifier.checkClass(desc.getDeclaringClass(), desc, checked); 63 } 64 } 65 } 66 67 static final class OptionsVerifier extends ClassVisitor { 68 69 public static void checkClass(Class<?> cls, OptionDescriptor option, Set<Class<?>> checked) throws IOException { 70 if (!checked.contains(cls)) { 71 checked.add(cls); 72 Class<?> superclass = cls.getSuperclass(); 73 if (superclass != null && !superclass.equals(Object.class)) { 74 checkClass(superclass, option, checked); 75 } 76 77 GraalServices.getClassfileAsStream(cls); 78 ClassReader cr = new ClassReader(GraalServices.getClassfileAsStream(cls)); 79 80 ClassVisitor cv = new OptionsVerifier(cls, option); 81 cr.accept(cv, 0); 82 } 83 } 84 85 /** 86 * The option field context of the verification. 87 */ 88 private final OptionDescriptor option; 89 90 /** 91 * The class in which {@link #option} is declared or a super-class of that class. This is 92 * the class whose {@code <clinit>} method is being verified. 93 */ 94 private final Class<?> cls; 95 96 /** 97 * Source file context for error reporting. 98 */ 99 String sourceFile = null; 100 101 /** 102 * Line number for error reporting. 103 */ 104 int lineNo = -1; 105 106 final Class<?>[] boxingTypes = {Boolean.class, Byte.class, Short.class, Character.class, Integer.class, Float.class, Long.class, Double.class}; 107 108 private static Class<?> resolve(String name) { 109 try { 110 return Class.forName(name.replace('/', '.')); 111 } catch (ClassNotFoundException e) { 112 throw new InternalError(e); 113 } 114 } 115 116 OptionsVerifier(Class<?> cls, OptionDescriptor desc) { 117 super(Opcodes.ASM5); 118 this.cls = cls; 119 this.option = desc; 120 } 121 122 @Override 123 public void visitSource(String source, String debug) { 124 this.sourceFile = source; 125 } 126 127 void verify(boolean condition, String message) { 128 if (!condition) { 129 error(message); 130 } 131 } 132 133 void error(String message) { 134 String errorMessage = format( 135 "%s:%d: Illegal code in %s.<clinit> which may be executed when %s.%s is initialized:%n%n %s%n%n" + "The recommended solution is to move " + option.getName() + 136 " into a separate class (e.g., %s.Options).%n", 137 sourceFile, lineNo, cls.getSimpleName(), option.getDeclaringClass().getSimpleName(), option.getName(), 138 message, option.getDeclaringClass().getSimpleName()); 139 throw new InternalError(errorMessage); 140 141 } 142 143 @Override 144 public MethodVisitor visitMethod(int access, String name, String d, String signature, String[] exceptions) { 145 if (name.equals("<clinit>")) { 146 return new MethodVisitor(Opcodes.ASM5) { 147 148 @Override 149 public void visitLineNumber(int line, Label start) { 150 lineNo = line; 151 } 152 153 @Override 154 public void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDesc) { 155 if (opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC) { 156 verify(resolve(owner).equals(option.getDeclaringClass()), format("store to field %s.%s", resolve(owner).getSimpleName(), fieldName)); 157 verify(opcode != Opcodes.PUTFIELD, format("store to non-static field %s.%s", resolve(owner).getSimpleName(), fieldName)); 158 } 159 } 160 161 private Executable resolveMethod(String owner, String methodName, String methodDesc) { 162 Class<?> declaringClass = resolve(owner); 163 if (methodName.equals("<init>")) { 164 for (Constructor<?> c : declaringClass.getDeclaredConstructors()) { 165 if (methodDesc.equals(Type.getConstructorDescriptor(c))) { 166 return c; 167 } 168 } 169 } else { 170 Type[] argumentTypes = Type.getArgumentTypes(methodDesc); 171 for (Method m : declaringClass.getDeclaredMethods()) { 172 if (m.getName().equals(methodName)) { 173 if (Arrays.equals(argumentTypes, Type.getArgumentTypes(m))) { 174 if (Type.getReturnType(methodDesc).equals(Type.getReturnType(m))) { 175 return m; 176 } 177 } 178 } 179 } 180 } 181 throw new NoSuchMethodError(declaringClass + "." + methodName + methodDesc); 182 } 183 184 /** 185 * Checks whether a given method is allowed to be called. 186 */ 187 private boolean checkInvokeTarget(Executable method) { 188 Class<?> holder = method.getDeclaringClass(); 189 if (method instanceof Constructor) { 190 if (OptionKey.class.isAssignableFrom(holder)) { 191 return true; 192 } 193 } else if (Arrays.asList(boxingTypes).contains(holder)) { 194 return method.getName().equals("valueOf"); 195 } else if (method.getDeclaringClass().equals(Class.class)) { 196 return method.getName().equals("desiredAssertionStatus"); 197 } 198 return false; 199 } 200 201 @Override 202 public void visitMethodInsn(int opcode, String owner, String methodName, String methodDesc, boolean itf) { 203 Executable callee = resolveMethod(owner, methodName, methodDesc); 204 verify(checkInvokeTarget(callee), "invocation of " + callee); 205 } 206 }; 207 } else { 208 return null; 209 } 210 } 211 } 212 213 }