1 /*
  2  * Copyright (c) 2018, 2026, 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  * @bug 8196830 8235351 8257874 8327639
 26  * @modules java.base/jdk.internal.reflect
 27  * @run junit/othervm CallerSensitiveAccess
 28  * @summary Check Lookup findVirtual, findStatic and unreflect behavior with
 29  *          caller sensitive methods with focus on AccessibleObject.setAccessible
 30  */
 31 
 32 import java.io.IOException;
 33 import java.io.UncheckedIOException;
 34 import java.lang.invoke.MethodHandle;
 35 import java.lang.invoke.MethodHandles;
 36 import java.lang.invoke.MethodHandles.Lookup;
 37 import java.lang.invoke.MethodType;
 38 import java.lang.module.ModuleReader;
 39 import java.lang.module.ModuleReference;
 40 import java.lang.reflect.AccessibleObject;
 41 import java.lang.reflect.Field;
 42 import java.lang.reflect.InaccessibleObjectException;
 43 import java.lang.reflect.Method;
 44 import java.lang.reflect.Modifier;
 45 import java.util.Arrays;
 46 import java.util.List;
 47 import java.util.StringJoiner;
 48 import java.util.stream.Collectors;
 49 import java.util.stream.Stream;
 50 
 51 import jdk.internal.reflect.CallerSensitive;
 52 
 53 import static org.junit.jupiter.api.Assertions.*;
 54 import org.junit.jupiter.api.BeforeAll;
 55 import org.junit.jupiter.api.Test;
 56 import org.junit.jupiter.params.ParameterizedTest;
 57 import org.junit.jupiter.params.provider.MethodSource;
 58 
 59 public class CallerSensitiveAccess {
 60     // Cache the list of Caller Sensitive Methods
 61     private static List<Method> CALLER_SENSITIVE_METHODS;
 62 
 63     @BeforeAll
 64     public static void setupCallerSensitiveMethods() {
 65         CALLER_SENSITIVE_METHODS = callerSensitiveMethods(Object.class.getModule());
 66     }
 67 
 68     /**
 69      * Caller sensitive methods in APIs exported by java.base.
 70      */
 71     static Object[][] callerSensitiveMethods() {
 72         return CALLER_SENSITIVE_METHODS.stream()
 73                 .map(m -> new Object[]{m, shortDescription(m)})
 74                 .toArray(Object[][]::new);
 75     }
 76 
 77     /**
 78      * Using publicLookup, attempt to use findVirtual or findStatic to obtain a
 79      * method handle to a caller sensitive method.
 80      */
 81     @ParameterizedTest
 82     @MethodSource("callerSensitiveMethods")
 83     public void testPublicLookupFind(Method method, String desc) throws Exception {
 84         Lookup lookup = MethodHandles.publicLookup();
 85         Class<?> refc = method.getDeclaringClass();
 86         String name = method.getName();
 87         MethodType mt = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
 88         assertThrows(IllegalAccessException.class, () -> {
 89             if (Modifier.isStatic(method.getModifiers())) {
 90                 lookup.findStatic(refc, name, mt);
 91             } else {
 92                 lookup.findVirtual(refc, name, mt);
 93             }
 94         });
 95     }
 96 
 97     /**
 98      * Using publicLookup, attempt to use unreflect to obtain a method handle to a
 99      * caller sensitive method.
100      */
101     @ParameterizedTest
102     @MethodSource("callerSensitiveMethods")
103     public void testPublicLookupUnreflect(Method method, String desc) throws Exception {
104         assertThrows(IllegalAccessException.class, () -> MethodHandles.publicLookup().unreflect(method));
105     }
106 
107     /**
108      * public accessible caller sensitive methods in APIs exported by java.base.
109      */
110     static Object[][] accessibleCallerSensitiveMethods() {
111         return CALLER_SENSITIVE_METHODS.stream()
112                 .filter(m -> Modifier.isPublic(m.getModifiers()))
113                 .map(m -> { m.setAccessible(true); return m; })
114                 .map(m -> new Object[] { m, shortDescription(m) })
115                 .toArray(Object[][]::new);
116     }
117 
118     /**
119      * Using publicLookup, attempt to use unreflect to obtain a method handle to a
120      * caller sensitive method.
121      */
122     @ParameterizedTest
123     @MethodSource("accessibleCallerSensitiveMethods")
124     public void testLookupUnreflect(Method method, String desc) throws Exception {
125         assertThrows(IllegalAccessException.class, () -> MethodHandles.publicLookup().unreflect(method));
126     }
127 
128     /**
129      * Using a Lookup with no original access that can't lookup caller-sensitive
130      * method
131      */
132     @ParameterizedTest
133     @MethodSource("callerSensitiveMethods")
134     public void testLookupNoOriginalAccessFind(Method method, String desc) throws Exception {
135         Lookup lookup = MethodHandles.lookup().dropLookupMode(Lookup.ORIGINAL);
136         assertTrue(lookup.hasFullPrivilegeAccess());
137         Class<?> refc = method.getDeclaringClass();
138         String name = method.getName();
139         MethodType mt = MethodType.methodType(method.getReturnType(), method.getParameterTypes());
140         assertThrows(IllegalAccessException.class, () -> {
141             if (Modifier.isStatic(method.getModifiers())) {
142                 lookup.findStatic(refc, name, mt);
143             } else {
144                 lookup.findVirtual(refc, name, mt);
145             }
146         });
147     }
148 
149     /**
150      * Using a Lookup with no original access that can't unreflect caller-sensitive
151      * method
152      */
153     @ParameterizedTest
154     @MethodSource("callerSensitiveMethods")
155     public void testLookupNoOriginalAccessUnreflect(Method method, String desc) throws Exception {
156         Lookup lookup = MethodHandles.lookup().dropLookupMode(Lookup.ORIGINAL);
157         assertTrue(lookup.hasFullPrivilegeAccess());
158         assertThrows(IllegalAccessException.class, () -> lookup.unreflect(method));
159     }
160 
161     // -- Test method handles to setAccessible --
162 
163     private int aField;
164 
165     Field accessibleField() {
166         var clazz = getClass();
167         return assertDoesNotThrow(() -> clazz.getDeclaredField("aField"));
168     }
169 
170     Field inaccessibleField() {
171         return assertDoesNotThrow(() -> String.class.getDeclaredField("hash"));
172     }
173 
174     void findAndInvokeSetAccessible(Class<? extends AccessibleObject> refc, Field f) throws Throwable {
175         MethodType mt = MethodType.methodType(void.class, boolean.class);
176         MethodHandle mh = MethodHandles.lookup().findVirtual(refc, "setAccessible", mt);
177         mh.invoke(f, true);  // may throw InaccessibleObjectException
178         assertTrue(f.isAccessible());
179     }
180 
181     void unreflectAndInvokeSetAccessible(Method m, Field f) throws Throwable {
182         assertTrue(m.getName().equals("setAccessible"));
183         assertFalse(Modifier.isStatic(m.getModifiers()));
184         MethodHandle mh = MethodHandles.lookup().unreflect(m);
185         mh.invoke(f, true);   // may throw InaccessibleObjectException
186         assertTrue(f.isAccessible());
187     }
188 
189     /**
190      * Create a method handle to setAccessible and use it to suppress access to an
191      * accessible member.
192      */
193     @Test
194     public void testSetAccessible1() throws Throwable {
195         findAndInvokeSetAccessible(AccessibleObject.class, accessibleField());
196     }
197     @Test
198     public void testSetAccessible2() throws Throwable {
199         findAndInvokeSetAccessible(Field.class, accessibleField());
200     }
201     @Test
202     public void testSetAccessible3() throws Throwable {
203         Method m = AccessibleObject.class.getMethod("setAccessible", boolean.class);
204         unreflectAndInvokeSetAccessible(m, accessibleField());
205     }
206     @Test
207     public void testSetAccessible4() throws Throwable {
208         Method m = Field.class.getMethod("setAccessible", boolean.class);
209         unreflectAndInvokeSetAccessible(m, accessibleField());
210     }
211 
212     /**
213      * Create a method handle to setAccessible and attempt to use it to suppress
214      * access to an inaccessible member.
215      */
216     @Test
217     public void testSetAccessible5() throws Throwable {
218         assertThrows(InaccessibleObjectException.class, () -> findAndInvokeSetAccessible(AccessibleObject.class, inaccessibleField()));
219     }
220     @Test
221     public void testSetAccessible6() throws Throwable {
222         assertThrows(InaccessibleObjectException.class, () -> findAndInvokeSetAccessible(Field.class, inaccessibleField()));
223     }
224     @Test
225     public void testSetAccessible7() throws Throwable {
226         Method m = AccessibleObject.class.getMethod("setAccessible", boolean.class);
227         assertThrows(InaccessibleObjectException.class, () -> unreflectAndInvokeSetAccessible(m, inaccessibleField()));
228     }
229     @Test
230     public void testSetAccessible8() throws Throwable {
231         Method m = Field.class.getMethod("setAccessible", boolean.class);
232         assertThrows(InaccessibleObjectException.class, () -> unreflectAndInvokeSetAccessible(m, inaccessibleField()));
233     }
234 
235 
236     // -- Test sub-classes of AccessibleObject --
237 
238     /**
239      * Custom AccessibleObject objects. One class overrides setAccessible, the other
240      * does not override this method.
241      */
242     static Object[][] customAccessibleObjectClasses() {
243         return new Object[][] { { new S1() }, { new S2() } };
244     }
245     public static class S1 extends AccessibleObject { }
246     public static class S2 extends AccessibleObject {
247         @Override
248         public void setAccessible(boolean flag) {
249             super.setAccessible(flag);
250         }
251     }
252 
253     void findAndInvokeSetAccessible(Lookup lookup, AccessibleObject obj) throws Throwable {
254         MethodType mt = MethodType.methodType(void.class, boolean.class);
255         MethodHandle mh = lookup.findVirtual(obj.getClass(), "setAccessible", mt);
256         mh.invoke(obj, true);
257         assertTrue(obj.isAccessible());
258     }
259 
260     void unreflectAndInvokeSetAccessible(Lookup lookup, AccessibleObject obj) throws Throwable {
261         Method m = obj.getClass().getMethod("setAccessible", boolean.class);
262         MethodHandle mh = lookup.unreflect(m);
263         mh.invoke(obj, true);
264         assertTrue(obj.isAccessible());
265     }
266 
267     /**
268      * Using publicLookup, create a method handle to setAccessible and invoke it
269      * on a custom AccessibleObject object.
270      */
271     @Test
272     public void testPublicLookupSubclass1() throws Throwable {
273         // S1 does not override setAccessible
274         assertThrows(IllegalAccessException.class, () -> findAndInvokeSetAccessible(MethodHandles.publicLookup(), new S1()));
275     }
276     @Test
277     public void testPublicLookupSubclass2() throws Throwable {
278         // S2 overrides setAccessible
279         findAndInvokeSetAccessible(MethodHandles.publicLookup(), new S2());
280     }
281     @Test
282     public void testPublicLookupSubclass3() throws Throwable {
283         // S1 does not override setAccessible
284         assertThrows(IllegalAccessException.class, () -> unreflectAndInvokeSetAccessible(MethodHandles.publicLookup(), new S1()));
285     }
286     @Test
287     public void testPublicLookupSubclass4() throws Throwable {
288         // S2 overrides setAccessible
289         unreflectAndInvokeSetAccessible(MethodHandles.publicLookup(), new S2());
290     }
291 
292     /**
293      * Using a full power lookup, create a method handle to setAccessible and
294      * invoke it on a custom AccessibleObject object.
295      */
296     @ParameterizedTest
297     @MethodSource("customAccessibleObjectClasses")
298     public void testLookupSubclass1(AccessibleObject obj) throws Throwable {
299         findAndInvokeSetAccessible(MethodHandles.lookup(), obj);
300     }
301     @ParameterizedTest
302     @MethodSource("customAccessibleObjectClasses")
303     public void testLookupSubclass2(AccessibleObject obj) throws Throwable {
304         unreflectAndInvokeSetAccessible(MethodHandles.lookup(), obj);
305     }
306 
307     /**
308      * Using a full power lookup, create a method handle to setAccessible on a
309      * sub-class of AccessibleObject and then attempt to invoke it on a Field object.
310      */
311     @ParameterizedTest
312     @MethodSource("customAccessibleObjectClasses")
313     public void testLookupSubclass3(AccessibleObject obj) throws Throwable {
314         MethodType mt = MethodType.methodType(void.class, boolean.class);
315         Lookup lookup = MethodHandles.lookup();
316         MethodHandle mh = lookup.findVirtual(obj.getClass(), "setAccessible", mt);
317         assertThrows(ClassCastException.class, () -> mh.invoke(accessibleField(), true));
318     }
319 
320     /**
321      * Using a full power lookup, use unreflect to create a method handle to
322      * setAccessible on a sub-class of AccessibleObject, then attempt to invoke
323      * it on a Field object.
324      */
325     @Test
326     public void testLookupSubclass4() throws Throwable {
327         // S1 does not override setAccessible
328         Method m = S1.class.getMethod("setAccessible", boolean.class);
329         assertTrue(m.getDeclaringClass() == AccessibleObject.class);
330         MethodHandle mh = MethodHandles.lookup().unreflect(m);
331         Field f = accessibleField();
332         mh.invoke(f, true);
333         assertTrue(f.isAccessible());
334     }
335     @Test
336     public void testLookupSubclass5() throws Throwable {
337         // S1 does not override setAccessible
338         Method m = S1.class.getMethod("setAccessible", boolean.class);
339         assertTrue(m.getDeclaringClass() == AccessibleObject.class);
340         MethodHandle mh = MethodHandles.lookup().unreflect(m);
341         assertThrows(InaccessibleObjectException.class, () -> mh.invoke(inaccessibleField(), true));
342     }
343     @Test
344     public void testLookupSubclass6() throws Throwable {
345         // S2 overrides setAccessible
346         Method m = S2.class.getMethod("setAccessible", boolean.class);
347         assertTrue(m.getDeclaringClass() == S2.class);
348         MethodHandle mh = MethodHandles.lookup().unreflect(m);
349         assertThrows(ClassCastException.class, () -> mh.invoke(accessibleField(), true));
350     }
351     @Test
352     public void testLookupSubclass7() throws Throwable {
353         // S2 overrides setAccessible
354         Method m = S2.class.getMethod("setAccessible", boolean.class);
355         assertTrue(m.getDeclaringClass() == S2.class);
356         MethodHandle mh = MethodHandles.lookup().unreflect(m);
357         assertThrows(ClassCastException.class, () -> mh.invoke(inaccessibleField(), true));
358     }
359 
360     /**
361      * Field::getInt and Field::setInt are caller-sensitive methods.
362      * Test access to private field.
363      */
364     private static int privateField = 0;
365     @Test
366     public void testPrivateField() throws Throwable {
367         Field f = CallerSensitiveAccess.class.getDeclaredField("privateField");
368         // Field::setInt
369         MethodType mtype = MethodType.methodType(void.class, Object.class, int.class);
370         MethodHandle mh = MethodHandles.lookup().findVirtual(Field.class, "setInt", mtype);
371         mh.invokeExact(f, (Object)null, 5);
372         // Field::getInt
373         mh = MethodHandles.lookup().findVirtual(Field.class, "getInt", MethodType.methodType(int.class, Object.class));
374         int value = (int)mh.invokeExact(f, (Object)null);
375         assertEquals(5, value);
376     }
377 
378     private static class Inner {
379         private static boolean privateField = false;
380     }
381 
382     @Test
383     public void testInnerPrivateField() throws Throwable {
384         Field f = Inner.class.getDeclaredField("privateField");
385         // Field::setInt
386         MethodType mtype = MethodType.methodType(void.class, Object.class, boolean.class);
387         MethodHandle mh = MethodHandles.lookup().findVirtual(Field.class, "setBoolean", mtype);
388         mh.invokeExact(f, (Object)null, true);
389         // Field::getInt
390         mh = MethodHandles.lookup().findVirtual(Field.class, "getBoolean", MethodType.methodType(boolean.class, Object.class));
391         boolean value = (boolean)mh.invokeExact(f, (Object)null);
392         assertTrue(value);
393     }
394 
395     // -- supporting methods --
396 
397     /**
398      * Returns a List of all caller sensitive methods on public classes in packages
399      * exported by a named module.
400      * Returns a List instead of a stream so the ModuleReader can be closed before returning.
401      */
402     static List<Method> callerSensitiveMethods(Module module) {
403         assertTrue(module.isNamed());
404         ModuleReference mref = module.getLayer().configuration()
405                 .findModule(module.getName())
406                 .orElseThrow(() -> new RuntimeException())
407                 .reference();
408         // find all ".class" resources in the module
409         // transform the resource name to a class name
410         // load every class in the exported packages
411         // return the caller sensitive methods of the public classes
412         try (ModuleReader reader = mref.open()) {
413             return reader.list()
414                     .filter(rn -> rn.endsWith(".class"))
415                     .map(rn -> rn.substring(0, rn.length() - 6)
416                                  .replace('/', '.'))
417                     .filter(cn -> module.isExported(packageName(cn)))
418                     .map(cn -> Class.forName(module, cn))
419                     .filter(refc -> refc != null
420                                     && Modifier.isPublic(refc.getModifiers()))
421                     .map(refc -> callerSensitiveMethods(refc))
422                     .flatMap(List::stream).toList();
423         } catch (IOException ioe) {
424             throw new UncheckedIOException(ioe);
425         }
426     }
427 
428     static String packageName(String cn) {
429         int last = cn.lastIndexOf('.');
430         if (last > 0) {
431             return cn.substring(0, last);
432         } else {
433             return "";
434         }
435     }
436 
437     /**
438      * Returns a list of the caller sensitive methods directly declared by the given
439      * class.
440      */
441     static List<Method> callerSensitiveMethods(Class<?> refc) {
442         return Arrays.stream(refc.getDeclaredMethods())
443                 .filter(m -> m.isAnnotationPresent(CallerSensitive.class))
444                 .collect(Collectors.toList());
445     }
446 
447     /**
448      * Returns a short description of the given method for tracing purposes.
449      */
450     static String shortDescription(Method m) {
451         var sb = new StringBuilder();
452         sb.append(m.getDeclaringClass().getName());
453         sb.append('.');
454         sb.append(m.getName());
455         sb.append('(');
456         StringJoiner sj = new StringJoiner(",");
457         for (Class<?> parameterType : m.getParameterTypes()) {
458             sj.add(parameterType.getTypeName());
459         }
460         sb.append(sj);
461         sb.append(')');
462         return sb.toString();
463     }
464 }