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
24import com.sun.tools.classfile.*;
25import com.sun.tools.jdeps.ClassFileReader;
26import static com.sun.tools.classfile.ConstantPool.*;
27import java.io.File;
28import java.io.IOException;
29import java.io.UncheckedIOException;
30import java.net.URI;
31import java.nio.file.FileSystem;
32import java.nio.file.FileSystems;
33import java.nio.file.Files;
34import java.nio.file.Path;
35import java.nio.file.Paths;
36import java.util.ArrayList;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Set;
40import java.util.concurrent.Callable;
41import java.util.concurrent.ConcurrentSkipListSet;
42import java.util.concurrent.ExecutionException;
43import java.util.concurrent.ExecutorService;
44import java.util.concurrent.Executors;
45import java.util.concurrent.FutureTask;
46import java.util.stream.Collectors;
47import 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 */
58public 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}
252