1/*
2 * Copyright (c) 2012, 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/* @test
25 * @summary test access checking by java.lang.invoke.MethodHandles.Lookup
26 * @compile AccessControlTest.java AccessControlTest_subpkg/Acquaintance_remote.java
27 * @run testng/othervm test.java.lang.invoke.AccessControlTest
28 */
29
30package test.java.lang.invoke;
31
32import java.lang.invoke.*;
33import java.lang.reflect.*;
34import java.lang.reflect.Modifier;
35import java.util.*;
36import org.testng.*;
37import org.testng.annotations.*;
38
39import static java.lang.invoke.MethodHandles.*;
40import static java.lang.invoke.MethodHandles.Lookup.*;
41import static java.lang.invoke.MethodType.*;
42import static org.testng.Assert.*;
43
44import test.java.lang.invoke.AccessControlTest_subpkg.Acquaintance_remote;
45
46
47/**
48 * Test many combinations of Lookup access and cross-class lookupStatic.
49 * @author jrose
50 */
51public class AccessControlTest {
52    static final Class<?> THIS_CLASS = AccessControlTest.class;
53    // How much output?
54    static int verbosity = 0;
55    static {
56        String vstr = System.getProperty(THIS_CLASS.getSimpleName()+".verbosity");
57        if (vstr == null)
58            vstr = System.getProperty(THIS_CLASS.getName()+".verbosity");
59        if (vstr != null)  verbosity = Integer.parseInt(vstr);
60    }
61
62    private class LookupCase implements Comparable<LookupCase> {
63        final Lookup   lookup;
64        final Class<?> lookupClass;
65        final int      lookupModes;
66        public LookupCase(Lookup lookup) {
67            this.lookup = lookup;
68            this.lookupClass = lookup.lookupClass();
69            this.lookupModes = lookup.lookupModes();
70            assert(lookupString().equals(lookup.toString()));
71            numberOf(lookupClass().getClassLoader()); // assign CL#
72        }
73        public LookupCase(Class<?> lookupClass, int lookupModes) {
74            this.lookup = null;
75            this.lookupClass = lookupClass;
76            this.lookupModes = lookupModes;
77            numberOf(lookupClass().getClassLoader()); // assign CL#
78        }
79
80        public final Class<?> lookupClass() { return lookupClass; }
81        public final int      lookupModes() { return lookupModes; }
82
83        public Lookup lookup() { lookup.getClass(); return lookup; }
84
85        @Override
86        public int compareTo(LookupCase that) {
87            Class<?> c1 = this.lookupClass();
88            Class<?> c2 = that.lookupClass();
89            if (c1 != c2) {
90                int cmp = c1.getName().compareTo(c2.getName());
91                if (cmp != 0)  return cmp;
92                cmp = numberOf(c1.getClassLoader()) - numberOf(c2.getClassLoader());
93                assert(cmp != 0);
94                return cmp;
95            }
96            return -(this.lookupModes() - that.lookupModes());
97        }
98
99        @Override
100        public boolean equals(Object that) {
101            return (that instanceof LookupCase && equals((LookupCase)that));
102        }
103        public boolean equals(LookupCase that) {
104            return (this.lookupClass() == that.lookupClass() &&
105                    this.lookupModes() == that.lookupModes());
106        }
107
108        @Override
109        public int hashCode() {
110            return lookupClass().hashCode() + (lookupModes() * 31);
111        }
112
113        /** Simulate all assertions in the spec. for Lookup.toString. */
114        private String lookupString() {
115            String name = lookupClass.getName();
116            String suffix = "";
117            if (lookupModes == 0)
118                suffix = "/noaccess";
119            else if (lookupModes == PUBLIC)
120                suffix = "/public";
121             else if (lookupModes == (PUBLIC|UNCONDITIONAL))
122                suffix = "/publicLookup";
123            else if (lookupModes == (PUBLIC|MODULE))
124                suffix = "/module";
125            else if (lookupModes == (PUBLIC|MODULE|PACKAGE))
126                suffix = "/package";
127            else if (lookupModes == (PUBLIC|MODULE|PACKAGE|PRIVATE))
128                suffix = "/private";
129            else if (lookupModes == (PUBLIC|MODULE|PACKAGE|PRIVATE|PROTECTED))
130                suffix = "";
131            else
132                suffix = "/#"+Integer.toHexString(lookupModes);
133            return name+suffix;
134        }
135
136        /** Simulate all assertions from the spec. for Lookup.in:
137         * <hr>
138         * Creates a lookup on the specified new lookup class.
139         * [A1] The resulting object will report the specified
140         * class as its own {@link #lookupClass lookupClass}.
141         * <p>
142         * [A2] However, the resulting {@code Lookup} object is guaranteed
143         * to have no more access capabilities than the original.
144         * In particular, access capabilities can be lost as follows:<ul>
145         * <li> [A3] If the old lookup class is in a named module, and the new
146         * lookup class is in a different module {@code M}, then no members, not
147         * even public members in {@code M}'s exported packages, will be accessible.
148         * The exception to this is when this lookup is publicLookup, in which case
149         * public access is not lost.
150         * <li> [A4] If the old lookup class is in an unnamed module, and the new
151         * lookup class is a different module then module access is lost.
152         * <li> [A5] If the new lookup class differs from the old one then UNCONDITIONAL
153         * is lost. If the new lookup class is not within the same package member as the
154         * old one, protected members will not be accessible by virtue of inheritance.
155         * (Protected members may continue to be accessible because of package sharing.)
156         * <li> [A6] If the new lookup class is in a different package than the old one,
157         * protected and default (package) members will not be accessible.
158         * <li> [A7] If the new lookup class is not within the same package member
159         * as the old one, private members will not be accessible.
160         * <li> [A8] If the new lookup class is not accessible to the old lookup class,
161         * then no members, not even public members, will be accessible.
162         * <li> [A9] (In all other cases, public members will continue to be accessible.)
163         * </ul>
164         * Other than the above cases, the new lookup will have the same
165         * access capabilities as the original. [A10]
166         * <hr>
167         */
168        public LookupCase in(Class<?> c2) {
169            Class<?> c1 = lookupClass();
170            int m1 = lookupModes();
171            int changed = 0;
172            // for the purposes of access control then treat classes in different unnamed
173            // modules as being in the same module.
174            boolean sameModule = (c1.getModule() == c2.getModule()) ||
175                                 (!c1.getModule().isNamed() && !c2.getModule().isNamed());
176            boolean samePackage = (c1.getClassLoader() == c2.getClassLoader() &&
177                                   c1.getPackageName().equals(c2.getPackageName()));
178            boolean sameTopLevel = (topLevelClass(c1) == topLevelClass(c2));
179            boolean sameClass = (c1 == c2);
180            assert(samePackage  || !sameTopLevel);
181            assert(sameTopLevel || !sameClass);
182            boolean accessible = sameClass;
183            if ((m1 & PACKAGE) != 0)  accessible |= samePackage;
184            if ((m1 & PUBLIC ) != 0)  accessible |= (c2.getModifiers() & PUBLIC) != 0;
185            if (!sameModule) {
186                if (c1.getModule().isNamed() && (m1 & UNCONDITIONAL) == 0) {
187                    accessible = false;  // [A3]
188                } else {
189                    changed |= (MODULE|PACKAGE|PRIVATE|PROTECTED);    // [A3] [A4]
190                }
191            }
192            if (!accessible) {
193                // Different package and no access to c2; lose all access.
194                changed |= (PUBLIC|MODULE|PACKAGE|PRIVATE|PROTECTED);  // [A8]
195            }
196            if (!samePackage) {
197                // Different package; loose PACKAGE and lower access.
198                changed |= (PACKAGE|PRIVATE|PROTECTED);  // [A6]
199            }
200            if (!sameTopLevel) {
201                // Different top-level class.  Lose PRIVATE and PROTECTED access.
202                changed |= (PRIVATE|PROTECTED);  // [A5] [A7]
203            }
204            if (!sameClass) {
205                changed |= (UNCONDITIONAL);     // [A5]
206            } else {
207                assert(changed == 0);       // [A10] (no deprivation if same class)
208            }
209            if (accessible)  assert((changed & PUBLIC) == 0);  // [A9]
210            int m2 = m1 & ~changed;
211            LookupCase l2 = new LookupCase(c2, m2);
212            assert(l2.lookupClass() == c2); // [A1]
213            assert((m1 | m2) == m1);        // [A2] (no elevation of access)
214            return l2;
215        }
216
217        @Override
218        public String toString() {
219            String s = lookupClass().getSimpleName();
220            String lstr = lookupString();
221            int sl = lstr.indexOf('/');
222            if (sl >= 0)  s += lstr.substring(sl);
223            ClassLoader cld = lookupClass().getClassLoader();
224            if (cld != THIS_LOADER)  s += "/loader#"+numberOf(cld);
225            return s;
226        }
227
228        /** Predict the success or failure of accessing this method. */
229        public boolean willAccess(Method m) {
230            Class<?> c1 = lookupClass();
231            Class<?> c2 = m.getDeclaringClass();
232
233            // publicLookup has access to all public types/members of types in unnamed modules
234            if ((lookupModes & UNCONDITIONAL) != 0
235                && (lookupModes & PUBLIC) != 0
236                && !c2.getModule().isNamed()
237                && Modifier.isPublic(c2.getModifiers())
238                && Modifier.isPublic(m.getModifiers()))
239                return true;
240
241            LookupCase lc = this.in(c2);
242            int m1 = lc.lookupModes();
243            int m2 = fixMods(m.getModifiers());
244            // privacy is strictly enforced on lookups
245            if (c1 != c2)  m1 &= ~PRIVATE;
246            // protected access is sometimes allowed
247            if ((m2 & PROTECTED) != 0) {
248                int prev = m2;
249                m2 |= PACKAGE;  // it acts like a package method also
250                if ((lookupModes() & PROTECTED) != 0 &&
251                    c2.isAssignableFrom(c1))
252                    m2 |= PUBLIC;  // from a subclass, it acts like a public method also
253            }
254            if (verbosity >= 2)
255                System.out.println(this+" willAccess "+lc+" m1="+m1+" m2="+m2+" => "+((m2 & m1) != 0));
256            return (m2 & m1) != 0;
257        }
258
259        /** Predict the success or failure of accessing this class. */
260        public boolean willAccessClass(Class<?> c2, boolean load) {
261            Class<?> c1 = lookupClass();
262            if (load && c2.getClassLoader() != null) {
263                if (c1.getClassLoader() == null) {
264                    // not visible
265                    return false;
266                }
267            }
268
269            // publicLookup has access to all public types/members of types in unnamed modules
270            if ((lookupModes & UNCONDITIONAL) != 0
271                && (lookupModes & PUBLIC) != 0
272                && (!c2.getModule().isNamed())
273                && Modifier.isPublic(c2.getModifiers()))
274                return true;
275
276            LookupCase lc = this.in(c2);
277            int m1 = lc.lookupModes();
278            boolean r = false;
279            if (m1 == 0) {
280                r = false;
281            } else {
282                int m2 = fixMods(c2.getModifiers());
283                if ((m2 & PUBLIC) != 0) {
284                    r = true;
285                } else if ((m1 & PACKAGE) != 0 && c1.getPackage() == c2.getPackage()) {
286                    r = true;
287                }
288            }
289            if (verbosity >= 2) {
290                System.out.println(this+" willAccessClass "+lc+" c1="+c1+" c2="+c2+" => "+r);
291            }
292            return r;
293        }
294    }
295
296    private static Class<?> topLevelClass(Class<?> cls) {
297        Class<?> c = cls;
298        for (Class<?> ec; (ec = c.getEnclosingClass()) != null; )
299            c = ec;
300        assert(c.getEnclosingClass() == null);
301        assert(c == cls || cls.getEnclosingClass() != null);
302        return c;
303    }
304
305    private final TreeSet<LookupCase> CASES = new TreeSet<>();
306    private final TreeMap<LookupCase,TreeSet<LookupCase>> CASE_EDGES = new TreeMap<>();
307    private final ArrayList<ClassLoader> LOADERS = new ArrayList<>();
308    private final ClassLoader THIS_LOADER = this.getClass().getClassLoader();
309    { if (THIS_LOADER != null)  LOADERS.add(THIS_LOADER); }  // #1
310
311    private LookupCase lookupCase(String name) {
312        for (LookupCase lc : CASES) {
313            if (lc.toString().equals(name))
314                return lc;
315        }
316        throw new AssertionError(name);
317    }
318
319    private int numberOf(ClassLoader cl) {
320        if (cl == null)  return 0;
321        int i = LOADERS.indexOf(cl);
322        if (i < 0) {
323            i = LOADERS.size();
324            LOADERS.add(cl);
325        }
326        return i+1;
327    }
328
329    private void addLookupEdge(LookupCase l1, Class<?> c2, LookupCase l2) {
330        TreeSet<LookupCase> edges = CASE_EDGES.get(l2);
331        if (edges == null)  CASE_EDGES.put(l2, edges = new TreeSet<>());
332        if (edges.add(l1)) {
333            Class<?> c1 = l1.lookupClass();
334            assert(l2.lookupClass() == c2); // [A1]
335            int m1 = l1.lookupModes();
336            int m2 = l2.lookupModes();
337            assert((m1 | m2) == m1);        // [A2] (no elevation of access)
338            LookupCase expect = l1.in(c2);
339            if (!expect.equals(l2))
340                System.out.println("*** expect "+l1+" => "+expect+" but got "+l2);
341            assertEquals(l2, expect);
342        }
343    }
344
345    private void makeCases(Lookup[] originalLookups) {
346        // make initial set of lookup test cases
347        CASES.clear(); LOADERS.clear(); CASE_EDGES.clear();
348        ArrayList<Class<?>> classes = new ArrayList<>();
349        for (Lookup l : originalLookups) {
350            CASES.add(new LookupCase(l));
351            classes.remove(l.lookupClass());  // no dups please
352            classes.add(l.lookupClass());
353        }
354        System.out.println("loaders = "+LOADERS);
355        int rounds = 0;
356        for (int lastCount = -1; lastCount != CASES.size(); ) {
357            lastCount = CASES.size();  // if CASES grow in the loop we go round again
358            for (LookupCase lc1 : CASES.toArray(new LookupCase[0])) {
359                for (Class<?> c2 : classes) {
360                    LookupCase lc2 = new LookupCase(lc1.lookup().in(c2));
361                    addLookupEdge(lc1, c2, lc2);
362                    CASES.add(lc2);
363                }
364            }
365            rounds++;
366        }
367        System.out.println("filled in "+CASES.size()+" cases from "+originalLookups.length+" original cases in "+rounds+" rounds");
368        if (false) {
369            System.out.println("CASES: {");
370            for (LookupCase lc : CASES) {
371                System.out.println(lc);
372                Set<LookupCase> edges = CASE_EDGES.get(lc);
373                if (edges != null)
374                    for (LookupCase prev : edges) {
375                        System.out.println("\t"+prev);
376                    }
377            }
378            System.out.println("}");
379        }
380    }
381
382    @Test public void test() {
383        makeCases(lookups());
384        if (verbosity > 0) {
385            verbosity += 9;
386            Method pro_in_self = targetMethod(THIS_CLASS, PROTECTED, methodType(void.class));
387            testOneAccess(lookupCase("AccessControlTest/public"),  pro_in_self, "find");
388            testOneAccess(lookupCase("Remote_subclass/public"),    pro_in_self, "find");
389            testOneAccess(lookupCase("Remote_subclass"),           pro_in_self, "find");
390            verbosity -= 9;
391        }
392        Set<Class<?>> targetClassesDone = new HashSet<>();
393        for (LookupCase targetCase : CASES) {
394            Class<?> targetClass = targetCase.lookupClass();
395            if (!targetClassesDone.add(targetClass))  continue;  // already saw this one
396            String targetPlace = placeName(targetClass);
397            if (targetPlace == null)  continue;  // Object, String, not a target
398            for (int targetAccess : ACCESS_CASES) {
399                MethodType methodType = methodType(void.class);
400                Method method = targetMethod(targetClass, targetAccess, methodType);
401                // Try to access target method from various contexts.
402                for (LookupCase sourceCase : CASES) {
403                    testOneAccess(sourceCase, method, "findClass");
404                    testOneAccess(sourceCase, method, "accessClass");
405                    testOneAccess(sourceCase, method, "find");
406                    testOneAccess(sourceCase, method, "unreflect");
407                }
408            }
409        }
410        System.out.println("tested "+testCount+" access scenarios; "+testCountFails+" accesses were denied");
411    }
412
413    private int testCount, testCountFails;
414
415    private void testOneAccess(LookupCase sourceCase, Method method, String kind) {
416        Class<?> targetClass = method.getDeclaringClass();
417        String methodName = method.getName();
418        MethodType methodType = methodType(method.getReturnType(), method.getParameterTypes());
419        boolean isFindOrAccessClass = "findClass".equals(kind) || "accessClass".equals(kind);
420        boolean willAccess = isFindOrAccessClass ?
421                sourceCase.willAccessClass(targetClass, "findClass".equals(kind)) : sourceCase.willAccess(method);
422        boolean didAccess = false;
423        ReflectiveOperationException accessError = null;
424        try {
425            switch (kind) {
426            case "accessClass":
427                sourceCase.lookup().accessClass(targetClass);
428                break;
429            case "findClass":
430                sourceCase.lookup().findClass(targetClass.getName());
431                break;
432            case "find":
433                if ((method.getModifiers() & Modifier.STATIC) != 0)
434                    sourceCase.lookup().findStatic(targetClass, methodName, methodType);
435                else
436                    sourceCase.lookup().findVirtual(targetClass, methodName, methodType);
437                break;
438            case "unreflect":
439                sourceCase.lookup().unreflect(method);
440                break;
441            default:
442                throw new AssertionError(kind);
443            }
444            didAccess = true;
445        } catch (ReflectiveOperationException ex) {
446            accessError = ex;
447        }
448        if (willAccess != didAccess) {
449            System.out.println(sourceCase+" => "+targetClass.getSimpleName()+(isFindOrAccessClass?"":"."+methodName+methodType));
450            System.out.println("fail "+(isFindOrAccessClass?kind:"on "+method)+" ex="+accessError);
451            assertEquals(willAccess, didAccess);
452        }
453        testCount++;
454        if (!didAccess)  testCountFails++;
455    }
456
457    static Method targetMethod(Class<?> targetClass, int targetAccess, MethodType methodType) {
458        assert targetAccess != MODULE;
459        String methodName = accessName(targetAccess)+placeName(targetClass);
460        if (verbosity >= 2)
461            System.out.println(targetClass.getSimpleName()+"."+methodName+methodType);
462        try {
463            Method method = targetClass.getDeclaredMethod(methodName, methodType.parameterArray());
464            assertEquals(method.getReturnType(), methodType.returnType());
465            int haveMods = method.getModifiers();
466            assert(Modifier.isStatic(haveMods));
467            assert(targetAccess == fixMods(haveMods));
468            return method;
469        } catch (NoSuchMethodException ex) {
470            throw new AssertionError(methodName, ex);
471        }
472    }
473
474    static String placeName(Class<?> cls) {
475        // return "self", "sibling", "nestmate", etc.
476        if (cls == AccessControlTest.class)  return "self";
477        String cln = cls.getSimpleName();
478        int under = cln.lastIndexOf('_');
479        if (under < 0)  return null;
480        return cln.substring(under+1);
481    }
482    static String accessName(int acc) {
483        switch (acc) {
484        case PUBLIC:     return "pub_in_";
485        case PROTECTED:  return "pro_in_";
486        case PACKAGE:    return "pkg_in_";
487        case PRIVATE:    return "pri_in_";
488        }
489        assert(false);
490        return "?";
491    }
492    // MODULE not a test case at this time
493    private static final int[] ACCESS_CASES = {
494        PUBLIC, PACKAGE, PRIVATE, PROTECTED
495    };
496    /** Return one of the ACCESS_CASES. */
497    static int fixMods(int mods) {
498        mods &= (PUBLIC|PRIVATE|PROTECTED);
499        switch (mods) {
500        case PUBLIC: case PRIVATE: case PROTECTED: return mods;
501        case 0:  return PACKAGE;
502        }
503        throw new AssertionError(mods);
504    }
505
506    static Lookup[] lookups() {
507        ArrayList<Lookup> tem = new ArrayList<>();
508        Collections.addAll(tem,
509                           AccessControlTest.lookup_in_self(),
510                           Inner_nestmate.lookup_in_nestmate(),
511                           AccessControlTest_sibling.lookup_in_sibling());
512        if (true) {
513            Collections.addAll(tem,Acquaintance_remote.lookups());
514        } else {
515            try {
516                Class<?> remc = Class.forName("test.java.lang.invoke.AccessControlTest_subpkg.Acquaintance_remote");
517                Lookup[] remls = (Lookup[]) remc.getMethod("lookups").invoke(null);
518                Collections.addAll(tem, remls);
519            } catch (ReflectiveOperationException ex) {
520                throw new LinkageError("reflection failed", ex);
521            }
522        }
523        tem.add(publicLookup());
524        tem.add(publicLookup().in(String.class));
525        tem.add(publicLookup().in(List.class));
526        return tem.toArray(new Lookup[0]);
527    }
528
529    static Lookup lookup_in_self() {
530        return MethodHandles.lookup();
531    }
532    public static      void pub_in_self() { }
533    protected static   void pro_in_self() { }
534    static /*package*/ void pkg_in_self() { }
535    private static     void pri_in_self() { }
536
537    static class Inner_nestmate {
538        static Lookup lookup_in_nestmate() {
539            return MethodHandles.lookup();
540        }
541        public static      void pub_in_nestmate() { }
542        protected static   void pro_in_nestmate() { }
543        static /*package*/ void pkg_in_nestmate() { }
544        private static     void pri_in_nestmate() { }
545    }
546}
547class AccessControlTest_sibling {
548    static Lookup lookup_in_sibling() {
549        return MethodHandles.lookup();
550    }
551    public static      void pub_in_sibling() { }
552    protected static   void pro_in_sibling() { }
553    static /*package*/ void pkg_in_sibling() { }
554    private static     void pri_in_sibling() { }
555}
556
557// This guy tests access from outside the package:
558/*
559package test.java.lang.invoke.AccessControlTest_subpkg;
560public class Acquaintance_remote {
561    public static Lookup[] lookups() { ...
562    }
563    ...
564}
565*/
566