| /* | |
| * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* @test | |
| * @bug 8196830 8235351 8257874 | |
| * @modules java.base/jdk.internal.reflect | |
| * @run testng/othervm CallerSensitiveAccess | |
| * @summary Check Lookup findVirtual, findStatic and unreflect behavior with | |
| * caller sensitive methods with focus on AccessibleObject.setAccessible | |
| */ | |
| import java.io.IOException; | |
| import java.io.UncheckedIOException; | |
| import java.lang.invoke.MethodHandle; | |
| import java.lang.invoke.MethodHandles; | |
| import java.lang.invoke.MethodHandles.Lookup; | |
| import java.lang.invoke.MethodType; | |
| import java.lang.module.ModuleReader; | |
| import java.lang.module.ModuleReference; | |
| import java.lang.reflect.AccessibleObject; | |
| import java.lang.reflect.Field; | |
| import java.lang.reflect.InaccessibleObjectException; | |
| import java.lang.reflect.Method; | |
| import java.lang.reflect.Modifier; | |
| import java.util.Arrays; | |
| import java.util.List; | |
| import java.util.StringJoiner; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import jdk.internal.reflect.CallerSensitive; | |
| import org.testng.annotations.DataProvider; | |
| import org.testng.annotations.NoInjection; | |
| import org.testng.annotations.Test; | |
| import static org.testng.Assert.*; | |
| public class CallerSensitiveAccess { | |
| /** | |
| * Caller sensitive methods in APIs exported by java.base. | |
| */ | |
| @DataProvider(name = "callerSensitiveMethods") | |
| static Object[][] callerSensitiveMethods() { | |
| try (Stream<Method> stream = callerSensitiveMethods(Object.class.getModule())) { | |
| return stream.map(m -> new Object[]{m, shortDescription(m)}) | |
| .toArray(Object[][]::new); | |
| } | |
| } | |
| /** | |
| * Using publicLookup, attempt to use findVirtual or findStatic to obtain a | |
| * method handle to a caller sensitive method. | |
| */ | |
| @Test(dataProvider = "callerSensitiveMethods", | |
| expectedExceptions = IllegalAccessException.class) | |
| public void testPublicLookupFind(@NoInjection Method method, String desc) throws Exception { | |
| Lookup lookup = MethodHandles.publicLookup(); | |
| Class<?> refc = method.getDeclaringClass(); | |
| String name = method.getName(); | |
| MethodType mt = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); | |
| if (Modifier.isStatic(method.getModifiers())) { | |
| lookup.findStatic(refc, name, mt); | |
| } else { | |
| lookup.findVirtual(refc, name, mt); | |
| } | |
| } | |
| /** | |
| * Using publicLookup, attempt to use unreflect to obtain a method handle to a | |
| * caller sensitive method. | |
| */ | |
| @Test(dataProvider = "callerSensitiveMethods", | |
| expectedExceptions = IllegalAccessException.class) | |
| public void testPublicLookupUnreflect(@NoInjection Method method, String desc) throws Exception { | |
| MethodHandles.publicLookup().unreflect(method); | |
| } | |
| /** | |
| * public accessible caller sensitive methods in APIs exported by java.base. | |
| */ | |
| @DataProvider(name = "accessibleCallerSensitiveMethods") | |
| static Object[][] accessibleCallerSensitiveMethods() { | |
| try (Stream<Method> stream = callerSensitiveMethods(Object.class.getModule())) { | |
| return stream | |
| .filter(m -> Modifier.isPublic(m.getModifiers())) | |
| .map(m -> { m.setAccessible(true); return m; }) | |
| .map(m -> new Object[] { m, shortDescription(m) }) | |
| .toArray(Object[][]::new); | |
| } | |
| } | |
| /** | |
| * Using publicLookup, attempt to use unreflect to obtain a method handle to a | |
| * caller sensitive method. | |
| */ | |
| @Test(dataProvider = "accessibleCallerSensitiveMethods", | |
| expectedExceptions = IllegalAccessException.class) | |
| public void testLookupUnreflect(@NoInjection Method method, String desc) throws Exception { | |
| MethodHandles.publicLookup().unreflect(method); | |
| } | |
| /** | |
| * Using a Lookup with no original access that can't lookup caller-sensitive | |
| * method | |
| */ | |
| @Test(dataProvider = "callerSensitiveMethods", | |
| expectedExceptions = IllegalAccessException.class) | |
| public void testLookupNoOriginalAccessFind(@NoInjection Method method, String desc) throws Exception { | |
| Lookup lookup = MethodHandles.lookup().dropLookupMode(Lookup.ORIGINAL); | |
| assertTrue(lookup.hasFullPrivilegeAccess()); | |
| Class<?> refc = method.getDeclaringClass(); | |
| String name = method.getName(); | |
| MethodType mt = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); | |
| if (Modifier.isStatic(method.getModifiers())) { | |
| lookup.findStatic(refc, name, mt); | |
| } else { | |
| lookup.findVirtual(refc, name, mt); | |
| } | |
| } | |
| /** | |
| * Using a Lookup with no original access that can't unreflect caller-sensitive | |
| * method | |
| */ | |
| @Test(dataProvider = "callerSensitiveMethods", | |
| expectedExceptions = IllegalAccessException.class) | |
| public void testLookupNoOriginalAccessUnreflect(@NoInjection Method method, String desc) throws Exception { | |
| Lookup lookup = MethodHandles.lookup().dropLookupMode(Lookup.ORIGINAL); | |
| assertTrue(lookup.hasFullPrivilegeAccess()); | |
| lookup.unreflect(method); | |
| } | |
| // -- Test method handles to setAccessible -- | |
| private int aField; | |
| Field accessibleField() { | |
| try { | |
| return getClass().getDeclaredField("aField"); | |
| } catch (NoSuchFieldException e) { | |
| fail(); | |
| return null; | |
| } | |
| } | |
| Field inaccessibleField() { | |
| try { | |
| return String.class.getDeclaredField("hash"); | |
| } catch (NoSuchFieldException e) { | |
| fail(); | |
| return null; | |
| } | |
| } | |
| void findAndInvokeSetAccessible(Class<? extends AccessibleObject> refc, Field f) throws Throwable { | |
| MethodType mt = MethodType.methodType(void.class, boolean.class); | |
| MethodHandle mh = MethodHandles.lookup().findVirtual(refc, "setAccessible", mt); | |
| mh.invoke(f, true); // may throw InaccessibleObjectException | |
| assertTrue(f.isAccessible()); | |
| } | |
| void unreflectAndInvokeSetAccessible(Method m, Field f) throws Throwable { | |
| assertTrue(m.getName().equals("setAccessible")); | |
| assertFalse(Modifier.isStatic(m.getModifiers())); | |
| MethodHandle mh = MethodHandles.lookup().unreflect(m); | |
| mh.invoke(f, true); // may throw InaccessibleObjectException | |
| assertTrue(f.isAccessible()); | |
| } | |
| /** | |
| * Create a method handle to setAccessible and use it to suppress access to an | |
| * accessible member. | |
| */ | |
| @Test | |
| public void testSetAccessible1() throws Throwable { | |
| findAndInvokeSetAccessible(AccessibleObject.class, accessibleField()); | |
| } | |
| @Test | |
| public void testSetAccessible2() throws Throwable { | |
| findAndInvokeSetAccessible(Field.class, accessibleField()); | |
| } | |
| @Test | |
| public void testSetAccessible3() throws Throwable { | |
| Method m = AccessibleObject.class.getMethod("setAccessible", boolean.class); | |
| unreflectAndInvokeSetAccessible(m, accessibleField()); | |
| } | |
| @Test | |
| public void testSetAccessible4() throws Throwable { | |
| Method m = Field.class.getMethod("setAccessible", boolean.class); | |
| unreflectAndInvokeSetAccessible(m, accessibleField()); | |
| } | |
| /** | |
| * Create a method handle to setAccessible and attempt to use it to suppress | |
| * access to an inaccessible member. | |
| */ | |
| @Test(expectedExceptions = InaccessibleObjectException.class) | |
| public void testSetAccessible5() throws Throwable { | |
| findAndInvokeSetAccessible(AccessibleObject.class, inaccessibleField()); | |
| } | |
| @Test(expectedExceptions = InaccessibleObjectException.class) | |
| public void testSetAccessible6() throws Throwable { | |
| findAndInvokeSetAccessible(Field.class, inaccessibleField()); | |
| } | |
| @Test(expectedExceptions = InaccessibleObjectException.class) | |
| public void testSetAccessible7() throws Throwable { | |
| Method m = AccessibleObject.class.getMethod("setAccessible", boolean.class); | |
| unreflectAndInvokeSetAccessible(m, inaccessibleField()); | |
| } | |
| @Test(expectedExceptions = InaccessibleObjectException.class) | |
| public void testSetAccessible8() throws Throwable { | |
| Method m = Field.class.getMethod("setAccessible", boolean.class); | |
| unreflectAndInvokeSetAccessible(m, inaccessibleField()); | |
| } | |
| // -- Test sub-classes of AccessibleObject -- | |
| /** | |
| * Custom AccessibleObject objects. One class overrides setAccessible, the other | |
| * does not override this method. | |
| */ | |
| @DataProvider(name = "customAccessibleObjects") | |
| static Object[][] customAccessibleObjectClasses() { | |
| return new Object[][] { { new S1() }, { new S2() } }; | |
| } | |
| public static class S1 extends AccessibleObject { } | |
| public static class S2 extends AccessibleObject { | |
| @Override | |
| public void setAccessible(boolean flag) { | |
| super.setAccessible(flag); | |
| } | |
| } | |
| void findAndInvokeSetAccessible(Lookup lookup, AccessibleObject obj) throws Throwable { | |
| MethodType mt = MethodType.methodType(void.class, boolean.class); | |
| MethodHandle mh = lookup.findVirtual(obj.getClass(), "setAccessible", mt); | |
| mh.invoke(obj, true); | |
| assertTrue(obj.isAccessible()); | |
| } | |
| void unreflectAndInvokeSetAccessible(Lookup lookup, AccessibleObject obj) throws Throwable { | |
| Method m = obj.getClass().getMethod("setAccessible", boolean.class); | |
| MethodHandle mh = lookup.unreflect(m); | |
| mh.invoke(obj, true); | |
| assertTrue(obj.isAccessible()); | |
| } | |
| /** | |
| * Using publicLookup, create a method handle to setAccessible and invoke it | |
| * on a custom AccessibleObject object. | |
| */ | |
| @Test(expectedExceptions = IllegalAccessException.class) | |
| public void testPublicLookupSubclass1() throws Throwable { | |
| // S1 does not override setAccessible | |
| findAndInvokeSetAccessible(MethodHandles.publicLookup(), new S1()); | |
| } | |
| @Test | |
| public void testPublicLookupSubclass2() throws Throwable { | |
| // S2 overrides setAccessible | |
| findAndInvokeSetAccessible(MethodHandles.publicLookup(), new S2()); | |
| } | |
| @Test(expectedExceptions = IllegalAccessException.class) | |
| public void testPublicLookupSubclass3() throws Throwable { | |
| // S1 does not override setAccessible | |
| unreflectAndInvokeSetAccessible(MethodHandles.publicLookup(), new S1()); | |
| } | |
| @Test | |
| public void testPublicLookupSubclass4() throws Throwable { | |
| // S2 overrides setAccessible | |
| unreflectAndInvokeSetAccessible(MethodHandles.publicLookup(), new S2()); | |
| } | |
| /** | |
| * Using a full power lookup, create a method handle to setAccessible and | |
| * invoke it on a custom AccessibleObject object. | |
| */ | |
| @Test(dataProvider = "customAccessibleObjects") | |
| public void testLookupSubclass1(AccessibleObject obj) throws Throwable { | |
| findAndInvokeSetAccessible(MethodHandles.lookup(), obj); | |
| } | |
| @Test(dataProvider = "customAccessibleObjects") | |
| public void testLookupSubclass2(AccessibleObject obj) throws Throwable { | |
| unreflectAndInvokeSetAccessible(MethodHandles.lookup(), obj); | |
| } | |
| /** | |
| * Using a full power lookup, create a method handle to setAccessible on a | |
| * sub-class of AccessibleObject and then attempt to invoke it on a Field object. | |
| */ | |
| @Test(dataProvider = "customAccessibleObjects", | |
| expectedExceptions = ClassCastException.class) | |
| public void testLookupSubclass3(AccessibleObject obj) throws Throwable { | |
| MethodType mt = MethodType.methodType(void.class, boolean.class); | |
| Lookup lookup = MethodHandles.lookup(); | |
| MethodHandle mh = lookup.findVirtual(obj.getClass(), "setAccessible", mt); | |
| mh.invoke(accessibleField(), true); // should throw ClassCastException | |
| } | |
| /** | |
| * Using a full power lookup, use unreflect to create a method handle to | |
| * setAccessible on a sub-class of AccessibleObject, then attempt to invoke | |
| * it on a Field object. | |
| */ | |
| @Test | |
| public void testLookupSubclass4() throws Throwable { | |
| // S1 does not override setAccessible | |
| Method m = S1.class.getMethod("setAccessible", boolean.class); | |
| assertTrue(m.getDeclaringClass() == AccessibleObject.class); | |
| MethodHandle mh = MethodHandles.lookup().unreflect(m); | |
| Field f = accessibleField(); | |
| mh.invoke(f, true); | |
| assertTrue(f.isAccessible()); | |
| } | |
| @Test(expectedExceptions = InaccessibleObjectException.class) | |
| public void testLookupSubclass5() throws Throwable { | |
| // S1 does not override setAccessible | |
| Method m = S1.class.getMethod("setAccessible", boolean.class); | |
| assertTrue(m.getDeclaringClass() == AccessibleObject.class); | |
| MethodHandle mh = MethodHandles.lookup().unreflect(m); | |
| mh.invoke(inaccessibleField(), true); // should throw InaccessibleObjectException | |
| } | |
| @Test(expectedExceptions = ClassCastException.class) | |
| public void testLookupSubclass6() throws Throwable { | |
| // S2 overrides setAccessible | |
| Method m = S2.class.getMethod("setAccessible", boolean.class); | |
| assertTrue(m.getDeclaringClass() == S2.class); | |
| MethodHandle mh = MethodHandles.lookup().unreflect(m); | |
| mh.invoke(accessibleField(), true); // should throw ClassCastException | |
| } | |
| @Test(expectedExceptions = ClassCastException.class) | |
| public void testLookupSubclass7() throws Throwable { | |
| // S2 overrides setAccessible | |
| Method m = S2.class.getMethod("setAccessible", boolean.class); | |
| assertTrue(m.getDeclaringClass() == S2.class); | |
| MethodHandle mh = MethodHandles.lookup().unreflect(m); | |
| mh.invoke(inaccessibleField(), true); // should throw ClassCastException | |
| } | |
| /** | |
| * Field::getInt and Field::setInt are caller-sensitive methods. | |
| * Test access to private field. | |
| */ | |
| private static int privateField = 0; | |
| @Test | |
| public void testPrivateField() throws Throwable { | |
| Field f = CallerSensitiveAccess.class.getDeclaredField("privateField"); | |
| // Field::setInt | |
| MethodType mtype = MethodType.methodType(void.class, Object.class, int.class); | |
| MethodHandle mh = MethodHandles.lookup().findVirtual(Field.class, "setInt", mtype); | |
| mh.invokeExact(f, (Object)null, 5); | |
| // Field::getInt | |
| mh = MethodHandles.lookup().findVirtual(Field.class, "getInt", MethodType.methodType(int.class, Object.class)); | |
| int value = (int)mh.invokeExact(f, (Object)null); | |
| assertTrue(value == 5); | |
| } | |
| private static class Inner { | |
| private static boolean privateField = false; | |
| } | |
| @Test | |
| public void testInnerPrivateField() throws Throwable { | |
| Field f = Inner.class.getDeclaredField("privateField"); | |
| // Field::setInt | |
| MethodType mtype = MethodType.methodType(void.class, Object.class, boolean.class); | |
| MethodHandle mh = MethodHandles.lookup().findVirtual(Field.class, "setBoolean", mtype); | |
| mh.invokeExact(f, (Object)null, true); | |
| // Field::getInt | |
| mh = MethodHandles.lookup().findVirtual(Field.class, "getBoolean", MethodType.methodType(boolean.class, Object.class)); | |
| boolean value = (boolean)mh.invokeExact(f, (Object)null); | |
| assertTrue(value); | |
| } | |
| // -- supporting methods -- | |
| /** | |
| * Returns a stream of all caller sensitive methods on public classes in packages | |
| * exported by a named module. | |
| */ | |
| static Stream<Method> callerSensitiveMethods(Module module) { | |
| assert module.isNamed(); | |
| ModuleReference mref = module.getLayer().configuration() | |
| .findModule(module.getName()) | |
| .orElseThrow(() -> new RuntimeException()) | |
| .reference(); | |
| // find all ".class" resources in the module | |
| // transform the resource name to a class name | |
| // load every class in the exported packages | |
| // return the caller sensitive methods of the public classes | |
| try (ModuleReader reader = mref.open()) { | |
| return reader.list() | |
| .filter(rn -> rn.endsWith(".class")) | |
| .map(rn -> rn.substring(0, rn.length() - 6) | |
| .replace('/', '.')) | |
| .filter(cn -> module.isExported(packageName(cn))) | |
| .map(cn -> Class.forName(module, cn)) | |
| .filter(refc -> refc != null | |
| && Modifier.isPublic(refc.getModifiers())) | |
| .map(refc -> callerSensitiveMethods(refc)) | |
| .flatMap(List::stream); | |
| } catch (IOException ioe) { | |
| throw new UncheckedIOException(ioe); | |
| } | |
| } | |
| static String packageName(String cn) { | |
| int last = cn.lastIndexOf('.'); | |
| if (last > 0) { | |
| return cn.substring(0, last); | |
| } else { | |
| return ""; | |
| } | |
| } | |
| /** | |
| * Returns a list of the caller sensitive methods directly declared by the given | |
| * class. | |
| */ | |
| static List<Method> callerSensitiveMethods(Class<?> refc) { | |
| return Arrays.stream(refc.getDeclaredMethods()) | |
| .filter(m -> m.isAnnotationPresent(CallerSensitive.class)) | |
| .collect(Collectors.toList()); | |
| } | |
| /** | |
| * Returns a short description of the given method for tracing purposes. | |
| */ | |
| static String shortDescription(Method m) { | |
| var sb = new StringBuilder(); | |
| sb.append(m.getDeclaringClass().getName()); | |
| sb.append('.'); | |
| sb.append(m.getName()); | |
| sb.append('('); | |
| StringJoiner sj = new StringJoiner(","); | |
| for (Class<?> parameterType : m.getParameterTypes()) { | |
| sj.add(parameterType.getTypeName()); | |
| } | |
| sb.append(sj); | |
| sb.append(')'); | |
| return sb.toString(); | |
| } | |
| } |