javareflectionjava-16methodhandlelambda-metafactory

LambdaMetaFactory and Private Methods


I would like to use LambdaMetaFactory to efficiently access a private method.

public class Foo {
  private void bar() {  // here's what I want to invoke
    System.out.println("bar!");
  }
}

I know it is not a security violation, because the following code works:

Foo foo = new Foo();
Method m = Foo.class.getDeclaredMethod("bar");
m.setAccessible(true);
m.invoke(foo);  // output: bar!

However, my attempts to use LambdaMetaFactory fail:

MethodHandles.Lookup lookup = MethodHandles.lookup();
Method m = Foo.class.getDeclaredMethod("bar");
m.setAccessible(true);

CallSite site = LambdaMetafactory.metafactory(lookup, "accept",
        MethodType.methodType(Consumer.class),
        MethodType.methodType(void.class, Object.class),
        lookup.unreflect(m),
        MethodType.methodType(void.class, Foo.class));
Consumer<Foo> func = (Consumer<Foo>) site.getTarget().invoke();
func.accept(foo);  // IllegalAccessException: member is private

Clearly m.setAccessible(true) is not enough here. I tried changing lookup to MethodHandles.privateLookupIn(Foo.class, MethodHandles.lookup()), which did solve it in my toy example ... but not in my actual application, where it generates an IllegalAccessException saying my class "does not have full privilege access". I have been unable to discover why my application "does not have full privilege access", or how to fix it.

The only thing I've found to sort-of-almost work is this:

MethodHandles.Lookup original = MethodHandles.lookup();
Field internal = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
internal.setAccessible(true);
TRUSTED = (MethodHandles.Lookup) internal.get(original);

Which allows me to use TRUSTED in place of lookup, as long as I have --illegal-access=permit in the VM options, which I can do. This produces a NoClassDefFoundError instead (saying it can't find Foo), which seems promising ... but I still can't figure out how to get it to work entirely, just produce this error instead of the other ones.

What is happening here, and how can I access bar through LambdaMetaFactory?


Solution

  • I suppose, you only tried your setAccessible(true) approach in the toy example, rather than the actual application. The differences between the rules of these operations are small in your case.

    Method.setAccessible(boolean)

    This method may be used by a caller in class C to enable access to a member of declaring class D if any of the following hold:

    • C and D are in the same module.
    • The member is public and D is public in a package that the module containing D exports to at least the module containing C.
    • The member is protected static, D is public in a package that the module containing D exports to at least the module containing C, and C is a subclass of D.
    • D is in a package that the module containing D opens to at least the module containing C. All packages in unnamed and open modules are open to all modules and so this method always succeeds when D is in an unnamed or open module.

    MethodHandles.privateLookupIn(…)

    A caller, specified as a Lookup object, in module M1 is allowed to do deep reflection on module M2 and package of the target class if and only if all of the following conditions are true:

    [...]

    • If the caller module M1 differs from the target module M2 then both of the following must be true:
      • M1 reads M2.
      • M2 opens the package containing the target class to at least M1.

    privateLookupIn(…) has stricter rules than setAccessible(true), which is not surprising, as it has a bigger impact than enabling access to a specific single member. Since the member in question is private, the points effectively applying to the operation do not differ. The access is granted if either, the accessor is in the same module or the member’s package has been opened to the caller’s module. (Since you’re accessing the class Foo in the caller code directly, the read edge obviously does already exist.)

    In your toy example, the classes are likely in the same module or not using modules at all (getting placed in the unnamed module at runtime). In contrast, the application code is trying to access another module’s member. Since your approach accessing IMPL_LOOKUP does work by using the option --illegal-access=permit, the caller code must be in the unnamed module whereas the target must be in a different module not opening the member’s package to the unnamed module.

    This implementation specific lookup object is special. It has the trusted flag allowing it to access everything, but its lookup class is java.lang.Object, so it can only see classes visible to the bootstrap loader. You have to change the lookup class using in; unlike ordinary lookup objects, this will not clear the permissions.

    Field internal = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
    internal.setAccessible(true);
    TRUSTED = (MethodHandles.Lookup) internal.get(null);
    
    MethodHandles.Lookup lookup = TRUSTED.in(Foo.class);
    MethodHandle mh = lookup.findSpecial(
        Foo.class, "bar", MethodType.methodType(void.class), Foo.class);
    
    // mh.invokeExact(foo); // could simply invoke it here
    
    CallSite site = LambdaMetafactory.metafactory(lookup, "accept",
            MethodType.methodType(Consumer.class),
            mh.type().erase(), mh, mh.type());
    Consumer<Foo> func = (Consumer<Foo>) site.getTarget().invoke();
    
    func.accept(foo);
    

    But it’s important to emphasize that this hack only works with this specific implementation and is even expected to stop working in a future version. The only clean way to implement the access, is to set up the modules correctly, i.e. add an appropriate opens directive.