From 5d57b5c2f0ef77f994fbc8c4f09e66f217f06f85 Mon Sep 17 00:00:00 2001 From: Chen Liang Date: Thu, 20 Jul 2023 00:59:28 +0000 Subject: [PATCH] 6983726: Reimplement MethodHandleProxies.asInterfaceInstance Co-authored-by: Mandy Chung Reviewed-by: jvernee, mchung --- src/hotspot/share/prims/jvm.cpp | 7 +- .../java/lang/invoke/MethodHandleProxies.java | 553 ++++++++++++++---- .../java/lang/reflect/ReflectAccess.java | 11 +- .../access/JavaLangReflectAccess.java | 12 +- .../classes/sun/invoke/WrapperInstance.java | 48 -- .../invoke/MethodHandleProxies/BasicTest.java | 378 ++++++++++++ .../invoke/MethodHandleProxies/Client.java | 39 ++ .../MethodHandlesProxiesTest.java | 122 ---- .../WithSecurityManagerTest.java | 68 +++ .../WrapperHiddenClassTest.java | 214 +++++++ .../MethodHandleProxies/jtreg.security.policy | 9 + .../reflect/Proxy/ProxyForMethodHandle.java | 85 --- .../reflect/Proxy/ProxyModuleMapping.java | 8 +- 13 files changed, 1151 insertions(+), 403 deletions(-) delete mode 100644 src/java.base/share/classes/sun/invoke/WrapperInstance.java create mode 100644 test/jdk/java/lang/invoke/MethodHandleProxies/BasicTest.java create mode 100644 test/jdk/java/lang/invoke/MethodHandleProxies/Client.java delete mode 100644 test/jdk/java/lang/invoke/MethodHandleProxies/MethodHandlesProxiesTest.java create mode 100644 test/jdk/java/lang/invoke/MethodHandleProxies/WithSecurityManagerTest.java create mode 100644 test/jdk/java/lang/invoke/MethodHandleProxies/WrapperHiddenClassTest.java create mode 100644 test/jdk/java/lang/invoke/MethodHandleProxies/jtreg.security.policy delete mode 100644 test/jdk/java/lang/reflect/Proxy/ProxyForMethodHandle.java diff --git a/src/hotspot/share/prims/jvm.cpp b/src/hotspot/share/prims/jvm.cpp index 92a3b08b1a6..3e0d5b1eb45 100644 --- a/src/hotspot/share/prims/jvm.cpp +++ b/src/hotspot/share/prims/jvm.cpp @@ -1016,8 +1016,11 @@ static jclass jvm_lookup_define_class(jclass lookup, const char *name, ik->is_hidden() ? "is hidden" : "is not hidden"); } } - assert(Reflection::is_same_class_package(lookup_k, ik), - "lookup class and defined class are in different packages"); + + if ((!is_hidden || is_nestmate) && !Reflection::is_same_class_package(lookup_k, ik)) { + // non-hidden class or nestmate class must be in the same package as the Lookup class + THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "Lookup class and defined class are in different packages"); + } if (init) { ik->initialize(CHECK_NULL); diff --git a/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java b/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java index 69d975a7289..36f10cad1e5 100644 --- a/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java +++ b/src/java.base/share/classes/java/lang/invoke/MethodHandleProxies.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2023, 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 @@ -25,19 +25,44 @@ package java.lang.invoke; -import java.lang.reflect.*; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.module.ModuleDescriptor; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import jdk.internal.access.JavaLangReflectAccess; import jdk.internal.access.SharedSecrets; -import sun.invoke.WrapperInstance; -import java.util.ArrayList; - +import jdk.internal.classfile.ClassHierarchyResolver; +import jdk.internal.classfile.Classfile; +import jdk.internal.classfile.CodeBuilder; +import jdk.internal.classfile.TypeKind; +import jdk.internal.module.Modules; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.Reflection; +import jdk.internal.util.ClassFileDumper; import sun.reflect.misc.ReflectUtil; + +import static java.lang.constant.ConstantDescs.*; import static java.lang.invoke.MethodHandleStatics.*; +import static java.lang.invoke.MethodType.methodType; +import static java.lang.module.ModuleDescriptor.Modifier.SYNTHETIC; +import static jdk.internal.classfile.Classfile.*; /** * This class consists exclusively of static methods that help adapt @@ -61,7 +86,8 @@ public class MethodHandleProxies { * even though it re-declares the {@code Object.equals} method and also * declares default methods, such as {@code Comparator.reverse}. *

- * The interface must be public and not {@linkplain Class#isSealed() sealed}. + * The interface must be public, not {@linkplain Class#isHidden() hidden}, + * and not {@linkplain Class#isSealed() sealed}. * No additional access checks are performed. *

* The resulting instance of the required type will respond to @@ -133,37 +159,20 @@ public class MethodHandleProxies { * @throws WrongMethodTypeException if the target cannot * be converted to the type required by the requested interface */ - // Other notes to implementors: - //

- // No stable mapping is promised between the single-method interface and - // the implementation class C. Over time, several implementation - // classes might be used for the same type. - //

- // If the implementation is able - // to prove that a wrapper of the required type - // has already been created for a given - // method handle, or for another method handle with the - // same behavior, the implementation may return that wrapper in place of - // a new wrapper. - //

- // This method is designed to apply to common use cases - // where a single method handle must interoperate with - // an interface that implements a function-like - // API. Additional variations, such as single-abstract-method classes with - // private constructors, or interfaces with multiple but related - // entry points, must be covered by hand-written or automatically - // generated adapter classes. - // - @SuppressWarnings({"removal", - "doclint:reference"}) // cross-module links + @SuppressWarnings("doclint:reference") // cross-module links @CallerSensitive public static T asInterfaceInstance(final Class intfc, final MethodHandle target) { if (!intfc.isInterface() || !Modifier.isPublic(intfc.getModifiers())) throw newIllegalArgumentException("not a public interface", intfc.getName()); if (intfc.isSealed()) throw newIllegalArgumentException("a sealed interface", intfc.getName()); + if (intfc.isHidden()) + throw newIllegalArgumentException("a hidden interface", intfc.getName()); + Objects.requireNonNull(target); final MethodHandle mh; - if (System.getSecurityManager() != null) { + @SuppressWarnings("removal") + var sm = System.getSecurityManager(); + if (sm != null) { final Class caller = Reflection.getCallerClass(); final ClassLoader ccl = caller != null ? caller.getClassLoader() : null; ReflectUtil.checkProxyPackageAccess(ccl, intfc); @@ -171,66 +180,300 @@ public class MethodHandleProxies { } else { mh = target; } - ClassLoader proxyLoader = intfc.getClassLoader(); - if (proxyLoader == null) { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); // avoid use of BCP - proxyLoader = cl != null ? cl : ClassLoader.getSystemClassLoader(); - } - final Method[] methods = getSingleNameMethods(intfc); - if (methods == null) - throw newIllegalArgumentException("not a single-method interface", intfc.getName()); - final MethodHandle[] vaTargets = new MethodHandle[methods.length]; - for (int i = 0; i < methods.length; i++) { - Method sm = methods[i]; - MethodType smMT = MethodType.methodType(sm.getReturnType(), sm.getParameterTypes()); - MethodHandle checkTarget = mh.asType(smMT); // make throw WMT - checkTarget = checkTarget.asType(checkTarget.type().changeReturnType(Object.class)); - vaTargets[i] = checkTarget.asSpreader(Object[].class, smMT.parameterCount()); - } - final InvocationHandler ih = new InvocationHandler() { - private Object getArg(String name) { - if ((Object)name == "getWrapperInstanceTarget") return target; - if ((Object)name == "getWrapperInstanceType") return intfc; - throw new AssertionError(); - } - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - for (int i = 0; i < methods.length; i++) { - if (method.equals(methods[i])) - return vaTargets[i].invokeExact(args); - } - if (method.getDeclaringClass() == WrapperInstance.class) - return getArg(method.getName()); - if (isObjectMethod(method)) - return callObjectMethod(proxy, method, args); - if (isDefaultMethod(method)) { - // no additional access check is performed - return JLRA.invokeDefault(proxy, method, args, null); - } - throw newInternalError("bad proxy method: "+method); - } - }; - final Object proxy; - if (System.getSecurityManager() != null) { - // sun.invoke.WrapperInstance is a restricted interface not accessible - // by any non-null class loader. - final ClassLoader loader = proxyLoader; - proxy = AccessController.doPrivileged(new PrivilegedAction<>() { - public Object run() { - return Proxy.newProxyInstance( - loader, - new Class[]{ intfc, WrapperInstance.class }, - ih); - } - }); - } else { - proxy = Proxy.newProxyInstance(proxyLoader, - new Class[]{ intfc, WrapperInstance.class }, - ih); + // Define one hidden class for each interface. Create an instance of + // the hidden class for a given target method handle which will be + // accessed via getfield. Multiple instances may be created for a + // hidden class. This approach allows the generated hidden classes + // more shareable. + // + // The implementation class is weakly referenced; a new class is + // defined if the last one has been garbage collected. + // + // An alternative approach is to define one hidden class with the + // target method handle as class data and the target method handle + // is loaded via ldc/condy. If more than one target method handles + // are used, the extra classes will pollute the same type profiles. + // In addition, hidden classes without class data is more friendly + // for pre-generation (shifting the dynamic class generation from + // runtime to an earlier phrase). + Class proxyClass = getProxyClass(intfc); // throws IllegalArgumentException + Lookup lookup = new Lookup(proxyClass); + Object proxy; + try { + MethodHandle constructor = lookup.findConstructor(proxyClass, + MT_void_Lookup_MethodHandle_MethodHandle) + .asType(MT_Object_Lookup_MethodHandle_MethodHandle); + proxy = constructor.invokeExact(lookup, target, mh); + } catch (Throwable ex) { + throw uncaughtException(ex); } + assert proxy.getClass().getModule().isNamed() : proxy.getClass() + " " + proxy.getClass().getModule(); return intfc.cast(proxy); } + private record MethodInfo(MethodTypeDesc desc, List thrown, String fieldName) {} + + private static final ClassFileDumper DUMPER = ClassFileDumper.getInstance( + "jdk.invoke.MethodHandleProxies.dumpClassFiles", "DUMP_MH_PROXY_CLASSFILES"); + + private static final Set> WRAPPER_TYPES = Collections.newSetFromMap(new WeakHashMap<>()); + private static final ClassValue>> PROXIES = new ClassValue<>() { + @Override + protected WeakReferenceHolder> computeValue(Class intfc) { + return new WeakReferenceHolder<>(newProxyClass(intfc)); + } + }; + + private static Class newProxyClass(Class intfc) { + List methods = new ArrayList<>(); + Set> referencedTypes = new HashSet<>(); + referencedTypes.add(intfc); + String uniqueName = null; + int count = 0; + for (Method m : intfc.getMethods()) { + if (!Modifier.isAbstract(m.getModifiers())) + continue; + + if (isObjectMethod(m)) + continue; + + // ensure it's SAM interface + String methodName = m.getName(); + if (uniqueName == null) { + uniqueName = methodName; + } else if (!uniqueName.equals(methodName)) { + // too many abstract methods + throw newIllegalArgumentException("not a single-method interface", intfc.getName()); + } + + // the field name holding the method handle for this method + String fieldName = "m" + count++; + var mt = methodType(m.getReturnType(), JLRA.getExecutableSharedParameterTypes(m), true); + var thrown = JLRA.getExecutableSharedExceptionTypes(m); + var exceptionTypeDescs = + thrown.length == 0 ? DEFAULT_RETHROWS + : Stream.concat(DEFAULT_RETHROWS.stream(), + Arrays.stream(thrown).map(MethodHandleProxies::desc)) + .distinct().toList(); + methods.add(new MethodInfo(desc(mt), exceptionTypeDescs, fieldName)); + + // find the types referenced by this method + addElementType(referencedTypes, m.getReturnType()); + addElementTypes(referencedTypes, JLRA.getExecutableSharedParameterTypes(m)); + addElementTypes(referencedTypes, JLRA.getExecutableSharedExceptionTypes(m)); + } + + if (uniqueName == null) + throw newIllegalArgumentException("no method in ", intfc.getName()); + + // create a dynamic module for each proxy class, which needs access + // to the types referenced by the members of the interface including + // the parameter types, return type and exception types + var loader = intfc.getClassLoader(); + Module targetModule = newDynamicModule(loader, referencedTypes); + + // generate a class file in the package of the dynamic module + String packageName = targetModule.getName(); + String intfcName = intfc.getName(); + int i = intfcName.lastIndexOf('.'); + // jdk.MHProxy#.Interface + String className = packageName + "." + (i > 0 ? intfcName.substring(i + 1) : intfcName); + byte[] template = createTemplate(loader, ClassDesc.of(className), desc(intfc), uniqueName, methods); + // define the dynamic module to the class loader of the interface + var definer = new Lookup(intfc).makeHiddenClassDefiner(className, template, Set.of(), DUMPER); + + @SuppressWarnings("removal") + var sm = System.getSecurityManager(); + Lookup lookup; + if (sm != null) { + @SuppressWarnings("removal") + var l = AccessController.doPrivileged((PrivilegedAction) () -> + definer.defineClassAsLookup(true)); + lookup = l; + } else { + lookup = definer.defineClassAsLookup(true); + } + // cache the wrapper type + var ret = lookup.lookupClass(); + WRAPPER_TYPES.add(ret); + return ret; + } + + private static final class WeakReferenceHolder { + private volatile WeakReference ref; + + WeakReferenceHolder(T value) { + set(value); + } + + void set(T value) { + ref = new WeakReference<>(value); + } + + T get() { + return ref.get(); + } + } + + private static Class getProxyClass(Class intfc) { + WeakReferenceHolder> r = PROXIES.get(intfc); + Class cl = r.get(); + if (cl != null) + return cl; + + // avoid spinning multiple classes in a race + synchronized (r) { + cl = r.get(); + if (cl != null) + return cl; + + // If the referent is cleared, create a new value and update cached weak reference. + cl = newProxyClass(intfc); + r.set(cl); + return cl; + } + } + + private static final List DEFAULT_RETHROWS = List.of(desc(RuntimeException.class), desc(Error.class)); + private static final ClassDesc CD_UndeclaredThrowableException = desc(UndeclaredThrowableException.class); + private static final ClassDesc CD_IllegalAccessException = desc(IllegalAccessException.class); + private static final MethodTypeDesc MTD_void_Throwable = MethodTypeDesc.of(CD_void, CD_Throwable); + private static final MethodType MT_void_Lookup_MethodHandle_MethodHandle = + methodType(void.class, Lookup.class, MethodHandle.class, MethodHandle.class); + private static final MethodType MT_Object_Lookup_MethodHandle_MethodHandle = + MT_void_Lookup_MethodHandle_MethodHandle.changeReturnType(Object.class); + private static final MethodType MT_MethodHandle_Object = methodType(MethodHandle.class, Object.class); + private static final MethodTypeDesc MTD_void_Lookup_MethodHandle_MethodHandle = + desc(MT_void_Lookup_MethodHandle_MethodHandle); + private static final MethodTypeDesc MTD_void_Lookup = MethodTypeDesc.of(CD_void, CD_MethodHandles_Lookup); + private static final MethodTypeDesc MTD_MethodHandle_MethodType = MethodTypeDesc.of(CD_MethodHandle, CD_MethodType); + private static final MethodTypeDesc MTD_Class = MethodTypeDesc.of(CD_Class); + private static final MethodTypeDesc MTD_int = MethodTypeDesc.of(CD_int); + private static final MethodTypeDesc MTD_String = MethodTypeDesc.of(CD_String); + private static final MethodTypeDesc MTD_void_String = MethodTypeDesc.of(CD_void, CD_String); + private static final String TARGET_NAME = "target"; + private static final String TYPE_NAME = "interfaceType"; + private static final String ENSURE_ORIGINAL_LOOKUP = "ensureOriginalLookup"; + + /** + * Creates an implementation class file for a given interface. One implementation class is + * defined for each interface. + * + * @param ifaceDesc the given interface + * @param methodName the name of the single abstract method + * @param methods the information for implementation methods + * @return the bytes of the implementation classes + */ + private static byte[] createTemplate(ClassLoader loader, ClassDesc proxyDesc, ClassDesc ifaceDesc, + String methodName, List methods) { + return Classfile.of(ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(loader))) + .build(proxyDesc, clb -> { + clb.withSuperclass(CD_Object); + clb.withFlags(ACC_FINAL | ACC_SYNTHETIC); + clb.withInterfaceSymbols(ifaceDesc); + + // static and instance fields + clb.withField(TYPE_NAME, CD_Class, ACC_PRIVATE | ACC_STATIC | ACC_FINAL); + clb.withField(TARGET_NAME, CD_MethodHandle, ACC_PRIVATE | ACC_FINAL); + for (var mi : methods) { + clb.withField(mi.fieldName, CD_MethodHandle, ACC_PRIVATE | ACC_FINAL); + } + + // + clb.withMethodBody(CLASS_INIT_NAME, MTD_void, ACC_STATIC, cob -> { + cob.constantInstruction(ifaceDesc); + cob.putstatic(proxyDesc, TYPE_NAME, CD_Class); + cob.return_(); + }); + + // (Lookup, MethodHandle target, MethodHandle callerBoundTarget) + clb.withMethodBody(INIT_NAME, MTD_void_Lookup_MethodHandle_MethodHandle, 0, cob -> { + cob.aload(0); + cob.invokespecial(CD_Object, INIT_NAME, MTD_void); + + // call ensureOriginalLookup to verify the given Lookup has access + cob.aload(1); + cob.invokestatic(proxyDesc, "ensureOriginalLookup", MTD_void_Lookup); + + // this.target = target; + cob.aload(0); + cob.aload(2); + cob.putfield(proxyDesc, TARGET_NAME, CD_MethodHandle); + + // method handles adjusted to the method type of each method + for (var mi : methods) { + // this.m = callerBoundTarget.asType(xxType); + cob.aload(0); + cob.aload(3); + cob.constantInstruction(mi.desc); + cob.invokevirtual(CD_MethodHandle, "asType", MTD_MethodHandle_MethodType); + cob.putfield(proxyDesc, mi.fieldName, CD_MethodHandle); + } + + // complete + cob.return_(); + }); + + // private static void ensureOriginalLookup(Lookup) checks if the given Lookup + // has ORIGINAL access to this class, i.e. the lookup class is this class; + // otherwise, IllegalAccessException is thrown + clb.withMethodBody(ENSURE_ORIGINAL_LOOKUP, MTD_void_Lookup, ACC_PRIVATE | ACC_STATIC, cob -> { + var failLabel = cob.newLabel(); + // check lookupClass + cob.aload(0); + cob.invokevirtual(CD_MethodHandles_Lookup, "lookupClass", MTD_Class); + cob.constantInstruction(proxyDesc); + cob.if_acmpne(failLabel); + // check original access + cob.aload(0); + cob.invokevirtual(CD_MethodHandles_Lookup, "lookupModes", MTD_int); + cob.constantInstruction(Lookup.ORIGINAL); + cob.iand(); + cob.ifeq(failLabel); + // success + cob.return_(); + // throw exception + cob.labelBinding(failLabel); + cob.new_(CD_IllegalAccessException); + cob.dup(); + cob.aload(0); // lookup + cob.invokevirtual(CD_Object, "toString", MTD_String); + cob.invokespecial(CD_IllegalAccessException, INIT_NAME, MTD_void_String); + cob.athrow(); + }); + + // implementation methods + for (MethodInfo mi : methods) { + // no need to generate thrown exception attribute + clb.withMethodBody(methodName, mi.desc, ACC_PUBLIC, cob -> cob + .trying(bcb -> { + // return this.handleField.invokeExact(arguments...); + bcb.aload(0); + bcb.getfield(proxyDesc, mi.fieldName, CD_MethodHandle); + for (int j = 0; j < mi.desc.parameterCount(); j++) { + bcb.loadInstruction(TypeKind.from(mi.desc.parameterType(j)), + bcb.parameterSlot(j)); + } + bcb.invokevirtual(CD_MethodHandle, "invokeExact", mi.desc); + bcb.returnInstruction(TypeKind.from(mi.desc.returnType())); + }, ctb -> ctb + // catch (Error | RuntimeException | Declared ex) { throw ex; } + .catchingMulti(mi.thrown, CodeBuilder::athrow) + // catch (Throwable ex) { throw new UndeclaredThrowableException(ex); } + .catchingAll(cb -> cb + .new_(CD_UndeclaredThrowableException) + .dup_x1() + .swap() + .invokespecial(CD_UndeclaredThrowableException, + INIT_NAME, MTD_void_Throwable) + .athrow() + ) + )); + } + }); + } + private static MethodHandle bindCaller(MethodHandle target, Class hostClass) { return MethodHandleImpl.bindCaller(target, hostClass).withVarargs(target.isVarargsCollector()); } @@ -241,16 +484,7 @@ public class MethodHandleProxies { * @return true if the reference is not null and points to an object produced by {@code asInterfaceInstance} */ public static boolean isWrapperInstance(Object x) { - return x instanceof WrapperInstance; - } - - private static WrapperInstance asWrapperInstance(Object x) { - try { - if (x != null) - return (WrapperInstance) x; - } catch (ClassCastException ex) { - } - throw newIllegalArgumentException("not a wrapper instance"); + return x != null && WRAPPER_TYPES.contains(x.getClass()); } /** @@ -263,7 +497,17 @@ public class MethodHandleProxies { * @throws IllegalArgumentException if the reference x is not to a wrapper instance */ public static MethodHandle wrapperInstanceTarget(Object x) { - return asWrapperInstance(x).getWrapperInstanceTarget(); + if (!isWrapperInstance(x)) + throw new IllegalArgumentException("not a wrapper instance: " + x); + + try { + Class type = x.getClass(); + MethodHandle getter = new Lookup(type).findGetter(type, TARGET_NAME, MethodHandle.class) + .asType(MT_MethodHandle_Object); + return (MethodHandle) getter.invokeExact(x); + } catch (Throwable ex) { + throw uncaughtException(ex); + } } /** @@ -275,52 +519,105 @@ public class MethodHandleProxies { * @throws IllegalArgumentException if the reference x is not to a wrapper instance */ public static Class wrapperInstanceType(Object x) { - return asWrapperInstance(x).getWrapperInstanceType(); + if (!isWrapperInstance(x)) + throw new IllegalArgumentException("not a wrapper instance: " + x); + + try { + Class type = x.getClass(); + MethodHandle originalTypeField = new Lookup(type).findStaticGetter(type, TYPE_NAME, Class.class); + return (Class) originalTypeField.invokeExact(); + } catch (Throwable e) { + throw uncaughtException(e); + } + } + + private static ClassDesc desc(Class cl) { + return cl.describeConstable().orElseThrow(() -> newInternalError("Cannot convert class " + + cl.getName() + " to a constant")); + } + + private static MethodTypeDesc desc(MethodType mt) { + return mt.describeConstable().orElseThrow(() -> newInternalError("Cannot convert method type " + + mt + " to a constant")); + } + + private static final JavaLangReflectAccess JLRA = SharedSecrets.getJavaLangReflectAccess(); + private static final AtomicInteger counter = new AtomicInteger(); + + private static String nextModuleName() { + return "jdk.MHProxy" + counter.incrementAndGet(); + } + + /** + * Create a dynamic module defined to the given class loader and has + * access to the given types. + *

+ * The dynamic module contains only one single package named the same as + * the name of the dynamic module. It's not exported or open. + */ + private static Module newDynamicModule(ClassLoader ld, Set> types) { + Objects.requireNonNull(types); + + // create a dynamic module and setup module access + String mn = nextModuleName(); + ModuleDescriptor descriptor = ModuleDescriptor.newModule(mn, Set.of(SYNTHETIC)) + .packages(Set.of(mn)) + .build(); + + Module dynModule = Modules.defineModule(ld, descriptor, null); + Module javaBase = Object.class.getModule(); + + Modules.addReads(dynModule, javaBase); + Modules.addOpens(dynModule, mn, javaBase); + + for (Class c : types) { + ensureAccess(dynModule, c); + } + return dynModule; } private static boolean isObjectMethod(Method m) { return switch (m.getName()) { case "toString" -> m.getReturnType() == String.class - && m.getParameterCount() == 0; + && m.getParameterCount() == 0; case "hashCode" -> m.getReturnType() == int.class - && m.getParameterCount() == 0; + && m.getParameterCount() == 0; case "equals" -> m.getReturnType() == boolean.class - && m.getParameterCount() == 1 - && m.getParameterTypes()[0] == Object.class; + && m.getParameterCount() == 1 + && JLRA.getExecutableSharedParameterTypes(m)[0] == Object.class; default -> false; }; } - private static Object callObjectMethod(Object self, Method m, Object[] args) { - assert(isObjectMethod(m)) : m; - return switch (m.getName()) { - case "toString" -> java.util.Objects.toIdentityString(self); - case "hashCode" -> System.identityHashCode(self); - case "equals" -> (self == args[0]); - default -> null; - }; - } - - private static Method[] getSingleNameMethods(Class intfc) { - ArrayList methods = new ArrayList<>(); - String uniqueName = null; - for (Method m : intfc.getMethods()) { - if (isObjectMethod(m)) continue; - if (!Modifier.isAbstract(m.getModifiers())) continue; - String mname = m.getName(); - if (uniqueName == null) - uniqueName = mname; - else if (!uniqueName.equals(mname)) - return null; // too many abstract methods - methods.add(m); + /* + * Ensure the given module can access the given class. + */ + private static void ensureAccess(Module target, Class c) { + Module m = c.getModule(); + // add read edge and qualified export for the target module to access + if (!target.canRead(m)) { + Modules.addReads(target, m); + } + String pn = c.getPackageName(); + if (!m.isExported(pn, target)) { + Modules.addExports(m, pn, target); } - if (uniqueName == null) return null; - return methods.toArray(new Method[methods.size()]); } - private static boolean isDefaultMethod(Method m) { - return !Modifier.isAbstract(m.getModifiers()); + private static void addElementTypes(Set> types, Class... classes) { + for (var cls : classes) { + addElementType(types, cls); + } } - private static final JavaLangReflectAccess JLRA = SharedSecrets.getJavaLangReflectAccess(); + private static void addElementType(Set> types, Class cls) { + Class e = cls; + while (e.isArray()) { + e = e.getComponentType(); + } + + if (!e.isPrimitive()) { + types.add(e); + } + } } diff --git a/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java b/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java index fa5a5d453ec..f815862edf6 100644 --- a/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java +++ b/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2023, 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 @@ -94,6 +94,10 @@ class ReflectAccess implements jdk.internal.access.JavaLangReflectAccess { return ex.getSharedParameterTypes(); } + public Class[] getExecutableSharedExceptionTypes(Executable ex) { + return ex.getSharedExceptionTypes(); + } + // // Copying routines, needed to quickly fabricate new Field, // Method, and Constructor objects from templates @@ -127,9 +131,4 @@ class ReflectAccess implements jdk.internal.access.JavaLangReflectAccess { { return ctor.newInstanceWithCaller(args, true, caller); } - - public Object invokeDefault(Object proxy, Method method, Object[] args, Class caller) - throws Throwable { - return Proxy.invokeDefault(proxy, method, args, caller); - } } diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java index 9e3ce846f02..f49221e44ed 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2023, 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 @@ -76,6 +76,9 @@ public interface JavaLangReflectAccess { /** Gets the shared array of parameter types of an Executable. */ public Class[] getExecutableSharedParameterTypes(Executable ex); + /** Gets the shared array of exception types of an Executable. */ + public Class[] getExecutableSharedExceptionTypes(Executable ex); + // // Copying routines, needed to quickly fabricate new Field, // Method, and Constructor objects from templates @@ -102,11 +105,4 @@ public interface JavaLangReflectAccess { /** Returns a new instance created by the given constructor with access check */ public T newInstance(Constructor ctor, Object[] args, Class caller) throws IllegalAccessException, InstantiationException, InvocationTargetException; - - /** Invokes the given default method if the method's declaring interface is - * accessible to the given caller. Otherwise, IllegalAccessException will - * be thrown. If the caller is null, no access check is performed. - */ - public Object invokeDefault(Object proxy, Method method, Object[] args, Class caller) - throws Throwable; } diff --git a/src/java.base/share/classes/sun/invoke/WrapperInstance.java b/src/java.base/share/classes/sun/invoke/WrapperInstance.java deleted file mode 100644 index f7cba5acc45..00000000000 --- a/src/java.base/share/classes/sun/invoke/WrapperInstance.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2011, 2013, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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. - */ - -package sun.invoke; - -import java.lang.invoke.MethodHandle; - -/** - * Private API used inside of java.lang.invoke.MethodHandles. - * Interface implemented by every object which is produced by - * {@link java.lang.invoke.MethodHandleProxies#asInterfaceInstance MethodHandleProxies.asInterfaceInstance}. - * The methods of this interface allow a caller to recover the parameters - * to {@code asInstance}. - * This allows applications to repeatedly convert between method handles - * and SAM objects, without the risk of creating unbounded delegation chains. - */ -public interface WrapperInstance { - /** Produce or recover a target method handle which is behaviorally - * equivalent to the SAM method of this object. - */ - public MethodHandle getWrapperInstanceTarget(); - /** Recover the SAM type for which this object was created. - */ - public Class getWrapperInstanceType(); -} - diff --git a/test/jdk/java/lang/invoke/MethodHandleProxies/BasicTest.java b/test/jdk/java/lang/invoke/MethodHandleProxies/BasicTest.java new file mode 100644 index 00000000000..656083e3a6e --- /dev/null +++ b/test/jdk/java/lang/invoke/MethodHandleProxies/BasicTest.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2023, 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. + */ + +import jdk.internal.classfile.ClassHierarchyResolver; +import jdk.internal.classfile.Classfile; +import org.junit.jupiter.api.Test; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.WrongMethodTypeException; +import java.lang.reflect.AccessFlag; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.IntSupplier; +import java.util.function.ToLongFunction; + +import static java.lang.constant.ConstantDescs.*; +import static java.lang.invoke.MethodHandleProxies.*; +import static java.lang.invoke.MethodType.genericMethodType; +import static java.lang.invoke.MethodType.methodType; +import static jdk.internal.classfile.Classfile.*; +import static org.junit.jupiter.api.Assertions.*; + +/* + * @test + * @bug 6983726 8206955 8269351 + * @modules java.base/jdk.internal.classfile + * java.base/jdk.internal.classfile.attribute + * java.base/jdk.internal.classfile.constantpool + * @summary Basic sanity tests for MethodHandleProxies + * @build BasicTest Client + * @run junit BasicTest + */ +public class BasicTest { + + @Test + public void testUsual() throws Throwable { + AtomicInteger ai = new AtomicInteger(5); + var mh = MethodHandles.lookup().findVirtual(AtomicInteger.class, "getAndIncrement", methodType(int.class)); + IntSupplier is = asInterfaceInstance(IntSupplier.class, mh.bindTo(ai)); + assertEquals(5, is.getAsInt()); + assertEquals(6, is.getAsInt()); + assertEquals(7, is.getAsInt()); + } + + /** + * Established null behaviors of MHP API. + */ + @Test + public void testNulls() { + assertThrows(NullPointerException.class, () -> + asInterfaceInstance(null, MethodHandles.zero(void.class)), + "asInterfaceInstance - intfc"); + assertThrows(NullPointerException.class, () -> + asInterfaceInstance(Runnable.class, null), + "asInterfaceInstance - target"); + + assertFalse(isWrapperInstance(null), "isWrapperInstance"); + + assertThrows(IllegalArgumentException.class, () -> wrapperInstanceTarget(null), + "wrapperInstanceTarget"); + assertThrows(IllegalArgumentException.class, () -> wrapperInstanceType(null), + "wrapperInstanceType"); + } + + @Test + public void testWrapperInstance() throws Throwable { + var mh = MethodHandles.publicLookup() + .findVirtual(Integer.class, "compareTo", methodType(int.class, Integer.class)); + @SuppressWarnings("unchecked") + Comparator proxy = (Comparator) asInterfaceInstance(Comparator.class, mh); + + assertTrue(isWrapperInstance(proxy)); + assertSame(mh, wrapperInstanceTarget(proxy)); + assertSame(Comparator.class, wrapperInstanceType(proxy)); + } + + /** + * Tests undeclared exceptions and declared exceptions in proxies. + */ + @Test + public void testThrowables() { + // don't wrap + assertThrows(Error.class, throwing(Error.class, new Error())::close, + "Errors should be propagated"); + assertThrows(RuntimeException.class, throwing(RuntimeException.class, new RuntimeException())::close, + "RuntimeException should be propagated"); + assertThrows(IOException.class, throwing(IOException.class, new IOException())::close, + "Declared IOException should be propagated"); + // wrap + assertThrows(UndeclaredThrowableException.class, throwing(IllegalAccessException.class, + new IllegalAccessException())::close, + "Undeclared IllegalAccessException should be wrapped"); + } + + /** + * Tests that invalid interfaces are rejected. + */ + @Test + public void testRejects() { + var mh = MethodHandles.constant(String.class, "42"); + assertThrows(IllegalArgumentException.class, () -> asInterfaceInstance(PackagePrivate.class, mh), + "non-public interface"); + assertThrows(IllegalArgumentException.class, () -> asInterfaceInstance(loadHidden(), mh), + "hidden interface"); + assertThrows(IllegalArgumentException.class, () -> asInterfaceInstance(MultiAbstractMethods.class, mh), + "multiple abstract method names"); + assertThrows(IllegalArgumentException.class, () -> asInterfaceInstance(NoAbstractMethods.class, mh), + "no abstract method"); + assertThrows(IllegalArgumentException.class, () -> asInterfaceInstance(Sealed.class, mh), + "sealed interface"); + } + + /** + * Tests that non-sealed interfaces can be implemented. + */ + @Test + public void testNonSealed() { + MethodHandle target = MethodHandles.constant(String.class, "Non-Sealed"); + NonSealed proxy = asInterfaceInstance(NonSealed.class, target); + assertEquals(proxy.m(), "Non-Sealed"); + } + + /** + * Tests that Proxy correctly proxies potential bridge abstract methods. + */ + @Test + public void testMultiSameName() throws Throwable { + var baseAndChild = loadBaseAndChild(); + var baseClass = baseAndChild.get(0); + var childClass = baseAndChild.get(1); + checkMethods(childClass.getMethods()); + checkMethods(childClass.getDeclaredMethods()); + + var lookup = MethodHandles.lookup(); + var baseValueMh = lookup.findVirtual(baseClass, "value", genericMethodType(0)) + .asType(genericMethodType(1)); + var childIntegerValueMh = lookup.findVirtual(childClass, "value", methodType(Integer.class)) + .asType(methodType(Integer.class, Object.class)); + var childIntValueMh = lookup.findVirtual(childClass, "value", methodType(int.class)) + .asType(methodType(int.class, Object.class)); + + Object child = asInterfaceInstance(childClass, MethodHandles.constant(Integer.class, 7)); + + assertEquals(7, (Object) baseValueMh.invokeExact(child)); + assertEquals(7, (Integer) childIntegerValueMh.invokeExact(child)); + assertEquals(7, (int) childIntValueMh.invokeExact(child)); + } + + /** + * Tests that default methods can be used. + */ + @Test + public void testDefaultMethods() { + MethodHandle target = MethodHandles.constant(String.class, "F"); + C proxy = asInterfaceInstance(C.class, target); + + assertEquals(proxy.f(), "F"); + assertEquals(proxy.a(), "A"); + assertEquals(proxy.b(), "B"); + assertEquals(proxy.c(), "C"); + assertEquals(proxy.concat(), "ABC"); + } + + /** + * Tests that correct implementation of default methods are called, + * and correct abstract methods are implemented. + */ + @Test + public void testOverrides() { + MethodHandle target = MethodHandles.constant(String.class, "concat"); + D proxy = asInterfaceInstance(D.class, target); + + assertEquals(proxy.a(), "OA"); + assertEquals(proxy.b(), "OB"); + assertEquals(proxy.c(), "OC"); + assertEquals(proxy.f(), "OF"); + assertEquals(proxy.concat(), "concat"); + } + + /** + * Tests primitive type conversions in proxies. + */ + @Test + public void testPrimitiveConversion() throws Throwable { + var mh = MethodHandles.lookup().findStatic(BasicTest.class, "mul", + methodType(long.class, int.class)); + @SuppressWarnings("unchecked") + Function func = (Function) asInterfaceInstance(Function.class, mh); + assertEquals(32423432L * 32423432L, func.apply(32423432)); + @SuppressWarnings("unchecked") + ToLongFunction func1 = (ToLongFunction) asInterfaceInstance(ToLongFunction.class, mh); + assertEquals(32423432L * 32423432L, func1.applyAsLong(32423432)); + @SuppressWarnings("unchecked") + IntFunction func2 = (IntFunction) asInterfaceInstance(IntFunction.class, mh); + assertEquals(32423432L * 32423432L, func2.apply(32423432)); + } + + /** + * Tests common type conversions in proxies. + */ + @Test + public void testBasicConversion() { + var mh = MethodHandles.constant(String.class, "42"); + asInterfaceInstance(Client.class, mh).exec(); // return value dropped, runs fine + + var nullMh = MethodHandles.zero(String.class); + var badIterable = asInterfaceInstance(Iterable.class, nullMh); + assertNull(badIterable.iterator()); // null is convertible + } + + /** + * Tests incompatible type conversions in proxy construction. + */ + @Test + public void testWrongConversion() { + var mh = MethodHandles.constant(String.class, "42"); + assertThrows(WrongMethodTypeException.class, () -> asInterfaceInstance(IntSupplier.class, mh), + "cannot convert String return to int under any circumstance"); + + var proxy = asInterfaceInstance(Iterable.class, mh); + assertThrows(ClassCastException.class, proxy::iterator); + } + + private static Closeable throwing(Class clz, T value) { + return asInterfaceInstance(Closeable.class, MethodHandles.throwException(void.class, clz).bindTo(value)); + } + + private static long mul(int i) { + return (long) i * i; + } + + void checkMethods(Method[] methods) { + assertTrue(methods.length > 1, () -> "Should have more than 1 declared methods, found only " + Arrays.toString(methods)); + for (Method method : methods) { + assertTrue(method.accessFlags().contains(AccessFlag.ABSTRACT), () -> method + " is not abstract"); + } + } + + private Class loadHidden() { + try (var is = BasicTest.class.getResourceAsStream("Client.class")) { + var bytes = Objects.requireNonNull(is).readAllBytes(); + var lookup = MethodHandles.lookup(); + return lookup.defineHiddenClass(bytes, true).lookupClass(); + } catch (Throwable ex) { + return fail("Hidden interface loading failure", ex); + } + } + + // Base: Object value(); + // Child: Integer value(); int value(); + private List> loadBaseAndChild() throws IllegalAccessException { + ClassDesc baseCd = ClassDesc.of("BasicTest$Base"); + ClassDesc childCd = ClassDesc.of("BasicTest$Child"); + var objMtd = MethodTypeDesc.of(CD_Object); + var integerMtd = MethodTypeDesc.of(CD_Integer); + var intMtd = MethodTypeDesc.of(CD_int); + var classfile = Classfile.of(Classfile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.defaultResolver().orElse( + ClassHierarchyResolver.of(List.of(baseCd, childCd), Map.ofEntries(Map.entry(baseCd, CD_Object), + Map.entry(childCd, CD_Object)))))); + + var baseBytes = classfile.build(baseCd, clb -> { + clb.withSuperclass(CD_Object); + clb.withFlags(ACC_PUBLIC | ACC_INTERFACE | ACC_ABSTRACT); + clb.withMethod("value", objMtd, ACC_PUBLIC | ACC_ABSTRACT, mb -> {}); + }); + + var lookup = MethodHandles.lookup(); + var base = lookup.ensureInitialized(lookup.defineClass(baseBytes)); + + var childBytes = classfile.build(childCd, clb -> { + clb.withSuperclass(CD_Object); + clb.withInterfaceSymbols(baseCd); + clb.withFlags(ACC_PUBLIC | ACC_INTERFACE | ACC_ABSTRACT); + clb.withMethod("value", integerMtd, ACC_PUBLIC | ACC_ABSTRACT, mb -> {}); + clb.withMethod("value", intMtd, ACC_PUBLIC | ACC_ABSTRACT, mb -> {}); + }); + + var child = lookup.ensureInitialized(lookup.defineClass(childBytes)); + return List.of(base, child); + } + + public interface MultiAbstractMethods { + String a(); + String b(); + } + + public interface NoAbstractMethods { + String toString(); + } + + interface PackagePrivate { + Object value(); + } + + public interface A { + default String a() { + return "A"; + } + } + + public interface B { + default String b() { + return "B"; + } + } + + public interface C extends A, B { + String f(); + + default String c() { + return "C"; + } + + default String concat() { + return a() + b() + c(); + } + } + + public interface D extends C { + String concat(); + + default String f() { + return "OF"; + } + + default String a() { + return "OA"; + } + + default String b() { + return "OB"; + } + + default String c() { + return "OC"; + } + } + + public sealed interface Sealed permits NonSealed { + String m(); + } + + public non-sealed interface NonSealed extends Sealed { + } +} diff --git a/test/jdk/java/lang/invoke/MethodHandleProxies/Client.java b/test/jdk/java/lang/invoke/MethodHandleProxies/Client.java new file mode 100644 index 00000000000..7fe03288f37 --- /dev/null +++ b/test/jdk/java/lang/invoke/MethodHandleProxies/Client.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, 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. + */ + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; + +/** + * A general-purposed public client interface for testing. + */ +public interface Client { + /** + * Returns a lookup from Client.class. + */ + static Lookup lookup() { + return MethodHandles.lookup(); + } + + void exec(); +} diff --git a/test/jdk/java/lang/invoke/MethodHandleProxies/MethodHandlesProxiesTest.java b/test/jdk/java/lang/invoke/MethodHandleProxies/MethodHandlesProxiesTest.java deleted file mode 100644 index 48624b1c925..00000000000 --- a/test/jdk/java/lang/invoke/MethodHandleProxies/MethodHandlesProxiesTest.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 8206955 8269351 - * @run testng/othervm -ea -esa test.java.lang.invoke.MethodHandlesProxiesTest - */ - -package test.java.lang.invoke; - -import org.testng.annotations.Test; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandleProxies; -import java.lang.invoke.MethodHandles; - -import static org.testng.Assert.assertEquals; - -public class MethodHandlesProxiesTest { - - public interface A { - default String a() { - return "A"; - } - } - - public interface B { - default String b() { - return "B"; - } - } - - public interface C extends A, B { - String f(); - - default String c() { - return "C"; - } - - default String concat() { - return a() + b() + c(); - } - } - - public interface Override extends C { - String f(); - - default String a() { - return "OA"; - } - - default String b() { - return "OB"; - } - - default String c() { - return "OC"; - } - } - - @Test - public static void testDefaultMethods() throws Throwable { - MethodHandle target = MethodHandles.constant(String.class, "F"); - C proxy = MethodHandleProxies.asInterfaceInstance(C.class, target); - - assertEquals(proxy.f(), "F"); - assertEquals(proxy.a(), "A"); - assertEquals(proxy.b(), "B"); - assertEquals(proxy.c(), "C"); - assertEquals(proxy.concat(), "ABC"); - } - - @Test - public static void testOverriddenDefaultMethods() throws Throwable { - MethodHandle target = MethodHandles.constant(String.class, "F"); - Override proxy = MethodHandleProxies.asInterfaceInstance(Override.class, target); - - assertEquals(proxy.a(), "OA"); - assertEquals(proxy.b(), "OB"); - assertEquals(proxy.c(), "OC"); - } - - public sealed interface Intf permits NonSealedInterface { - String m(); - } - - public non-sealed interface NonSealedInterface extends Intf { - } - - @Test(expectedExceptions = { IllegalArgumentException.class }) - public void testSealedInterface() { - MethodHandle target = MethodHandles.constant(String.class, "Sealed"); - MethodHandleProxies.asInterfaceInstance(Intf.class, target); - } - - @Test - public void testNonSealedInterface() { - MethodHandle target = MethodHandles.constant(String.class, "Non-Sealed"); - NonSealedInterface proxy = MethodHandleProxies.asInterfaceInstance(NonSealedInterface.class, target); - assertEquals(proxy.m(), "Non-Sealed"); - } -} diff --git a/test/jdk/java/lang/invoke/MethodHandleProxies/WithSecurityManagerTest.java b/test/jdk/java/lang/invoke/MethodHandleProxies/WithSecurityManagerTest.java new file mode 100644 index 00000000000..7e1882237c4 --- /dev/null +++ b/test/jdk/java/lang/invoke/MethodHandleProxies/WithSecurityManagerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023, 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. + */ + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandleProxies; +import java.lang.invoke.MethodHandles; +import java.util.List; + +import static jdk.test.lib.Asserts.assertSame; + +/* + * @test + * @bug 6983726 + * @library /test/lib + * @build jdk.test.lib.Asserts Client + * @run main/othervm/policy=jtreg.security.policy WithSecurityManagerTest + * @summary Checks MethodHandleProxies behavior with security manager present + */ +public class WithSecurityManagerTest { + public interface NestedInterface { + void task(); + } + + public static void main(String... args) { + var originalMh = MethodHandles.zero(void.class); + + // Test system and user interfaces + for (Class cl : List.of(Runnable.class, Client.class, NestedInterface.class)) { + try { + Object o = MethodHandleProxies.asInterfaceInstance(cl, originalMh); + testWrapperInstanceTarget(o, originalMh); + testWrapperInstanceType(o, cl); + } catch (Throwable ex) { + throw new AssertionError("Test failed for " + cl, ex); + } + } + } + + private static void testWrapperInstanceTarget(Object wrapper, MethodHandle originalMh) { + var recoveredTarget = MethodHandleProxies.wrapperInstanceTarget(wrapper); + assertSame(originalMh, recoveredTarget, "wrapperInstanceTarget recovery"); + } + + private static void testWrapperInstanceType(Object wrapper, Class type) { + var recoveredType = MethodHandleProxies.wrapperInstanceType(wrapper); + assertSame(type, recoveredType, "wrapperInstanceType recovery"); + } +} diff --git a/test/jdk/java/lang/invoke/MethodHandleProxies/WrapperHiddenClassTest.java b/test/jdk/java/lang/invoke/MethodHandleProxies/WrapperHiddenClassTest.java new file mode 100644 index 00000000000..d3d488385ae --- /dev/null +++ b/test/jdk/java/lang/invoke/MethodHandleProxies/WrapperHiddenClassTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2023, 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. + */ + +import jdk.internal.classfile.Classfile; +import jdk.test.lib.util.ForceGC; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandleProxies; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.ref.WeakReference; +import java.util.Comparator; + +import static java.lang.constant.ConstantDescs.*; +import static java.lang.invoke.MethodHandleProxies.*; +import static java.lang.invoke.MethodType.methodType; +import static jdk.internal.classfile.Classfile.*; +import static org.junit.jupiter.api.Assertions.*; + +/* + * @test + * @bug 6983726 + * @library /test/lib + * @modules java.base/jdk.internal.classfile + * java.base/jdk.internal.classfile.attribute + * java.base/jdk.internal.classfile.constantpool + * @summary Tests on implementation hidden classes spinned by MethodHandleProxies + * @build WrapperHiddenClassTest Client jdk.test.lib.util.ForceGC + * @run junit WrapperHiddenClassTest + */ +public class WrapperHiddenClassTest { + + /** + * Tests an adversary "implementation" class will not be + * "recovered" by the wrapperInstance* APIs + */ + @Test + public void testWrapperInstance() throws Throwable { + Comparator hostile = createHostileInstance(); + var mh = MethodHandles.publicLookup() + .findVirtual(Integer.class, "compareTo", methodType(int.class, Integer.class)); + @SuppressWarnings("unchecked") + Comparator proxy = (Comparator) asInterfaceInstance(Comparator.class, mh); + + assertTrue(isWrapperInstance(proxy)); + assertFalse(isWrapperInstance(hostile)); + assertSame(mh, wrapperInstanceTarget(proxy)); + assertThrows(IllegalArgumentException.class, () -> wrapperInstanceTarget(hostile)); + assertSame(Comparator.class, wrapperInstanceType(proxy)); + assertThrows(IllegalArgumentException.class, () -> wrapperInstanceType(hostile)); + } + + private static final String TYPE = "interfaceType"; + private static final String TARGET = "target"; + private static final ClassDesc CD_HostileWrapper = ClassDesc.of("HostileWrapper"); + private static final ClassDesc CD_Comparator = ClassDesc.of("java.util.Comparator"); + private static final MethodTypeDesc MTD_int_Object_Object = MethodTypeDesc.of(CD_int, CD_Object, CD_Object); + private static final MethodTypeDesc MTD_int_Integer = MethodTypeDesc.of(CD_int, CD_Integer); + + // Update this template when the MHP template is updated + @SuppressWarnings("unchecked") + private Comparator createHostileInstance() throws Throwable { + var cf = Classfile.of(); + var bytes = cf.build(CD_HostileWrapper, clb -> { + clb.withSuperclass(CD_Object); + clb.withFlags(ACC_FINAL | ACC_SYNTHETIC); + clb.withInterfaceSymbols(CD_Comparator); + + // static and instance fields + clb.withField(TYPE, CD_Class, ACC_PRIVATE | ACC_STATIC | ACC_FINAL); + clb.withField(TARGET, CD_MethodHandle, ACC_PRIVATE | ACC_FINAL); + + // + clb.withMethodBody(CLASS_INIT_NAME, MTD_void, ACC_STATIC, cob -> { + cob.constantInstruction(CD_Comparator); + cob.putstatic(CD_HostileWrapper, TYPE, CD_Class); + cob.return_(); + }); + + // + clb.withMethodBody(INIT_NAME, MTD_void, ACC_PUBLIC, cob -> { + cob.aload(0); + cob.invokespecial(CD_Object, INIT_NAME, MTD_void); + cob.return_(); + }); + + // implementation + clb.withMethodBody("compare", MTD_int_Object_Object, ACC_PUBLIC, cob -> { + cob.aload(1); + cob.checkcast(CD_Integer); + cob.aload(2); + cob.checkcast(CD_Integer); + cob.invokestatic(CD_Integer, "compareTo", MTD_int_Integer); + cob.ireturn(); + }); + }); + var l = MethodHandles.lookup().defineHiddenClass(bytes, true); + return (Comparator) l.findConstructor(l.lookupClass(), MethodType.methodType(void.class)).invoke(); + } + + /** + * Ensures a user interface cannot access a Proxy implementing it. + */ + @Test + public void testNoAccess() { + var instance = asInterfaceInstance(Client.class, MethodHandles.zero(void.class)); + var instanceClass = instance.getClass(); + var interfaceLookup = Client.lookup(); + assertEquals(MethodHandles.Lookup.ORIGINAL, interfaceLookup.lookupModes() & MethodHandles.Lookup.ORIGINAL, + "Missing original flag on interface's lookup"); + assertThrows(IllegalAccessException.class, () -> MethodHandles.privateLookupIn(instanceClass, + interfaceLookup)); + } + + /** + * Tests the Proxy module properties for Proxies implementing system and + * user interfaces. + */ + @ParameterizedTest + @ValueSource(classes = {Client.class, Runnable.class}) + public void testModule(Class ifaceClass) { + var mh = MethodHandles.zero(void.class); + + var inst = asInterfaceInstance(ifaceClass, mh); + Module ifaceModule = ifaceClass.getModule(); + Class implClass = inst.getClass(); + Module implModule = implClass.getModule(); + + String implPackage = implClass.getPackageName(); + assertFalse(implModule.isExported(implPackage), + "implementation should not be exported"); + assertTrue(ifaceModule.isExported(ifaceClass.getPackageName(), implModule), + "interface package should be exported to implementation"); + assertTrue(implModule.isOpen(implPackage, MethodHandleProxies.class.getModule()), + "implementation class is not reflectively open to MHP class"); + assertTrue(implModule.isNamed(), "dynamic module must be named"); + assertTrue(implModule.getName().startsWith("jdk.MHProxy"), + () -> "incorrect dynamic module name: " + implModule.getName()); + + assertSame(ifaceClass.getClassLoader(), implClass.getClassLoader(), + "wrapper class should use the interface's loader "); + assertSame(implClass.getClassLoader(), implModule.getClassLoader(), + "module class loader should be wrapper class's loader"); + } + + /** + * Tests the access control of Proxies implementing system and user + * interfaces. + */ + @ParameterizedTest + @ValueSource(classes = {Client.class, Runnable.class}) + public void testNoInstantiation(Class ifaceClass) throws ReflectiveOperationException { + var mh = MethodHandles.zero(void.class); + var instanceClass = asInterfaceInstance(ifaceClass, mh).getClass(); + var ctor = instanceClass.getDeclaredConstructor(MethodHandles.Lookup.class, MethodHandle.class, MethodHandle.class); + + assertThrows(IllegalAccessException.class, () -> ctor.newInstance(Client.lookup(), mh, mh)); + assertThrows(IllegalAccessException.class, () -> ctor.newInstance(MethodHandles.lookup(), mh, mh)); + assertThrows(IllegalAccessException.class, () -> ctor.newInstance(MethodHandles.publicLookup(), mh, mh)); + } + + /** + * Tests the caching and weak reference of implementation classes for + * system and user interfaces. + */ + @ParameterizedTest + @ValueSource(classes = {Runnable.class, Client.class}) + public void testWeakImplClass(Class ifaceClass) { + var mh = MethodHandles.zero(void.class); + + var wrapper1 = asInterfaceInstance(ifaceClass, mh); + var implClass = wrapper1.getClass(); + + System.gc(); // helps debug if incorrect items are weakly referenced + var wrapper2 = asInterfaceInstance(ifaceClass, mh); + assertSame(implClass, wrapper2.getClass(), + "MHP should reuse old implementation class when available"); + + var implClassRef = new WeakReference<>(implClass); + // clear strong references + implClass = null; + wrapper1 = null; + wrapper2 = null; + + if (!ForceGC.wait(() -> implClassRef.refersTo(null))) { + fail("MHP impl class cannot be cleared by GC"); + } + } +} diff --git a/test/jdk/java/lang/invoke/MethodHandleProxies/jtreg.security.policy b/test/jdk/java/lang/invoke/MethodHandleProxies/jtreg.security.policy new file mode 100644 index 00000000000..d32c7af9b3f --- /dev/null +++ b/test/jdk/java/lang/invoke/MethodHandleProxies/jtreg.security.policy @@ -0,0 +1,9 @@ +/* + * security policy used by the test process + * must allow file reads so that jtreg itself can run + */ + +grant { + // standard test activation permissions + permission java.io.FilePermission "*", "read"; +}; diff --git a/test/jdk/java/lang/reflect/Proxy/ProxyForMethodHandle.java b/test/jdk/java/lang/reflect/Proxy/ProxyForMethodHandle.java deleted file mode 100644 index a5877018705..00000000000 --- a/test/jdk/java/lang/reflect/Proxy/ProxyForMethodHandle.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2015, 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. - */ - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandleProxies; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; - -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; -import static org.testng.Assert.*; - -/** - * @test - * @summary test MethodHandleProxies that adds qualified export of sun.invoke - * from java.base to a dynamic module - * @run testng ProxyForMethodHandle - */ -public class ProxyForMethodHandle { - /** - * MethodHandleProxies will add qualified export of sun.invoke from java.base - * to a dynamic module - */ - @Test - public static void testRunnableMethodHandle() throws Exception { - MethodHandles.Lookup lookup = MethodHandles.lookup(); - MethodType mt = MethodType.methodType(void.class); - MethodHandle mh = lookup.findStatic(ProxyForMethodHandle.class, "runForRunnable", mt); - Runnable proxy = MethodHandleProxies.asInterfaceInstance(Runnable.class, mh); - proxy.run(); - - Class proxyClass = proxy.getClass(); - Module target = proxyClass.getModule(); - assertDynamicModule(target, proxyClass.getClassLoader(), proxyClass); - } - - static void runForRunnable() { - System.out.println("runForRunnable"); - } - - public static void assertDynamicModule(Module m, ClassLoader ld, Class proxyClass) { - if (!m.isNamed() || !m.getName().startsWith("jdk.proxy")) { - throw new RuntimeException(m.getName() + " not dynamic module"); - } - - if (ld != m.getClassLoader() || proxyClass.getClassLoader() != ld) { - throw new RuntimeException("unexpected class loader"); - } - - try { - Constructor cons = proxyClass.getConstructor(InvocationHandler.class); - cons.newInstance(handler); - throw new RuntimeException("Expected IllegalAccessException: " + proxyClass); - } catch (IllegalAccessException e) { - // expected - } catch (NoSuchMethodException|InstantiationException|InvocationTargetException e) { - throw new RuntimeException(e); - } - } - private final static InvocationHandler handler = - (proxy, m, params) -> { throw new RuntimeException(m.toString()); }; -} diff --git a/test/jdk/java/lang/reflect/Proxy/ProxyModuleMapping.java b/test/jdk/java/lang/reflect/Proxy/ProxyModuleMapping.java index 4e263d000a4..f75abf02946 100644 --- a/test/jdk/java/lang/reflect/Proxy/ProxyModuleMapping.java +++ b/test/jdk/java/lang/reflect/Proxy/ProxyModuleMapping.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2023, 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 @@ -28,7 +28,7 @@ import java.lang.reflect.Proxy; /* * @test * @summary Basic test of proxy module mapping and the access to Proxy class - * @modules java.base/sun.invoke + * @modules java.base/jdk.internal.misc */ public class ProxyModuleMapping { @@ -37,8 +37,8 @@ public class ProxyModuleMapping { Module unnamed = ld.getUnnamedModule(); new ProxyModuleMapping(Runnable.class).test(); - // unnamed module gets access to sun.invoke package (e.g. via --add-exports) - new ProxyModuleMapping(sun.invoke.WrapperInstance.class).test(); + // unnamed module gets access to jdk.internal.misc package (e.g. via --add-exports) + new ProxyModuleMapping(jdk.internal.misc.VM.BufferPool.class).test(); Class modulePrivateIntf = Class.forName("sun.net.PlatformSocketImpl"); new ProxyModuleMapping(modulePrivateIntf).test();