1 /* 2 * Copyright (c) 2014, 2018, 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.ByteArrayOutputStream; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.lang.reflect.Constructor; 34 import java.lang.reflect.Executable; 35 import java.lang.reflect.Method; 36 import java.net.URL; 37 import java.net.URLClassLoader; 38 import java.nio.file.Files; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.HashSet; 42 import java.util.LinkedHashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.Set; 47 48 import org.graalvm.compiler.options.OptionDescriptor; 49 import org.graalvm.compiler.options.OptionDescriptors; 50 import org.graalvm.compiler.options.OptionKey; 51 import org.graalvm.compiler.options.OptionsParser; 52 import org.graalvm.compiler.serviceprovider.JavaVersionUtil; 53 import org.junit.Test; 54 import org.objectweb.asm.ClassReader; 55 import org.objectweb.asm.ClassVisitor; 56 import org.objectweb.asm.Label; 57 import org.objectweb.asm.MethodVisitor; 58 import org.objectweb.asm.Opcodes; 59 import org.objectweb.asm.Type; 60 61 /** 62 * Verifies a class declaring one or more {@linkplain OptionKey options} has a class initializer 63 * that only initializes the option(s). This sanity check mitigates the possibility of an option 64 * value being used before being set. 65 */ 66 public class OptionsVerifierTest { 67 68 @Test 69 public void verifyOptions() throws IOException { 70 try (Classpath cp = new Classpath()) { 71 HashSet<Class<?>> checked = new HashSet<>(); 72 for (OptionDescriptors opts : OptionsParser.getOptionsLoader()) { 73 for (OptionDescriptor desc : opts) { 74 OptionsVerifier.checkClass(desc.getDeclaringClass(), desc, checked, cp); 75 } 76 } 77 } 78 } 79 80 static class Classpath implements AutoCloseable { 81 private final Map<String, Object> entries = new LinkedHashMap<>(); 82 83 Classpath() throws IOException { 84 List<String> names = new ArrayList<>(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator))); 85 if (JavaVersionUtil.JAVA_SPEC <= 8) { 86 names.addAll(Arrays.asList(System.getProperty("sun.boot.class.path").split(File.pathSeparator))); 87 } else { 88 names.addAll(Arrays.asList(System.getProperty("jdk.module.path").split(File.pathSeparator))); 89 } 90 for (String n : names) { 91 File path = new File(n); 92 if (path.exists()) { 93 if (path.isDirectory()) { 94 entries.put(n, path); 95 } else if (n.endsWith(".jar") || n.endsWith(".zip")) { 96 URL url = new URL("jar", "", "file:" + n + "!/"); 97 entries.put(n, new URLClassLoader(new URL[]{url})); 98 } 99 } 100 } 101 } 102 103 @Override 104 public void close() throws IOException { 105 for (Object e : entries.values()) { 106 if (e instanceof URLClassLoader) { 107 ((URLClassLoader) e).close(); 108 } 109 } 110 } 111 112 public byte[] getInputStream(String classFilePath) throws IOException { 113 for (Object e : entries.values()) { 114 if (e instanceof File) { 115 File path = new File((File) e, classFilePath.replace('/', File.separatorChar)); 116 if (path.exists()) { 117 return Files.readAllBytes(path.toPath()); 118 } 119 } else { 120 assert e instanceof URLClassLoader; 121 URLClassLoader ucl = (URLClassLoader) e; 122 try (InputStream in = ucl.getResourceAsStream(classFilePath)) { 123 if (in != null) { 124 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 125 int nRead; 126 byte[] data = new byte[1024]; 127 while ((nRead = in.read(data, 0, data.length)) != -1) { 128 buffer.write(data, 0, nRead); 129 } 130 return buffer.toByteArray(); 131 } 132 } 133 } 134 } 135 return null; 136 } 137 } 138 139 static final class OptionsVerifier extends ClassVisitor { 140 141 public static void checkClass(Class<?> cls, OptionDescriptor option, Set<Class<?>> checked, Classpath cp) throws IOException { 142 if (!checked.contains(cls)) { 143 checked.add(cls); 144 Class<?> superclass = cls.getSuperclass(); 145 if (superclass != null && !superclass.equals(Object.class)) { 146 checkClass(superclass, option, checked, cp); 147 } 148 149 String classFilePath = cls.getName().replace('.', '/') + ".class"; 150 ClassReader cr = new ClassReader(Objects.requireNonNull(cp.getInputStream(classFilePath), "Could not find class file for " + cls.getName())); 151 152 ClassVisitor cv = new OptionsVerifier(cls, option); 153 cr.accept(cv, 0); 154 } 155 } 156 157 /** 158 * The option field context of the verification. 159 */ 160 private final OptionDescriptor option; 161 162 /** 163 * The class in which {@link #option} is declared or a super-class of that class. This is 164 * the class whose {@code <clinit>} method is being verified. 165 */ 166 private final Class<?> cls; 167 168 /** 169 * Source file context for error reporting. 170 */ 171 String sourceFile = null; 172 173 /** 174 * Line number for error reporting. 175 */ 176 int lineNo = -1; 177 178 final Class<?>[] boxingTypes = {Boolean.class, Byte.class, Short.class, Character.class, Integer.class, Float.class, Long.class, Double.class}; 179 180 private static Class<?> resolve(String name) { 181 try { 182 return Class.forName(name.replace('/', '.')); 183 } catch (ClassNotFoundException e) { 184 throw new InternalError(e); 185 } 186 } 187 188 OptionsVerifier(Class<?> cls, OptionDescriptor desc) { 189 super(Opcodes.ASM5); 190 this.cls = cls; 191 this.option = desc; 192 } 193 194 @Override 195 public void visitSource(String source, String debug) { 196 this.sourceFile = source; 197 } 198 199 void verify(boolean condition, String message) { 200 if (!condition) { 201 error(message); 202 } 203 } 204 205 void error(String message) { 206 String errorMessage = format( 207 "%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() + 208 " into a separate class (e.g., %s.Options).%n", 209 sourceFile, lineNo, cls.getSimpleName(), option.getDeclaringClass().getSimpleName(), option.getName(), 210 message, option.getDeclaringClass().getSimpleName()); 211 throw new InternalError(errorMessage); 212 213 } 214 215 @Override 216 public MethodVisitor visitMethod(int access, String name, String d, String signature, String[] exceptions) { 217 if (name.equals("<clinit>")) { 218 return new MethodVisitor(Opcodes.ASM5) { 219 220 @Override 221 public void visitLineNumber(int line, Label start) { 222 lineNo = line; 223 } 224 225 @Override 226 public void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDesc) { 227 if (opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC) { 228 verify(resolve(owner).equals(option.getDeclaringClass()), format("store to field %s.%s", resolve(owner).getSimpleName(), fieldName)); 229 verify(opcode != Opcodes.PUTFIELD, format("store to non-static field %s.%s", resolve(owner).getSimpleName(), fieldName)); 230 } 231 } 232 233 private Executable resolveMethod(String owner, String methodName, String methodDesc) { 234 Class<?> declaringClass = resolve(owner); 235 if (methodName.equals("<init>")) { 236 for (Constructor<?> c : declaringClass.getDeclaredConstructors()) { 237 if (methodDesc.equals(Type.getConstructorDescriptor(c))) { 238 return c; 239 } 240 } 241 } else { 242 Type[] argumentTypes = Type.getArgumentTypes(methodDesc); 243 for (Method m : declaringClass.getDeclaredMethods()) { 244 if (m.getName().equals(methodName)) { 245 if (Arrays.equals(argumentTypes, Type.getArgumentTypes(m))) { 246 if (Type.getReturnType(methodDesc).equals(Type.getReturnType(m))) { 247 return m; 248 } 249 } 250 } 251 } 252 } 253 throw new NoSuchMethodError(declaringClass + "." + methodName + methodDesc); 254 } 255 256 /** 257 * Checks whether a given method is allowed to be called. 258 */ 259 private boolean checkInvokeTarget(Executable method) { 260 Class<?> holder = method.getDeclaringClass(); 261 if (method instanceof Constructor) { 262 if (OptionKey.class.isAssignableFrom(holder)) { 263 return true; 264 } 265 } else if (Arrays.asList(boxingTypes).contains(holder)) { 266 return method.getName().equals("valueOf"); 267 } else if (method.getDeclaringClass().equals(Class.class)) { 268 return method.getName().equals("desiredAssertionStatus"); 269 } 270 return false; 271 } 272 273 @Override 274 public void visitMethodInsn(int opcode, String owner, String methodName, String methodDesc, boolean itf) { 275 Executable callee = resolveMethod(owner, methodName, methodDesc); 276 verify(checkInvokeTarget(callee), "invocation of " + callee); 277 } 278 }; 279 } else { 280 return null; 281 } 282 } 283 } 284 285 }