MethodHandles.Lookup and modules

John Rose john.r.rose at oracle.com
Thu Dec 17 23:37:52 UTC 2015


On Dec 14, 2015, at 7:02 AM, Alan Bateman <Alan.Bateman at oracle.com> wrote:
> ...
> In graph terms then the vertices in the readability graph are the modules. A directed edge from module M to module M2 means that M reads M2. The graph is mutable in that edges can be added via the API at runtime. Code in module M can add a directed edge so that M reads M2. Code in M2 might add a read edge in the other direction. Code in a module cannot change other modules: M cannot change M2 so that M2 reads M or M3 or any other module. For completeness then I should say that edges are only removed when the the vertices (modules) they connect are GC'ed.

This graph of the "M1 READS M2" relation is a handy visualization.
How does it interact with exports, and the unconditional/qualified distinction?
Let me try to figure it out…

I guess the READS edges would be labeled by the types that flow across them.
Let's say TYPES_VIA(M1 READS M2) = {m2pkg.P, m2pkg.Q, …} is the set of
types (all public) from M2 that M1 can read.

And, I guess that TYPES_VIA(M1 READS M2) contains exactly
M2's unconditional (non-qualified) exports, plus M2's qualified
(non-unconditional) exports to M1.

Which is not to say that code in M1 actually uses all those types,
but it could.  And it can't use anything else from M2.

Yes?  (TIA)

>> A. Agent with full-power lookup wants to invoke another agent with the lookup,
>> but wants to limit access, because he doesn't fully trust the other agent.
>> He does a single L.in(A) to a remote-enough type A, creating a non-full-power lookup.
>> (Note:  Picking A is sometimes non-trivial.  This might be an API flaw.)
>> 
>> B. …
> 
> Case A is where our current approach might be too limited. This may be tied into the discussion point as to how to choose A. If A is in the same module as LC then it's as before. However if code in named module M creates a full-power lookup and chooses A in another module M2 then the resulting L.in(A) has zero access.
> 
> It wouldn't be hard to change this to allow PUBLIC be preserved so that L.in(A) would at least allow access to public types in packages that are exported unconditionally by the modules that m(A) reads. Would that increase usefulness? Would such cases be cases where PL is equally useful?

Hmm…  What PL does is ignore the M1 READS M2 graph, or (equivalently)
adds temporary edges as needed to access unconditionally exported names.

(Anticipating a reply to Alex's message of today…)  Possibly, a PUBLIC-type
lookup QL would only be able to see types readable by M1=MODULE(QL.LC).
That is, QL (possibly) would respect the READS graph as it exists, and not
attempt to extend it on the fly like PL (publicLookup) does.  Points about this:

1. Such a lookup QL is stronger than PL in that it might see qualified exports
readable by M1.  (This would be Alex's QUALIFIED mode, which I like.)
But QL would also be weaker than PL, in that it would respect the existing
READS edges, and not create temporary edges like PL does.  (Am I right
that this is a real distinction?  I'm coming up to speed here!)

2. The restriction on QL is interesting in theory but maybe not in practice.
After all, if a nosy browser of modules wants to see an unconditional export
from M2, he doesn't need to bother with a QL in M1; he just uses PL.

3. How much do we all care that modules are completely leaky in their
unconditional (unqualified) exports?  It's really helpful that a module
can hide its unexported types (and seal them up for optimization, etc.).
It's also helpful that modules can use qualified exports to be friends.
Do we (can we) discourage Java apps from browsing modules
for their unconditional exports?  I'm guessing "no (and no)" is the answer.
If that's true then there is little reason to limit any QL's ability to be
a superset of PL.

4. OTOH, suppose a user is trying to be a good citizen of Module World,
and wants to avoid accessing unqualified exports in the absence of a
previously declared READS edge.  It that a real use case?  If so, then
having a QL that throws an error if a READS edge is missing will help
debug the READS graph, instead of sweeping errors under the rug.
From this point of view, usage of PL would be slightly bug-prone, since
PL ignores the carefully constructed READS graph.

Bottom line:  If the READS graph provides useful restrictions, even
for unconditional (unqualified) exports, then it would be useful to
have a QL (not stronger than PL) which emulates a lookup from
some M1, including the *inability* to read any old export..

Bottom line #2:  I like Alex's suggestion of having a QL which
mimics the lookup "perspective" of public types visible to some M1,
but external to M1.  (This QL would be not weaker than PL, as well
as not stronger.)  I was kind of fishing for that extra "mode bit",
and I think we caught something.

Bottom line #3:  If we believe that QL should be incommensurable
with PL, it follows that PL has unique capabilities which should be
used with care.  (Not for security, so much as for bug hygiene.)

Bottom line #4:  I think we want an API point Lookup.restrictModes(int)
that can be used to clear unwanted bits from Lookup.lookupModes().
The Lookup.in API point does this also, but less directly than restrictModes.
Given the new degrees of freedom, it is good to provide the direct API
point, rather than forcing the user to find A (for L.in(A)) when A might
not exist or be easily discoverable.

Possible code for restrictModes:

        public Lookup restrictModes(int mask) {
            int newModes = (lookupModes() & mask);
            validateModes(newModes);
            return new Lookup(lookupClass(), newModes);
        }
        // We don't want to find out what happens with lookups that
        // are private but not public, etc.
        private static void validateModes(int modes) {
            if (modes != normalizeModes(modes))
                throw new IllegalArgumentException("bad mode mask "+Integer.toHexString(modes));
        }
        // Drop bits from modes until it looks like one that arises in nature.
        private static int normalizeModes(int modes) {
            modes &= ALL_MODES;
            if ((modes & PUBLIC) == 0)  return 0;
            if ((modes & MODULE) == 0)  return PUBLIC;
            if ((modes & PACKAGE) == 0)  return (PUBLIC|MODULE);
            …
        }

> Preserving PUBLIC would mean compromising on the guarantee that there be no more access that the original but that is only because m(A) might read modules that m(LC) does not read. It would not give access to non-exported types in m(A). It would also not give access to public type in packages that are conditionally exported to m(A).

Yes, I see.  Well, as stated above, my take is that it may be useful to *not*
compromise the guarantee, and just let PL be an oddity.  I'm increasingly
uncomfortable with allowing PL, with its odd laxity about READS, be an
underlying capability of almost every Lookup.

I really like the monotonicity guarantee:  It makes reasoning about lookup
delegation easier.  Maybe it could be broken safely in the case of PL powers,
but, gee, exceptions like that make security analysis harder, and it's already
hard enough.

— John


More information about the jigsaw-dev mailing list