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

Can a MethodHandle constant be used in such a way as to bypass access control?

我正在使用 JDK 15。(我正在使用 ByteBuddy 1.10.16 生成一些 classes 但我认为它与这里大部分无关,除了作为背景信息。)

在这些生成的 classes 之一中,我在 MethodHandle constant 上调用 invokeExact() 我已经设法存储在生成的class。它是通过 MethodHandles.Lookup#findSetter.

获取的“字段 setter”

(在下文中我知道 MethodHandles.privateLookupIn() 方法。)

我注意到问题中的“字段 setter”MethodHandle 在表示 private 字段时失败。在大多数层面上,这并不让我感到惊讶:直接的 MethodHandle 是,好吧,直接的:虽然我不假装对所有这些东西的内部了解很多,但在我看来,它肯定只是包装了一些东西没有访问检查的低级字节码。

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

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

所以另一种表达我的问题的方式是:表示 MethodHandle 的常量形式是否能够存储其特权?或者是否有一些一次性的方式来“提升”给定存储为常量的 MethodHandle 的特权?或者,给定的 MethodHandle 被存储为常量这一事实是否会阻止它始终访问除常规可访问的 Java 构造之外的任何内容? I didn't see anything super obvious in the JVM specification in the relevant section.

specification you’ve linked 状态:

To resolve MH, all symbolic references to classes, interfaces, fields, and methods in MH's bytecode behavior are resolved, using the following four steps:

R is resolved. This occurs as if by field resolution (§5.4.3.2) when MH's bytecode behavior is kind 1, 2, 3, or 4, and as if by method resolution (§5.4.3.3) when MH's bytecode behavior is kind 5, 6, 7, or 8, and as if by interface method resolution (§5.4.3.4) when MH's bytecode behavior is kind 9.

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

因此,通过 class 文件常量池的 CONSTANT_MethodHandle_info 条目获取的直接方法句柄无法访问 classes 或不能通过字节码直接访问的成员说明。

但是自 JDK 11 起,您可以使用 Dynamic Constants 加载由任意 bootstrapping 进程定义的任意类型的常量。所以当你可以用 Java 代码表达如何获取常量时,比如 privateLookupIn 的使用,你也可以将其定义为动态常量的 bootstrapping 并加载该常量在您将加载直接方法句柄的地方。

考虑以下起点:

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        // express the constant
        Handle theHandle = new Handle(H_INVOKESTATIC,
            Type.getInternalName(DynConstant.class), "inacessibleMethod",
            Type.getMethodDescriptor(Type.VOID_TYPE), false);

        String generatedClassName
                = DynConstant.class.getPackageName().replace('.', '/')+"/Test";

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT,
                generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(
                ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL,
                "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName+".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for(Path p = classFile;;p=p.getParent()) {
                    Files.delete(p);
                    if(p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        }
        catch(Throwable t) {
            t.printStackTrace();
        }
    }
}

它试图定义一个新的运行时 class,试图通过 CONSTANT_MethodHandle_info 加载指向 inacessibleMethod()MethodHandle 常量。程序打印

interface instexamples.Test {
  public static void test();
    Code:
       0: ldc           #12                 // MethodHandle REF_invokeStatic instexamples/DynConstant.inacessibleMethod:()V
       2: invokevirtual #17                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
       5: return
}
java.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')
    at instexamples.Test.test(Unknown Source)
    at instexamples.DynConstant.main(DynConstant.java:100)

现在,让我们将常量更改为动态常量,其执行等同于

MethodHandles.Lookup l = MethodHandles.lookup();
l = MethodHandles.privateLookupIn(DynConstant.class, l);
MethodHandle mh = l.findStatic(
        DynConstant.class, "inacessibleMethod", MethodType.methodType(void.class));

第一次解析常量时。常量的定义“有点”复杂。由于代码包含三个方法调用,因此定义需要三个方法句柄,此外,另一个句柄指向已存在的 bootstrap 方法 ConstantBootstraps.invoke(…),允许对 bootstrapping 使用任意方法调用。这些句柄可用于定义动态常量,而动态常量允许作为另一个动态常量的常量输入。

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

Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
Type oArray = Type.getType(Object[].class), object = oArray.getElementType();
Type mhLookup = Type.getType(MethodHandles.Lookup.class);
Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);
Type targetType = Type.getType(DynConstant.class);

String methodHandles = Type.getInternalName(MethodHandles.class);

Handle methodHandlesLookup = new Handle(H_INVOKESTATIC, methodHandles,
    "lookup", Type.getMethodDescriptor(mhLookup), false);
Handle privateLookupIn = new Handle(H_INVOKESTATIC, methodHandles,
    "privateLookupIn", Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
Handle findStatic = new Handle(H_INVOKEVIRTUAL, mhLookup.getInternalName(),
    "findStatic", Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
Handle invoke = new Handle(H_INVOKESTATIC,
    Type.getInternalName(ConstantBootstraps.class), "invoke",
    Type.getMethodDescriptor(object, mhLookup, string, clazz, mHandle, oArray), false);

ConstantDynamic methodHandlesLookupC = new ConstantDynamic("lookup",
    mhLookup.getDescriptor(), invoke, methodHandlesLookup);
ConstantDynamic privateLookupInC = new ConstantDynamic("privateLookupIn",
    mhLookup.getDescriptor(), invoke, privateLookupIn, targetType, methodHandlesLookupC);
ConstantDynamic theHandle = new ConstantDynamic("findStatic",
    mHandle.getDescriptor(), invoke, findStatic,
    privateLookupInC, targetType, "inacessibleMethod", Type.getMethodType("()V"));

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

这个程序打印:

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

由方法调用创建的三个常量组成的动态常量的复杂性会导致相当大的常量池。我们可能会生成一个自定义 bootstrap 方法,并得到一个明显更小的 class 文件,尽管我们有一个额外的方法:

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
        Type mhLookup = Type.getType(MethodHandles.Lookup.class);
        Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);

        Type targetType = Type.getType(DynConstant.class);

        String myBootstrapName = "privateLookup";
        String myBootstrapDesc = Type.getMethodDescriptor(mHandle, mhLookup, string, clazz, clazz, mType);

        String generatedClassName = DynConstant.class.getPackageName().replace('.', '/')+"/Test";

        Handle myBootStrap = new Handle(H_INVOKESTATIC, generatedClassName,
            myBootstrapName, myBootstrapDesc, true);
        ConstantDynamic theHandle = new ConstantDynamic("inacessibleMethod",
            mHandle.getDescriptor(), myBootStrap, targetType, Type.getMethodType("()V"));

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT, generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        mv = cw.visitMethod(ACC_PRIVATE|ACC_STATIC, myBootstrapName, myBootstrapDesc, null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 0); // MethodHandles.lookup() generated as JVM arg
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "privateLookupIn",
            Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 1); // invoked name, i.e. "inacessibleMethod"
        mv.visitVarInsn(ALOAD, 4); // bootstrap argument, i.e. MethodType ()V
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",
            Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(4, 5);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName+".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-p", "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for(Path p = classFile;;p=p.getParent()) {
                    Files.delete(p);
                    if(p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        }
        catch(Throwable t) {
            t.printStackTrace();
        }
    }
}
interface instexamples.custombootstrap.Test {
  public static void test();
    Code:
       0: ldc           #18                 // Dynamic #0:inacessibleMethod:Ljava/lang/invoke/MethodHandle;
       2: invokevirtual #23                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
       5: return

  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);
    Code:
       0: aload_3
       1: aload_0
       2: invokestatic  #29                 // Method java/lang/invoke/MethodHandles.privateLookupIn:(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup;
       5: aload_3
       6: aload_1
       7: aload         4
       9: invokevirtual #35                 // Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
      12: areturn
}
java.lang.Exception: inacessibleMethod() called
    at instexamples.custombootstrap.DynConstant.inacessibleMethod(DynConstant.java:22)
    at instexamples.custombootstrap.Test.test(Unknown Source)
    at instexamples.custombootstrap.DynConstant.main(DynConstant.java:91)

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

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