MethodHandle 常量是否可以以绕过访问控制的方式使用?

Lai*_*son 2 java methodhandle

我正在使用 JDK 15。(我正在使用 ByteBuddy 1.10.16 生成一些类,但我认为,除了作为背景信息之外,它在这里几乎不相关。)

在这些生成的类之一中,我调用invokeExact()了一个我设法存储在生成的类中的MethodHandle 常量。它是通过 获得的“场地设置者” MethodHandles.Lookup#findSetter

(下面我知道这个MethodHandles.privateLookupIn()方法。)

我注意到,MethodHandle所讨论的“字段设置器”在代表字段时会失败private。在大多数级别上,这并不令我感到惊讶:直接MethodHandle是,嗯,直接:虽然我并不假装对所有这些东西的内部结构了解很多,但在我看来,它肯定只是包装了一些没有的低级字节码访问检查。

但考虑到它的存在privateLookupIn()表明在某些情况下绕过访问检查是可能的,是否有一条路径可以让我MethodHandle从 A 类中“收获”一个可以读取private字段的“字段设置器”,然后将其作为常量存储在另一个字段中B类这样就invokeExact()一定会成功吗?

我相信我过去做过类似的事情(必须检查)涉及private方法,但在这些情况下我没有使用常量MethodHandle 即我在使用结果并将结果存储在字段MethodHandle期间获取类初始化时间,然后调用该字段的内容。如果我必须继续走这条路,我会的,但常数在这里似乎很有吸引力,如果可以的话,使用它们会很好。<clinit>privateLookupIn()MethodHandleprivate static finalinvokeExact()MethodHandle

因此,我的问题的另一种表达方式是:表示 a 的常量形式MethodHandle是否能够存储其特权?或者是否有某种一次性方法可以“提高”作为MethodHandle常量存储的特权?或者,给定值存储为常量这一事实是否MethodHandle会阻止它始终访问除常规可访问的 Java 构造之外的任何内容? 我在相关部分的 JVM 规范中没有看到任何非常明显的内容。

Hol*_*ger 5

您\xe2\x80\x99链接的规范指出:

\n
\n

要解析,请使用以下四个步骤解析MH对字节码行为中的类、接口、字段和方法的所有符号引用:MH

\n

R已解决。当\ 的字节码行为为种类 1、2、3 或 4时,就像通过字段解析 ( \xc2\xa75.4.3.2 )发生这种情况,并且就像通过方法解析 ( \xc2\xa75.4.3.3 )发生这种情况当\ 的字节码行为为种类 5、6、7 或 8 时,并且当\ 的字节码行为为种类 9时,就像通过接口方法解析 ( \xc2\xa75.4.3.4 )一样。MHMHMH

\n
\n

链接的章节,即字段的\xc2\xa75.4.3.2,描述了普通解析过程,包括访问控制。即使没有明确的声明,您也可以从前面的描述中得出访问控制的存在,该描述指出这些符号方法句柄引用应该等效于特定列出的字节码行为。

\n

因此,通过类文件\xe2\x80\x99s常量池的条目获取的直接方法句柄CONSTANT_MethodHandle_info无法访问\xe2\x80\x99也无法通过字节码指令直接访问的类或成员。

\n

但从 JDK\xc2\xa011 开始,您可以使用动态常量来加载由任意引导过程定义的任意类型的常量。因此,当您可以用 Java 代码表达如何获取常量(例如使用 )时privateLookupIn,您还可以将其定义为动态常量的引导,并在原本要加载直接方法句柄的位置加载该常量。

\n

考虑以下起点:

\n
public class DynConstant {\n    private static void inacessibleMethod() {\n        new Exception("inacessibleMethod() called").printStackTrace();\n    }\n\n    public static void main(String[] args) throws Throwable {\n        // express the constant\n        Handle theHandle = new Handle(H_INVOKESTATIC,\n            Type.getInternalName(DynConstant.class), "inacessibleMethod",\n            Type.getMethodDescriptor(Type.VOID_TYPE), false);\n\n        String generatedClassName\n                = DynConstant.class.getPackageName().replace(\'.\', \'/\')+"/Test";\n\n        ClassWriter cw = new ClassWriter(0);\n        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT,\n                generatedClassName, null, "java/lang/Object", null);\n\n        MethodVisitor mv = cw.visitMethod(\n                ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);\n        mv.visitCode();\n        mv.visitLdcInsn(theHandle);\n        mv.visitMethodInsn(INVOKEVIRTUAL,\n                "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);\n        mv.visitInsn(RETURN);\n        mv.visitMaxs(1, 0);\n        mv.visitEnd();\n        cw.visitEnd();\n        byte[] code = cw.toByteArray();\n\n        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {\n            String fName = generatedClassName+".class";\n            try {\n                Path dir = Files.createTempDirectory("javapTmp");\n                Path classFile = dir.resolve(fName);\n                Files.createDirectories(classFile.getParent());\n                Files.write(classFile, code);\n                javap.run(System.out, System.err, "-c", "-cp",\n                    dir.toAbsolutePath().toString(), generatedClassName);\n                for(Path p = classFile;;p=p.getParent()) {\n                    Files.delete(p);\n                    if(p.equals(dir)) break;\n                }\n            } catch (IOException ex) {\n                throw new UncheckedIOException(ex);\n            }\n        }, () -> System.out.println("javap not found in current environment"));\n\n        try {\n            MethodHandles.Lookup lookup = MethodHandles.lookup();\n            lookup.findStatic(lookup.defineClass(code),\n                "test", MethodType.methodType(void.class)).invokeExact();\n        }\n        catch(Throwable t) {\n            t.printStackTrace();\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

它尝试定义一个新的运行时类,该类尝试MethodHandle通过. 程序打印出inacessibleMethod()CONSTANT_MethodHandle_info

\n
interface instexamples.Test {\n  public static void test();\n    Code:\n       0: ldc           #12                 // MethodHandle REF_invokeStatic instexamples/DynConstant.inacessibleMethod:()V\n       2: invokevirtual #17                 // Method java/lang/invoke/MethodHandle.invokeExact:()V\n       5: return\n}\njava.lang.IllegalAccessError: class instexamples.Test tried to access private method \'void instexamples.DynConstant.inacessibleMethod()\' (instexamples.Test and instexamples.DynConstant are in unnamed module of loader \'app\')\n    at instexamples.Test.test(Unknown Source)\n    at instexamples.DynConstant.main(DynConstant.java:100)\n
Run Code Online (Sandbox Code Playgroud)\n

现在,让\xe2\x80\x99s 将常量更改为动态常量,该常量将执行相当于

\n
MethodHandles.Lookup l = MethodHandles.lookup();\nl = MethodHandles.privateLookupIn(DynConstant.class, l);\nMethodHandle mh = l.findStatic(\n        DynConstant.class, "inacessibleMethod", MethodType.methodType(void.class));\n
Run Code Online (Sandbox Code Playgroud)\n

当常数第一次被解析时。常量的定义是 \xe2\x80\x9ca 位\xe2\x80\x9d 涉及的比较多。由于代码包含三个方法调用,因此定义需要三个方法句柄,此外,还需要另一个现有引导方法的句柄ConstantBootstraps.invoke(\xe2\x80\xa6),该方法允许使用任意方法调用进行引导。这些句柄可用于定义动态常量,而动态常量允许作为另一个动态常量的常量输入。

\n

// express the constant因此我们将注释后的定义替换为:

\n
Type string = Type.getType(String.class), clazz = Type.getType(Class.class);\nType oArray = Type.getType(Object[].class), object = oArray.getElementType();\nType mhLookup = Type.getType(MethodHandles.Lookup.class);\nType mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);\nType targetType = Type.getType(DynConstant.class);\n\nString methodHandles = Type.getInternalName(MethodHandles.class);\n\nHandle methodHandlesLookup = new Handle(H_INVOKESTATIC, methodHandles,\n    "lookup", Type.getMethodDescriptor(mhLookup), false);\nHandle privateLookupIn = new Handle(H_INVOKESTATIC, methodHandles,\n    "privateLookupIn", Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);\nHandle findStatic = new Handle(H_INVOKEVIRTUAL, mhLookup.getInternalName(),\n    "findStatic", Type.getMethodDescriptor(mHandle, clazz, string, mType), false);\nHandle invoke = new Handle(H_INVOKESTATIC,\n    Type.getInternalName(ConstantBootstraps.class), "invoke",\n    Type.getMethodDescriptor(object, mhLookup, string, clazz, mHandle, oArray), false);\n\nConstantDynamic methodHandlesLookupC = new ConstantDynamic("lookup",\n    mhLookup.getDescriptor(), invoke, methodHandlesLookup);\nConstantDynamic privateLookupInC = new ConstantDynamic("privateLookupIn",\n    mhLookup.getDescriptor(), invoke, privateLookupIn, targetType, methodHandlesLookupC);\nConstantDynamic theHandle = new ConstantDynamic("findStatic",\n    mHandle.getDescriptor(), invoke, findStatic,\n    privateLookupInC, targetType, "inacessibleMethod", Type.getMethodType("()V"));\n
Run Code Online (Sandbox Code Playgroud)\n

为了避免重复非常长的常量方法描述符字符串,我使用 ASM\xe2\x80\x99sType抽象。原则上,我们可以对所有类型名称和签名使用常量字符串。

\n

该程序打印:

\n
interface instexamples.Test {\n  public static void test();\n    Code:\n       0: ldc           #45                 // Dynamic #2:findStatic:Ljava/lang/invoke/MethodHandle;\n       2: invokevirtual #50                 // Method java/lang/invoke/MethodHandle.invokeExact:()V\n       5: return\n}\njava.lang.Exception: inacessibleMethod() called\n    at instexamples.DynConstant.inacessibleMethod(DynConstant.java:23)\n    at instexamples.Test.test(Unknown Source)\n    at instexamples.DynConstant.main(DynConstant.java:89)\n
Run Code Online (Sandbox Code Playgroud)\n

由方法调用创建的三个常量组成的动态常量的复杂性将导致相当大的常量池。尽管我们有一个附加方法,但我们可以生成一个自定义引导方法,并获得一个小得多的类文件:

\n
public class DynConstant {\n    private static void inacessibleMethod() {\n        new Exception("inacessibleMethod() called").printStackTrace();\n    }\n\n    public static void main(String[] args) throws Throwable {\n        Type string = Type.getType(String.class), clazz = Type.getType(Class.class);\n        Type mhLookup = Type.getType(MethodHandles.Lookup.class);\n        Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);\n\n        Type targetType = Type.getType(DynConstant.class);\n\n        String myBootstrapName = "privateLookup";\n        String myBootstrapDesc = Type.getMethodDescriptor(mHandle, mhLookup, string, clazz, clazz, mType);\n\n        String generatedClassName = DynConstant.class.getPackageName().replace(\'.\', \'/\')+"/Test";\n\n        Handle myBootStrap = new Handle(H_INVOKESTATIC, generatedClassName,\n            myBootstrapName, myBootstrapDesc, true);\n        ConstantDynamic theHandle = new ConstantDynamic("inacessibleMethod",\n            mHandle.getDescriptor(), myBootStrap, targetType, Type.getMethodType("()V"));\n\n        ClassWriter cw = new ClassWriter(0);\n        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT, generatedClassName, null, "java/lang/Object", null);\n\n        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);\n        mv.visitCode();\n        mv.visitLdcInsn(theHandle);\n        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);\n        mv.visitInsn(RETURN);\n        mv.visitMaxs(1, 0);\n        mv.visitEnd();\n        mv = cw.visitMethod(ACC_PRIVATE|ACC_STATIC, myBootstrapName, myBootstrapDesc, null, null);\n        mv.visitCode();\n        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class\n        mv.visitVarInsn(ALOAD, 0); // MethodHandles.lookup() generated as JVM arg\n        mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "privateLookupIn",\n            Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);\n        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class\n        mv.visitVarInsn(ALOAD, 1); // invoked name, i.e. "inacessibleMethod"\n        mv.visitVarInsn(ALOAD, 4); // bootstrap argument, i.e. MethodType ()V\n        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",\n            Type.getMethodDescriptor(mHandle, clazz, string, mType), false);\n        mv.visitInsn(ARETURN);\n        mv.visitMaxs(4, 5);\n        mv.visitEnd();\n        cw.visitEnd();\n        byte[] code = cw.toByteArray();\n\n        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {\n            String fName = generatedClassName+".class";\n            try {\n                Path dir = Files.createTempDirectory("javapTmp");\n                Path classFile = dir.resolve(fName);\n                Files.createDirectories(classFile.getParent());\n                Files.write(classFile, code);\n                javap.run(System.out, System.err, "-p", "-c", "-cp",\n                    dir.toAbsolutePath().toString(), generatedClassName);\n                for(Path p = classFile;;p=p.getParent()) {\n                    Files.delete(p);\n                    if(p.equals(dir)) break;\n                }\n            } catch (IOException ex) {\n                throw new UncheckedIOException(ex);\n            }\n        }, () -> System.out.println("javap not found in current environment"));\n\n        try {\n            MethodHandles.Lookup lookup = MethodHandles.lookup();\n            lookup.findStatic(lookup.defineClass(code),\n                "test", MethodType.methodType(void.class)).invokeExact();\n        }\n        catch(Throwable t) {\n            t.printStackTrace();\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
interface instexamples.custombootstrap.Test {\n  public static void test();\n    Code:\n       0: ldc           #18                 // Dynamic #0:inacessibleMethod:Ljava/lang/invoke/MethodHandle;\n       2: invokevirtual #23                 // Method java/lang/invoke/MethodHandle.invokeExact:()V\n       5: return\n\n  private static java.lang.invoke.MethodHandle privateLookup(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.Class, java.lang.Class, java.lang.invoke.MethodType);\n    Code:\n       0: aload_3\n       1: aload_0\n       2: invokestatic  #29                 // Method java/lang/invoke/MethodHandles.privateLookupIn:(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup;\n       5: aload_3\n       6: aload_1\n       7: aload         4\n       9: invokevirtual #35                 // Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;\n      12: areturn\n}\njava.lang.Exception: inacessibleMethod() called\n    at instexamples.custombootstrap.DynConstant.inacessibleMethod(DynConstant.java:22)\n    at instexamples.custombootstrap.Test.test(Unknown Source)\n    at instexamples.custombootstrap.DynConstant.main(DynConstant.java:91)\n
Run Code Online (Sandbox Code Playgroud)\n

引导方法被设计为可重用。它接收所有必要的信息作为常量参数,因此不同的ldc指令可以使用它来获取不同成员的句柄。JVM 确实已经将调用者\xe2\x80\x99s 查找上下文作为第一个参数传递,因此我们可以使用它,并且不需要调用MethodHandles.lookup(). 要搜索成员的类是第一个附加参数,它用作 和 的第一个privateLookupIn参数findStatic。由于每个动态常量都有一个标准名称参数,因此我们可以使用它来表示成员\xe2\x80\x99s 名称。最后一个参数表示MethodType要查找的方法。当我们针对字段查找进行改进时,我们可以删除该参数,作为第三个标准参数,预期的常量类型可以与预期的 field\xe2\x80\x99s 类型匹配。

\n

基本上,自定义引导方法会执行privateLookupIn您在问题中描述的基于查找,但使用它ldc可以进行延迟初始化(而不是字段的类初始化时间),同时在链接指令后static final仍然像字段一样进行优化。static final此外,这些动态常量还可以作为其他动态常量或invokedynamic指令的其他引导方法的常量输入(不过,您也可以使用此引导方法static final将现有字段调整为动态常量)。

\n