1 /*
   2  * Copyright (c) 2017, 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 import com.sun.tools.classfile.*;
  25 import com.sun.tools.jdeps.ClassFileReader;
  26 import static com.sun.tools.classfile.ConstantPool.*;
  27 import java.io.File;
  28 import java.io.IOException;
  29 import java.io.UncheckedIOException;
  30 import java.net.URI;
  31 import java.nio.file.FileSystem;
  32 import java.nio.file.FileSystems;
  33 import java.nio.file.Files;
  34 import java.nio.file.Path;
  35 import java.nio.file.Paths;
  36 import java.util.ArrayList;
  37 import java.util.HashSet;
  38 import java.util.List;
  39 import java.util.Set;
  40 import java.util.concurrent.Callable;
  41 import java.util.concurrent.ConcurrentSkipListSet;
  42 import java.util.concurrent.ExecutionException;
  43 import java.util.concurrent.ExecutorService;
  44 import java.util.concurrent.Executors;
  45 import java.util.concurrent.FutureTask;
  46 import java.util.stream.Collectors;
  47 import java.util.stream.Stream;
  48 
  49 /*
  50  * @test
  51  * @summary CallerSensitive methods should be static or final instance
  52  *          methods except the known list of non-final instance methods
  53  * @modules jdk.jdeps/com.sun.tools.classfile
  54  *          jdk.jdeps/com.sun.tools.jdeps
  55  * @build CheckCSMs
  56  * @run main/othervm/timeout=900 CheckCSMs
  57  */
  58 public class CheckCSMs {
  59     private static int numThreads = 3;
  60     private static boolean listCSMs = false;
  61     private final ExecutorService pool;
  62 
  63     // The goal is to remove this list of Non-final instance @CS methods
  64     // over time.  Do not add any new one to this list.
  65     private static Set<String> KNOWN_NON_FINAL_CSMS =
  66       Set.of("java/io/ObjectStreamField#getType ()Ljava/lang/Class;",
  67              "java/io/ObjectStreamClass#forClass ()Ljava/lang/Class;",
  68              "java/lang/Runtime#load (Ljava/lang/String;)V",
  69              "java/lang/Runtime#loadLibrary (Ljava/lang/String;)V",
  70              "java/lang/Thread#getContextClassLoader ()Ljava/lang/ClassLoader;",
  71              "javax/sql/rowset/serial/SerialJavaObject#getFields ()[Ljava/lang/reflect/Field;"
  72       );
  73 
  74     public static void main(String[] args) throws Exception {
  75         if (args.length > 0 && args[0].equals("--list")) {
  76             listCSMs = true;
  77         }
  78 
  79         CheckCSMs checkCSMs = new CheckCSMs();
  80         Set<String> result = checkCSMs.run(getPlatformClasses());
  81         if (!KNOWN_NON_FINAL_CSMS.equals(result)) {
  82             Set<String> diff = new HashSet<>(result);
  83             diff.removeAll(KNOWN_NON_FINAL_CSMS);
  84             throw new RuntimeException("Unexpected non-final instance method: " +
  85                 result.stream().sorted()
  86                       .collect(Collectors.joining("\n", "\n", "")));
  87         }
  88     }
  89 
  90     private final Set<String> nonFinalCSMs = new ConcurrentSkipListSet<>();
  91     private final ReferenceFinder finder;
  92     public CheckCSMs() {
  93         this.finder = new ReferenceFinder(getFilter(), getVisitor());
  94         pool = Executors.newFixedThreadPool(numThreads);
  95 
  96     }
  97 
  98     public Set<String> run(Stream<Path> classes)
  99         throws IOException, InterruptedException, ExecutionException,
 100                ConstantPoolException
 101     {
 102         classes.forEach(this::processPath);
 103         waitForCompletion();
 104         pool.shutdown();
 105         return nonFinalCSMs;
 106     }
 107 
 108 
 109     private ReferenceFinder.Filter getFilter() {
 110         final String classname = "jdk/internal/reflect/Reflection";
 111         final String method = "getCallerClass";
 112         return new ReferenceFinder.Filter() {
 113             public boolean accept(ConstantPool cpool, CPRefInfo cpref) {
 114                 try {
 115                     CONSTANT_NameAndType_info nat = cpref.getNameAndTypeInfo();
 116                     return cpref.getClassName().equals(classname) && nat.getName().equals(method);
 117                 } catch (ConstantPoolException ex) {
 118                     throw new RuntimeException(ex);
 119                 }
 120             }
 121         };
 122     }
 123 
 124     private ReferenceFinder.Visitor getVisitor() {
 125         return new ReferenceFinder.Visitor() {
 126             public void visit(ClassFile cf, Method m,  List<CPRefInfo> refs) {
 127                 try {
 128                     // ignore jdk.unsupported/sun.reflect.Reflection.getCallerClass
 129                     // which is a "special" delegate to the internal getCallerClass
 130                     if (cf.getName().equals("sun/reflect/Reflection") &&
 131                         m.getName(cf.constant_pool).equals("getCallerClass"))
 132                         return;
 133 
 134                     String name = String.format("%s#%s %s", cf.getName(),
 135                                                 m.getName(cf.constant_pool),
 136                                                 m.descriptor.getValue(cf.constant_pool));
 137                     if (!CheckCSMs.isStaticOrFinal(cf, m, cf.constant_pool)) {
 138                         System.err.println("Unsupported @CallerSensitive: " + name);
 139                         nonFinalCSMs.add(name);
 140                     } else {
 141                         if (listCSMs) {
 142                             System.out.format("@CS  %s%n", name);
 143                         }
 144                     }
 145                 } catch (ConstantPoolException ex) {
 146                     throw new RuntimeException(ex);
 147                 }
 148             }
 149         };
 150     }
 151 
 152     void processPath(Path path) {
 153         try {
 154             ClassFileReader reader = ClassFileReader.newInstance(path);
 155             for (ClassFile cf : reader.getClassFiles()) {
 156                 if (cf.access_flags.is(AccessFlags.ACC_MODULE))
 157                     continue;
 158 
 159                 String classFileName = cf.getName();
 160                 // for each ClassFile
 161                 //    parse constant pool to find matching method refs
 162                 //      parse each method (caller)
 163                 //      - visit and find method references matching the given method name
 164                 pool.submit(getTask(cf));
 165             }
 166         } catch (IOException x) {
 167             throw new UncheckedIOException(x);
 168         } catch (ConstantPoolException x) {
 169             throw new RuntimeException(x);
 170         }
 171     }
 172 
 173     private static final String CALLER_SENSITIVE_ANNOTATION
 174         = "Ljdk/internal/reflect/CallerSensitive;";
 175 
 176     private static boolean isCallerSensitive(Method m, ConstantPool cp)
 177         throws ConstantPoolException
 178     {
 179         RuntimeAnnotations_attribute attr =
 180             (RuntimeAnnotations_attribute)m.attributes.get(Attribute.RuntimeVisibleAnnotations);
 181         if (attr != null) {
 182             for (int i = 0; i < attr.annotations.length; i++) {
 183                 Annotation ann = attr.annotations[i];
 184                 String annType = cp.getUTF8Value(ann.type_index);
 185                 if (CALLER_SENSITIVE_ANNOTATION.equals(annType)) {
 186                     return true;
 187                 }
 188             }
 189         }
 190         return false;
 191     }
 192 
 193     private static boolean isStaticOrFinal(ClassFile cf, Method m, ConstantPool cp)
 194         throws ConstantPoolException
 195     {
 196         if (!isCallerSensitive(m, cp))
 197             return false;
 198 
 199         // either a static method or a final instance method
 200         return m.access_flags.is(AccessFlags.ACC_STATIC) ||
 201                m.access_flags.is(AccessFlags.ACC_FINAL) ||
 202                cf.access_flags.is(AccessFlags.ACC_FINAL);
 203     }
 204 
 205     private final List<FutureTask<Void>> tasks = new ArrayList<FutureTask<Void>>();
 206     private FutureTask<Void> getTask(final ClassFile cf) {
 207         FutureTask<Void> task = new FutureTask<Void>(new Callable<Void>() {
 208             public Void call() throws Exception {
 209                 finder.parse(cf);
 210                 return null;
 211             }
 212         });
 213         tasks.add(task);
 214         return task;
 215     }
 216 
 217     private void waitForCompletion() throws InterruptedException, ExecutionException {
 218         for (FutureTask<Void> t : tasks) {
 219             t.get();
 220         }
 221         if (tasks.isEmpty()) {
 222             throw new RuntimeException("No classes found, or specified.");
 223         }
 224         System.out.println("Parsed " + tasks.size() + " classfiles");
 225     }
 226 
 227     static Stream<Path> getPlatformClasses() throws IOException {
 228         Path home = Paths.get(System.getProperty("java.home"));
 229 
 230         // Either an exploded build or an image.
 231         File classes = home.resolve("modules").toFile();
 232         if (classes.isDirectory()) {
 233             return Stream.of(classes.toPath());
 234         } else {
 235             return jrtPaths();
 236         }
 237     }
 238 
 239     static Stream<Path> jrtPaths() {
 240         FileSystem jrt = FileSystems.getFileSystem(URI.create("jrt:/"));
 241         Path root = jrt.getPath("/");
 242 
 243         try {
 244             return Files.walk(root)
 245                     .filter(p -> p.getNameCount() > 1)
 246                     .filter(p -> p.toString().endsWith(".class"));
 247         } catch (IOException x) {
 248             throw new UncheckedIOException(x);
 249         }
 250     }
 251 }