MethodHandles.Lookup and modules

John Rose john.r.rose at oracle.com
Thu Dec 10 23:08:49 UTC 2015


On Dec 10, 2015, at 11:20 AM, stanislav lukyanov <stanislav.lukyanov at oracle.com> wrote:
> 
> Alan,
> thanks for the comments!
> Please see below.
> 
> On 10.12.2015 1:16, Alan Bateman wrote:
>> 
>> On 09/12/2015 14:54, stanislav lukyanov wrote:
>>> 
>>> 1) Lookup.in() javadoc says
>>> "If the lookup for this Lookup is in a named module, and the new lookup class is in a different
>>> module M, then no members, not even public members in M's exported packages, will be accessible"
>>> However, it seems to be possible to have PUBLIC access after
>>> lookupInA.in(ClassInB.class)
>>> without breaking "no more access capabilities than the original" rule.
>>> 
>>> Are such transitions intentionally forbidden?
>> Yes, this is intention as the lookup class and mode bits would otherwise not be sufficient when you teleport through a sequence of lookup classes. It might be clearer if you extend the example to in(B.class).in(C.class).in(D.class) where B, C and D are in different named modules and work out the intersection.
> This is connected to the second part of the question (2).
> If access to readable modules were allowed only if MODULE bit is set,
> transition from A to B would be safe, since only one transition could be made (actually, same as it is now, but not from unnamed module only).
> BTW, 'requires public' chain could be traversed safely even with PUBLIC mode:
> A.in(B).in(C).in(D) would have access to D's exported members and D's 'requires public' - same as A.
> 
> WDYT?

This stuff makes my head hurt, but I'm fine with any semantics that preserves the following:

1. full power: MethodHandles.lookup() has the same privileges as (non-constructor) bytecode in the caller class

2. downward monotonic
2a. L.in(A) never has more privileges than L (for all L, A)
2b. L.in(A) never has more privileges than a full power lookup on A (for all L, A)
2c. as a corollary, a chain L.in(A).in(B).in(C)… has no more privileges than L or any lookups in A, B, C, …

3. graceful degradation:  L.in(A) loses only privileges broken by the distance between LC=L.lookupClass and A
3a. if A==LC no privileges are lost; the identical L can be the result
3b. if A and LC are nestmates, only protected privileges may be lost (dynamic emulation of JLS nestmate access)
3c. if A and LC are in the same package, only private privileges may be lost
3d. if A and LC are in the same module, only package privileges may be lost

4. downward convergence (to publicLookup or empty privileges)
4a. if A is inaccessible to LC=L.lookupClass, L.in(A) has no privileges (less than publicLookup)
4b. if A is accessible to LC and L has non-empty privileges, L.in(A) is no less privileged than publicLookup
4c. for any L with non-empty privileges, there is a sequence of types A,B where L.in(A).in(B) is equivalent to publicLookup

5. publicLookup has a reasonable minimal set of globally acceptable privileges
5a. this set of privileges is singular, and does not depend on the lookupClass
5b. the only possible results of publicLookup.in(A) are no change, and falling to empty privileges

Access can only be measured against the current state of the module graph,
which means that certain results can vary over time, in a consistent way
across the whole system.

The above are my preferences; I can imaging tweaking some of those things
in order to make the API fit more gracefully into modules.  I rely on Alan & co.
to figure that out!


>>> 2) Lookup.in() javadoc says
>>> "If the lookup class for this Lookup is not in a named module, and the new lookup class is
>>> in a named module M, then no members in M's non-exported packages will be accessible"
>>> 
>>> Spec says nothing about M's 'requires' and 'exports to M'.
>>> In current implementation, modules required by M are still accessible
>>> and packages exported to M (with 'exports to')  are not accessible.
>>> 
>>> It seems right that packages exported to M are not accessible.
>>> Should it be specified in the javadoc, or is it implied due to definitions somewhere else?
>> It is implied because the javadoc provides the guarantee that the resulting Lookup has no more access than the original. In this case there are packages exported by M's friends to M that are not accessible to code in unnamed modules. However, I think you are right that this could be make clearer in the javadoc. I'm sure that once the design settles down that we'll do several passes over the javadoc.
> OK, thanks!
> Is there a JBS RFE that aggregates such issues, or anything like that?

(BTW, see https://bugs.openjdk.java.net/browse/JDK-8145070 )

>> 
>>> :
>>> 
>>> However, this creates some uncertainty.
>>> Suppose we have A requires B. From user's point resulting lookup of
>>> publicLookup().in(ClassInA.class)
>>> may or may not have access to the module B - it depends on module A's 'requires' which user doesn't know about.
>>> 
>>> It may come to the following situation. Code
>>> publicLookup().in(BlassInA.class).findStatic(ClassInB.class, "m", ...).invoke()
>>> works with version of A that have 'requires B'.
>>> In the next version, A removes 'requires B' which should not
>>> have any impact on A's users, but the code above stops working.
>> Sure, the other thing is that readability graph can mutate at run-time so that `A` reads additional modules.
> Method handles API is really "conservative" in regard of access control and I believe it supposed to be nearly as safe
> as plain method calls (since Lookup basically reproduces bytecode-level checks).

Yes.  The above "axioms" are attempting to make such an API.

> I think it shouldn't allow something just because reflection API is able to do the trick anyway.

I agree with this.  Lookups are primarily about emulating bytecode behavior (not reflection behavior), and providing a framework for safe and sane delegation.

> After all, it doesn't go easy with method access checks despite we have Method.setAccessible

It does, actually, for the Lookup.unreflect* methods:
         * If the method's {@code accessible} flag is not set,
         * access checking is performed immediately on behalf of the lookup class.

But, the existence of that "bridge" to reflection semantics is not supposed create other
vectors for privilege escalation in lookups.

>>> 
>>> 
>>> 3) publicLookup() seems to allow work around the module access control.
>>> publicLookup() is available for any module and has access to any module's exported packages.
>>> So, if we have modules A and B that do not require each other, we can still access B from A:
>>> // code in A
>>> publicLookup().in(ClassInB.class)
>>> 
>>> Is it OK to be able to do such tricks?
>> 
>> 
>> The publicLookup can only be used to access types that are are public and in packages that are exported unconditionally. It can't be used to break encapsulation, either directly or by teleporting and using a lookup class in another module.
>> 
>> Also `A` can call addReads to read any other module so this allows it to access any public type in any package exported by other modules if it really wants. This means that there is nothing that the publicLookup can be used to access that `A` can't access anyway. 
> I understand that the exported packages could be accessed from outside anyway, so privacy of B is not violated.
> But isn't dependency mechanism supposed to prevent A from using B unless A have deliberately declared the dependency (via module-info or addReads)?
> 
> One more thing about javadoc.
> publicLookup() spec says
> - "As a matter of pure convention, the lookup class of this lookup object will be in an unnamed module."
> It's not about pure convention anymore - it really matters now where publicLookup() resides.
> - "Discussion: The lookup class can be changed to any other class C using an expression of the form publicLookup().in(C.class)."
> publicLookup().in(A.class) doesn't necessarily preserve access capabilities anymore, so this text is not true now.

Yes; this is really a query against axiom #5.

Does publicLookup.in(A) create a new set of privileges depending on A?

I think that is the current design; maybe it's best, but it's a little odd.
If there really is a uniquely minimal set of public privileges,
then that should be publicLookup, and PL.in(A) should not affect
that minimal set.  That's the intention of saying the LC of PL is
mere convention.

We can modify axiom #5 to say:  PL accesses a unique family of
privileges sets, indexed by the LC of each PL.  This family is
minimal in the sense that, for any PL in the family, and any
lesser privileged L, L is either another member of the PL family,
or has empty privileges.

In this case, the LC of a PL provides a scoped or restricted
view of "really public" names, all of which are non-problematic
to view from any point in the system.  The fact that you have
to dial in a special LC to get to one of those names is merely
a formality.  No abstraction can ever be broken via a PL,
even if the various PLs have slightly different capabilities.

(N.B.  Because of 4a, PL.in(A) can go empty, if PL.LC cannot
access A.)

— John


More information about the jigsaw-dev mailing list