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