Discussion:
MethodHandles.Lookup and modules
(too old to reply)
stanislav lukyanov
2015-12-09 14:54:46 UTC
Permalink
Hi Alan and Jigsaw Team,

I have questions regarding Jigsaw-related updates in MethodHandles.Lookup.

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?


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?

I have some doubts about M's 'requires' (non-public).
Technically, allowing access to non-publicly required modules for PUBLIC
lookup looks correct because
- the only way to have only PUBLIC access to lookup module is to
teleport to a named module from an unnamed
- unnamed module has access to all named modules anyway
so the requirement of "having no more access" after Lookup.in() is
fulfilled.
(BTW, if Lookup.in() were allowed then the reasoning would break. But it
depends on the answer on (1))

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.


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?

Thanks,
Stas
Alan Bateman
2015-12-09 22:16:44 UTC
Permalink
Post by stanislav lukyanov
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.
Post by stanislav lukyanov
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.
Post by stanislav lukyanov
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.
Post by stanislav lukyanov
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
// 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.

-Alan
stanislav lukyanov
2015-12-10 19:20:20 UTC
Permalink
Alan,
thanks for the comments!
Please see below.
Post by Alan Bateman
Post by stanislav lukyanov
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?
Post by Alan Bateman
Post by stanislav lukyanov
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?
Post by Alan Bateman
Post by stanislav lukyanov
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).
I think it shouldn't allow something just because reflection API is able
to do the trick anyway.
After all, it doesn't go easy with method access checks despite we have
Method.setAccessible
Post by Alan Bateman
Post by stanislav lukyanov
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
// 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.


Thanks,
Stas
John Rose
2015-12-10 23:08:49 UTC
Permalink
Post by stanislav lukyanov
Alan,
thanks for the comments!
Please see below.
Post by stanislav lukyanov
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).
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!
Post by stanislav lukyanov
Post by stanislav lukyanov
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 )
Post by stanislav lukyanov
Post by stanislav lukyanov
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.
Post by stanislav lukyanov
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.
Post by stanislav lukyanov
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.
Post by stanislav lukyanov
Post by stanislav lukyanov
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.
// 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
Alan Bateman
2015-12-11 14:02:52 UTC
Permalink
John,

Thanks for jumping in on this and the guidance. Comments inline.
Post by John Rose
This stuff makes my head hurt, but I'm fine with any semantics that
1. full power: MethodHandles.lookup() has the same privileges as
(non-constructor) bytecode in the caller class
Preserved, no changes.
Post by John Rose
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, …
Preserved with the exception of A, B and C in the same named module M
and where the set modules that M reads increases (say where code in M
reads additional modules).
Post by John Rose
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
Preserved but perhaps with the (initially surprising) consequence that
all access is lost when m(LC) is a named module and m(A) is a different
module. This arises because the two modules may read very different sets
of modules, the intersection cannot be expressed via a lookup class + modes.
Post by John Rose
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
Downward convergence to zero access/empty privileges.

No downward convergence to publicLookup because
m(publicLookup.lookupClass) must read all modules, thus a superset of
the modules that named modules will read. I should say of course that
the publicLookup can only be used to create method handles to public
members in packages that are exported unconditionally. So nothing that
code in a named module couldn't otherwise access when it increases
readability.
Post by John Rose
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
publicLookup is minimally trusted and so can only create method handles
where the target type is public and exported (unconditionally).

For 5b then publicLookup.in(A) may result in no change or degrade but
not to empty privileges in one hop. The "no change" case is where A is
in an unnamed module (think class path). The "degrade" case is where A
is a named module and so the resulting lookup can only be used to access
the public types that are exported unconditionally by modules that m(A)
reads.
Post by John Rose
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.
Yes, consistent with bytecode and also the access checks in core reflection.
Post by John Rose
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!
and we rely on you to ensure that what we do make sense. I hope this
does not constitute a circular dependency :-)
Post by John Rose
(BTW, see https://bugs.openjdk.java.net/browse/JDK-8145070 )
The defining loader of publicLookup.lookupClass() is no longer null and
looks like Nashorn trips up on the permission check in
Class::getClassLoader.
Post by John Rose
I agree with this. Lookups are primarily about emulating bytecode
behavior (not reflection behavior), and providing a framework for safe
and sane delegation.
Adding edges to the readability graph impacts all access checks
consistently and I think in a safe way.
Post by John Rose
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.
As publicLookup is no longer the minimal set of privileges then
publicLookup.in(A) will impact reduce access for the case that m(A) is a
named module. Stanislav makes a good point about the javadoc as a named
module may not read all modules and so publicLookup needs to be in an
unnamed module for now.
Post by John Rose
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.)
I hope the adjustments that we have are reasonable. We start with a
singleton PL that can produce method handles to public members of
exported packages. It can then be restricted, with PL.in(A), to just the
public types exported unconditionally by A or the modules that A reads.

We have looked of taking snapshots and persisting intersections but it
diverges from bytecode behavior which I think rules it out.

-Alan
John Rose
2015-12-11 20:38:30 UTC
Permalink
Post by Alan Bateman
John,
Thanks for jumping in on this and the guidance. Comments inline.
Nice, thanks.
Post by Alan Bateman
Post by John Rose
1. full power: MethodHandles.lookup() has the same privileges as (non-constructor) bytecode in the caller class
Preserved, no changes.
Post by John Rose
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, …
Preserved with the exception of A, B and C in the same named module M and where the set modules that M reads increases (say where code in M reads additional modules).
Does this mean adding edges to the readability graph at M?
If so, then that's covered by the blanket exception mentioned later.
If you mean something else, maybe we need a point 2d.
Post by Alan Bateman
Post by John Rose
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
Preserved but perhaps with the (initially surprising) consequence that all access is lost when m(LC) is a named module and m(A) is a different module. This arises because the two modules may read very different sets of modules, the intersection cannot be expressed via a lookup class + modes.
That's fine. There are two main use cases for Lookup.in,
neither of which require the tracking of long chains of L.in(A/B/C…).

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. Agent with full-power lookup wants to get access to private nestmate in A.
He does a single L.in(A) where LC and A are in the same package member.
This works around differences between access checks at JVM and JLS levels,
just as the package-private accessor methods from javac do. (Yuck!)
Post by Alan Bateman
Post by John Rose
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
Downward convergence to zero access/empty privileges.
So points 4abc are just wrong. I can live with that.

And, publicLookup can be used to get a lookup object that
provides controlled access to a given module's M unconditional
exports, without at the same time conferring access to M's internals.
This is an important kind of weakened lookup.

Going back to graceful degradation:

3e. If A is in a named module and LC in the unnamed module (or L is a public lookup), only public names readable from A are retained

And then we get:

4. downward convergence to empty privileges
4a. if A is inaccessible to LC=L.lookupClass, L.in(A) has no privileges
4b. if A and LC are in different named modules, L.in(A) has no privileges (there is no attempt to retain an intersection of readability sets)
Post by Alan Bateman
No downward convergence to publicLookup because m(publicLookup.lookupClass) must read all modules, thus a superset of the modules that named modules will read. I should say of course that the publicLookup can only be used to create method handles to public members in packages that are exported unconditionally. So nothing that code in a named module couldn't otherwise access when it increases readability.
Right. (FWIW, the term "unconditional" is not in the Lookup javadoc.)

Should there be a way to build a lookup, for two modules M1/M2, which
reads those names of M2 which M1 can read, except no internals
of M1? I wonder if such a thing would be useful? Probably not.

But it would be useful to have a lookup in a module M1 which can
read the exports of *every* M2 that M1 can see, except no M1 internals.
(This includes the unconditionally exported public names of M1.)
This would be a Lookup with an LC in M1 and flags of PUBLIC only.

I guess that is the effect of PL.in(M1) and point 3a, right?
Post by Alan Bateman
Post by John Rose
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
publicLookup is minimally trusted and so can only create method handles where the target type is public and exported (unconditionally).
For 5b then publicLookup.in(A) may result in no change or degrade but not to empty privileges in one hop. The "no change" case is where A is in an unnamed module (think class path). The "degrade" case is where A is a named module and so the resulting lookup can only be used to access the public types that are exported unconditionally by modules that m(A) reads.
OK, then:

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 (but it will always be in the unnamed module)
5b. the only possible results of publicLookup.in(A) are no change, and (following 3e) a public-only access from a named module


Post by Alan Bateman
Post by John Rose
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.
As publicLookup is no longer the minimal set of privileges then publicLookup.in(A) will impact reduce access for the case that m(A) is a named module. Stanislav makes a good point about the javadoc as a named module may not read all modules and so publicLookup needs to be in an unnamed module for now.
Post by John Rose
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.)
I hope the adjustments that we have are reasonable. We start with a singleton PL that can produce method handles to public members of exported packages. It can then be restricted, with PL.in(A), to just the public types exported unconditionally by A or the modules that A reads.
Thanks for discussing this. I understand it better now, and agree that these
are the two most useful new "powers" for lookup objects.
Post by Alan Bateman
We have looked of taking snapshots and persisting intersections but it diverges from bytecode behavior which I think rules it out.
Well, other lookups diverge from bytecode behavior also, but only by
dropping away access modes (like private, package, etc.).
This covers use case A above. You could argue that intersecting
readability sets is useful in a similar way, but it is way too complex.
We don't aspire to create a Lookup object on exactly three API
points and no more—that's overkill. A hypothetical application
that wants to express intersections (or unions) of Lookup capabilities
can build this on top of lookups.

— John
Alan Bateman
2015-12-14 15:02:12 UTC
Permalink
Post by John Rose
Post by Alan Bateman
Post by John Rose
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, …
Preserved with the exception of A, B and C in the same named module M
and where the set modules that M reads increases (say where code in M
reads additional modules).
Does this mean adding edges to the readability graph at M?
If so, then that's covered by the blanket exception mentioned later.
If you mean something else, maybe we need a point 2d.
Yes, it's adding a directed edge so that M reads other modules. Only
code in M can do this.

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.
Post by John Rose
That's fine. There are two main use cases for Lookup.in,
neither of which require the tracking of long chains of L.in(A/B/C…).
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. Agent with full-power lookup wants to get access to private
nestmate in A.
He does a single L.in(A) where LC and A are in the same package member.
This works around differences between access checks at JVM and JLS levels,
just as the package-private accessor methods from javac do. (Yuck!)
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?

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).
Post by John Rose
3e. If A is in a named module and LC in the unnamed module (or L is a
public lookup), only public names readable from A are retained
4. downward convergence to empty privileges
4a. if A is inaccessible to LC=L.lookupClass, L.in(A) has no privileges
4b. if A and LC are in different named modules, L.in(A) has no
privileges (there is no attempt to retain an intersection of
readability sets)
Yes to both.
Post by John Rose
Post by Alan Bateman
No downward convergence to publicLookup because
m(publicLookup.lookupClass) must read all modules, thus a superset of
the modules that named modules will read. I should say of course that
the publicLookup can only be used to create method handles to public
members in packages that are exported unconditionally. So nothing
that code in a named module couldn't otherwise access when it
increases readability.
Right. (FWIW, the term "unconditional" is not in the Lookup javadoc.)
I looked over the javadoc after Stanislav's first mail and it does need
to be expanded and improved. In particular, It does not make clear that
it does not allow access to types in packages that are exported
conditionally (or "qualified exports" as we have been calling them).
Post by John Rose
Should there be a way to build a lookup, for two modules M1/M2, which
reads those names of M2 which M1 can read, except no internals
of M1? I wonder if such a thing would be useful? Probably not.
But it would be useful to have a lookup in a module M1 which can
read the exports of *every* M2 that M1 can see, except no M1 internals.
(This includes the unconditionally exported public names of M1.)
This would be a Lookup with an LC in M1 and flags of PUBLIC only.
I guess that is the effect of PL.in(M1) and point 3a, right?
Yes, PL.in(M1) gives us this.

More specifically, if C1 is a public and in a package exported by M1
then PL.in(C1) will result in a Lookup that is C1/public. This lookup
can only be used to access public types in packages that are exported
unconditionally and only by modules that M1 reads (assume M1 reads itself).

Alternatively, if C2 is a type in M2 that is not in an exported package
then PL.in(C2) will result in a Lookup that is C2/noaccess. This is of
course because PL.lookupClass() has no access to C2.
Post by John Rose
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 (but it will always be in the unnamed module)
5b. the only possible results of publicLookup.in(A) are no change, and
(following 3e) a public-only access from a named module
Yes to all of these.
Post by John Rose
Post by Alan Bateman
We have looked of taking snapshots and persisting intersections but
it diverges from bytecode behavior which I think rules it out.
Well, other lookups diverge from bytecode behavior also, but only by
dropping away access modes (like private, package, etc.).
This covers use case A above. You could argue that intersecting
readability sets is useful in a similar way, but it is way too complex.
We don't aspire to create a Lookup object on exactly three API
points and no more—that's overkill. A hypothetical application
that wants to express intersections (or unions) of Lookup capabilities
can build this on top of lookups.
Yes, the intersection would be complex, more so when qualified exports
are taken into account. Preserving PUBLIC might be a compromise,
assuming it is useful.

-Alan
John Rose
2015-12-17 23:37:52 UTC
Permalink
...
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)
Post by John Rose
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
Alex Buckley
2015-12-17 20:44:03 UTC
Permalink
Hi John,
Post by John Rose
That's fine. There are two main use cases for Lookup.in,
neither of which require the tracking of long chains of L.in(A/B/C…).
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. Agent with full-power lookup wants to get access to private nestmate in A.
He does a single L.in(A) where LC and A are in the same package member.
This works around differences between access checks at JVM and JLS levels,
just as the package-private accessor methods from javac do. (Yuck!)
Focusing on case A, please consider the following design:

- PUBLIC lookup mode means:

Any 'public' type of any package exported unconditionally by the
package's module.

- QUALIFIED lookup mode means:

Any 'public' type of any package exported in qualified fashion by the
package's module to the lookup class's module.

- MODULE lookup mode means:

Any 'public' type of any package in the lookup class's module.

(Sidebar: QUALIFIED is split from MODULE primarily to be explicit about
access rights and secondarily to support more precise slicing of access
rights in a future MethodHandles.Lookup API. Example: give me a lookup
object to access the types in this module that offer a contract, i.e.
are declared 'public' without regard to exported-ness. Example: give me
a lookup object to access the types outside this module which are
exported to it by its friends.)

- Start with an arbitrary class in an arbitrary module calling
MethodHandles.Lookup.lookup() to get a "full power" lookup object L. L's
lookup modes are PUBLIC + QUALIFIED + MODULE + PROTECTED + PACKAGE +
PRIVATE.

- The arbitrary class obtains a Class object representing class A, then
calls L.in(A):

-- If A is in a different module than L's lookup class, then the
resulting lookup object has lookup mode PUBLIC.

-- If A is in the same module as L's lookup class, but a different
package, then the resulting lookup object has lookup mode PUBLIC +
QUALIFIED + MODULE + PROTECTED. (#include some stuff about actually
accessing protected members outside A's package.)

-- If A is in the same module as L's lookup class, and in the same
package, but A is a different class than L's lookup class, then the
resulting lookup object has lookup modes PUBLIC + QUALIFIED + MODULE +
PROTECTED + PACKAGE.

-- If A is the same class as L's lookup class, then the resulting
lookup object has lookup modes PUBLIC + MODULE + PROTECTED + PACKAGE +
PRIVATE.

- L.in(A) succeeds (returns a lookup object) regardless of whether its
caller is in a module that reads A's module. Only when find* is called
on a lookup object is there a check that the caller-of-find*'s module
reads the module containing the lookup class embodied by the lookup
object. It's easy for the caller-of-find* to pass the check by calling
addReads(...) just before calling find*.

(Sidebar: Separately, the module containing the lookup class embodied by
the lookup object had better have readability to other modules in order
for find* to look up [ctors, methods, and fields of] classes in those
other modules.)
Post by John Rose
Should there be a way to build a lookup, for two modules M1/M2, which
reads those names of M2 which M1 can read, except no internals
of M1? I wonder if such a thing would be useful? Probably not.
But it would be useful to have a lookup in a module M1 which can
read the exports of *every* M2 that M1 can see, except no M1 internals.
(This includes the unconditionally exported public names of M1.)
This would be a Lookup with an LC in M1 and flags of PUBLIC only.
The difference between these two paragraphs is hard to discern. The
first paragraph seems to fix M1 and M2 while the second paragraph fixes
M1 and varies M2, but there's also a switch from "M1 can read" to "M1
can see". Modules read modules, classes see classes, types access types.
Can you restate?

Alex
John Rose
2015-12-18 02:01:08 UTC
Permalink
Any 'public' type of any package exported unconditionally by the package's module.
Accessing those is the uniquely special power of PL=publicLookup, which cavalierly
ignores the READS graph, and therefore does not need to be computed relative to anything.

For clarity I would like to give this thing a special name, UNCONDITIONAL.

UNCONDITIONAL(M1) = { T, if IS_PUBLIC(T), in PACKAGE_TYPES(P), if IS_UNCONDITIONALLY_EXPORTED(P), for P in MODULE_PACKAGES(M1) }

UNCONDITIONAL() = UNION { UNCONDITIONAL(M1), for M1 in ALL_MODULES }
Any 'public' type of any package exported in qualified fashion by the package's module to the lookup class's module.
QUALIFIED(M1) = { T, if IS_PUBLIC(T), for T in PACKAGE_TYPES(P), if IS_CONDITIONALLY_EXPORTED(P, M1), for P in MODULE_PACKAGES(M2) }

L_QUALIFIED(L) = QUALIFIED(TYPE_MODULE(L.LC))

N.B. This leaves the following related expression as an orphan (cannot be recomposed from the others):

- READABLE(M1) lookup mode means:

Any 'public' type of any package exported in qualified or unqualified fashion by the package's module to the lookup class's module.

READABLE(M1) = QUALIFIED(M1) + UNQUALIFIED(M1)
where UNQUALIFIED(M1) = UNION { UNCONDITIONAL(M2), for M1 READS M2 in READS_GRAPH }

Note that UNQUALIFIED(M1) is a subset of UNCONDITIONAL() for all M1.

These classifications are nicely disjoint, so the eventual nesting
behavior arises by disjoint union.

If PUBLIC/UNCONDITIONAL is the wrong ground-level default (my point today),
then READABLE(M1) is more useful than QUALIFIED(M1).
Any 'public' type of any package in the lookup class's module.
MODULE(M1) = { T, if IS_PUBLIC(T), for T in PACKAGE_TYPES(P), for P in MODULE_PACKAGES(M1) }

MODULE(M1) is disjoint from READABLE(M1) and QUALIFIED(M1), but not from PUBLIC/UNCONDITIONAL.

For lookups we can derive things like:

L_UNCONDITIONAL(L) = UNCONDITIONAL()
L_READABLE(L) = READABLE(TYPE_MODULE(L.LC))
L_MODULE(L) = MODULE(TYPE_MODULE(L.LC))
(Sidebar: QUALIFIED is split from MODULE primarily to be explicit about access rights and secondarily to support more precise slicing of access rights in a future MethodHandles.Lookup API. Example: give me a lookup object to access the types in this module that offer a contract, i.e. are declared 'public' without regard to exported-ness. Example: give me a lookup object to access the types outside this module which are exported to it by its friends.)
This is a useful building block, but needs to be associated with UNQUALIFIED(M1) or UNCONDITIONAL().
- Start with an arbitrary class in an arbitrary module calling MethodHandles.Lookup.lookup() to get a "full power" lookup object L. L's lookup modes are PUBLIC + QUALIFIED + MODULE + PROTECTED + PACKAGE + PRIVATE.
Or, in terms of my previous message, it could omit PUBLIC/UNCONDITIONAL and be READABLE + MODULE + PROTECTED + PACKAGE + PRIVATE

This means that PL is not a subset of all other non-trivial lookups.
It also means that the read-graph blindness stays unique to PL.
-- If A is in a different module than L's lookup class, then the resulting lookup object has lookup mode PUBLIC.
If we started with READABLE instead of PUBLIC/UNCONDITIONAL, the resulting lookup
L.in(A) would be degenerate (call it NOACCESS mode). This surprise would be the cost
of recognizing and enforcing the uniqueness of PL. At the present moment, I think
this is the best way to go.

PL.in(A) would continue to be UNCONDITIONAL (as long as A is in UNCONDITIONAL()).

(There's no union or non-empty intersection of L and PL, if they do incommensurate things.)
-- If L is PUBLIC, then L.in(A) is also PUBLIC, unless A is inaccessible to L, in which case L.in(A) has no access.
In fact, a PUBLIC/UNCONDITIONAL lookup is always the result of publicLookup()
itself or a derivative of publicLookup() via a chain of L.in(A).

BTW, there are three global rules that interact with these rules:

-- If A is not accessible to L's lookup class, using L's lookup modes, then L.in(A) has no access.

-- In all cases, the lookup modes of L.in(A) are a subset of (or equal to) the lookup modes of L.

-- In all cases (assuming no concurrent change in schemata) the set of names accessible to L.in(A) is a subset of (or equal to) the set of names accessible to L.

The third rule is a very broad requirement with all sorts of detailed implications.
All the other rules are designed to add up to the this main rule.
-- If A is in the same module as L's lookup class, but a different package, then the resulting lookup object has lookup mode PUBLIC + QUALIFIED + MODULE + PROTECTED.
Or, the resulting lookup object has a lookup mode no greater than
READABLE + MODULE /* + PROTECTED */.
(#include some stuff about actually accessing protected members outside A's package.)
Actually, PROTECTED drops away first. (The doc for Lookup.in talks about this.)
Trying to keep track of previous protected access across teleports is too hard, like
keeping track of which modules have been previously visited. Better to just drop
access modes quickly; it keeps things simple.
-- If A is in the same module as L's lookup class, and in the same package, but A is a different class than L's lookup class, then the resulting lookup object has lookup modes PUBLIC + QUALIFIED + MODULE + PROTECTED + PACKAGE.
… the resulting lookup object has a lookup mode no greater than
READABLE + MODULE + PACKAGE
-- If A is the same class as L's lookup class, then the resulting lookup object has lookup modes PUBLIC + MODULE + PROTECTED + PACKAGE + PRIVATE.
(Where did QUALIFIED go? Assuming a typo here.)

This is a degenerate case. Teleporting to your own class always produces an equivalent lookup (in fact, the same object).

-- If A is the same class as L's lookup class, then the resulting lookup object has the same lookup modes as L.

Here's a missing case:

-- If A is nested in the same package member as L's lookup class, then the resulting lookup object has lookup modes no greater than READABLE + MODULE + PACKAGE + PRIVATE.

(This is where PROTECTED drops away.)
- L.in(A) succeeds (returns a lookup object) regardless of whether its caller is in a module that reads A's module.
That would be true if L contains PUBLIC/UNCONDITIONAL, but not if it only contains the other modes.
The reason is that extra check, that L must be able to access A, else L.in(A) is NOACCESS.
Only when find* is called on a lookup object is there a check that the caller-of-find*'s module reads the module containing the lookup class embodied by the lookup object. It's easy for the caller-of-find* to pass the check by calling addReads(...) just before calling find*.
Umm, the API specifies that it performs the LC access check early (by .in),
and drops the L.in(A) to NOACCESS if A is not reachable by L.

(FTR, the access check on the referenced class REFC—first operand to
find*—happens in Lookup.checkAccess which calls VerifyAccess.isMemberAccessible
which calls VerifyAccess.isClassAccessible. The LC may be identical
to the REFC, if the code is doing self access within the LC.
In general the classes differ. The Lookup API requires a chain of
accessibility to both LC and REFC.)
(Sidebar: Separately, the module containing the lookup class embodied by the lookup object had better have readability to other modules in order for find* to look up [ctors, methods, and fields of] classes in those other modules.)
Since bytecodes look up members using string descriptors,
the only readability is for the referenced class (REFC),
and not for any of the types mentioned in other parts
of the member name (field type, method arguments, etc.).

So it's not clear what readability checks would be natural
during a find*, other than on REFC. Am I missing something?
Post by John Rose
Should there be a way to build a lookup, for two modules M1/M2, which
reads those names of M2 which M1 can read, except no internals
of M1? I wonder if such a thing would be useful? Probably not.
By that I mean this small slice of types flowing along one edge M1 READS M2:

IMPORTED(M1 READS M2) = QUALIFIED_EXPORTS_VIA(M1 READS M2) + UNQUALIFIED_EXPORTS_VIA(M1 READS M2)

The combined set QUALIFIED(M1) + UNQUALIFIED(M1) is a superset of any IMPORTED(M1 READS M2).

IMO this slice is too small to be useful.
Post by John Rose
But it would be useful to have a lookup in a module M1 which can
read the exports of *every* M2 that M1 can see, except no M1 internals.
(This includes the unconditionally exported public names of M1.)
This would be a Lookup with an LC in M1 and flags of PUBLIC only.
That is READABLE(M1) above.
The difference between these two paragraphs is hard to discern. The first paragraph seems to fix M1 and M2 while the second paragraph fixes M1 and varies M2, but there's also a switch from "M1 can read" to "M1 can see".
My bad. In "every M2 that M1 can see", s/see/read/.
Modules read modules, classes see classes, types access types. Can you restate?
Better now?

What do you think about segregating publicLookup instead of folding
his behavior into every other non-empty lookup? The cost of this
is introducing some oddities with the previously-defined mode PUBLIC,
plus two more modes (MODULE, READABLE).

Given a three-way distinction between UNCONDITIONAL(),
MODULE(M1), and READABLE(M1), there are four places to
put the pre-existing access mode PUBLIC.

0. (UNCONDITIONAL, READABLE, MODULE)

1. (PUBLIC /*= UNCONDITIONAL*/, READABLE, MODULE)

2. (UNCONDITIONAL, PUBLIC /*= READABLE*/, MODULE)

3. (UNCONDITIONAL, READABLE, PUBLIC /*= MODULE*/)

In case 0, we just retire the name Lookup.PUBLIC as being hopelessly ambiguous in the new world.

In case 1, we keep Lookup.PUBLIC but use it only for PL=publicLookup. Other guys never get PUBLIC.

In case 2, we use Lookup.PUBLIC to refer to the most-public (weakest) access level that respects the READS graph.

In case 3, we use Lookup.PUBLIC to mean just "stuff inside my module", and reserve the new names for extra-modular relations.

Then we also have the option to refuse to distinguish UNCONDITIONAL() from
UNQUALIFIED(M1), which is what you proposed, Alex. That would be a fifth choice:

4. (PUBLIC /*= UNCONDITIONAL*/, QUALIFIED, MODULE)

In case 4, everybody starts off as a superset of PL, which means a superset of all bytecode behavior.

Those seem to be our choices. I think 4 is not a disaster, though it feels dirty, since it is overly powerful.

You'd think that case 1 would be the only other choice, given the name of "publicLookup",
but in fact the spec. carefully avoids saying what are the access modes of a PL.
So we could compatibly replace PUBLIC (in that place) by a new mode UNCONDITIONAL.

I think I would prefer case 2. The user model is PUBLIC is the weakest (non-empty) access
mode available to bytecode behaviors. As such it respects the LC's position in the module
graph, and excludes module-private, package-private, and class-private. UNCONDITIONAL
is the special thing provided by publicLookup, which ignores the module graph. Then
PACKAGE opens up the LC's package, MODULE opens up the LC's module, and PRIVATE
opens up the LC itself (plus its nestmates). Feels pretty good, especially since MODULE
and PACKAGE continue to have a parallel sense of restriction.

What do you think?

— John
John Rose
2015-12-18 08:20:08 UTC
Permalink
Post by John Rose
I think I would prefer case 2. The user model is PUBLIC is the weakest (non-empty) access
mode available to bytecode behaviors. As such it respects the LC's position in the module
graph, and excludes module-private, package-private, and class-private. UNCONDITIONAL
is the special thing provided by publicLookup, which ignores the module graph. Then
PACKAGE opens up the LC's package, MODULE opens up the LC's module, and PRIVATE
opens up the LC itself (plus its nestmates). Feels pretty good, especially since MODULE
and PACKAGE continue to have a parallel sense of restriction.
What do you think?
So I caught you in the hall and we talked, and this seems agreeable to us both,
perhaps with a name change to UNCONDITIONAL, and also a distinction between
PUBLIC and QUALIFIED (as you originally proposed).

To try and tease out some symmetry here:
- Always, any type T is accessible to itself, when T = LC.
- PACKAGE mode: Any type T is accessible (within its own package), when PACKAGE(T) = PACKAGE(LC).
- MODULE mode: A public type T is accessible (within or beyond its package), when MODULE(T) = MODULE(LC).
- QUALIFIED mode: A public type T is accessible beyond its module, when IS_CE(T, LC),
where IS_CE(T, LC) = IS_CONDITIONALLY_EXPORTED(PACKAGE(T), MODULE(LC)) and MODULE(LC) READS MODULE(T).
- PUBLIC mode: A public type T is accessible beyond its module friends when IS_UE(T, LC),
where IS_UE(T, LC) = IS_UNCONDITIONALLY_EXPORTED(PACKAGE(T)) and MODULE(LC) READS MODULE(T).

These conditions can be tested independently. PACKAGE implies MODULE, but everything else is disjoint.

Also:
- UNCONDITIONAL: In this mode, a type T is accessible if IS_UNCONDITIONALLY_EXPORTED(PACKAGE(T)), regardless of LC.
- PRIVATE/PROTECTED: These protection modes apply only to non-types (JVM does not enforce "private" on classes).
- NOACCESS: This is not a mode but the absence of any combination of modes; no access is allowed.

The publicLookup should have UNCONDITIONAL and PUBLIC set.
An original full-power lookup does *not* have UNCONDITIONAL set, just PUBLIC.
The purpose of UNCONDITIONAL is to allow publicLookup to be unconcerned
(as documented) about its LC. We can restore LC to be java.lang.Object.

The distinction between QUALIFIED and PUBLIC is present simply because of
the logical fact (as you point out) that, if you teleport to a new module, you
must lose your qualified imports, but you shouldn't lose your unconditional ones.

The distinction between PUBLIC and UNCONDITIONAL is present in order
to capture the differing behaviors of lookups derived from publicLookup and
those derived from full-power lookups.

The presence of MODULE captures the larger but package-like scope of
a module's internal names.

About "mode stripping":

You suggested (which sounds OK) that there is no need to "validate" bit masks of lookup objects.
Just have excludeModes clear some bits and continue. This means there can be lookups which
are able to read (say) using PACKAGE mode but not PUBLIC mode. (Today's lookups can
have PUBLIC without PACKAGE but not vice versa.) Each mode bit enables a single
line in the above logic, and (as you can see) the lines can be applied independently.
Some implications follow.

When looking up a type member T.M, the additional access checking on the
member's enclosing type T (from LC) may lack PACKAGE, MODULE, QUALIFIED,
and PUBLIC modes (if excludeModes stripped those bits from a full-power lookup).
This means that such mode-stripped lookups can (logically) only obtain members
LC.M from the single lookup class LC (since a class always has access to itself).
That seems reasonable and useful.

A stripped lookup with only MODULE or QUALIFIED bits will never be able to "see"
any T.M, since members only match in PUBLIC, PRIVATE, PROTECTED, and
PACKAGE modes. Odd, and probably not useful, but logical.

There does not seem to be a way to say "give me only the T.M for which T
is package-private and M is public", or "give me only the T.M for which T
is module-private but M is public". Those can be composed on top,
especially with the new Lookup.accessClass API point as a post-test on T.

As an odd use case, a stripped lookup with only PACKAGE modes
will be able to see any package-mate T of LC, and any package-private
API point T.M, but it won't be able to query anything *outside* of the
package of T. Unfortunately, it also won't be able to query any public
member T.M, unless the PUBLIC bit is present. So I suppose stripping
MODULE and QUALIFIED, leaving PUBLIC and PACKAGE, would
provide useful access to T.M even if M were public.

As you see, I'm trying to apply the same mode bits to both types
and their members, as mechanically as possible. One place where
that is tricky is with UNCONDITIONAL. That is rescued by making
sure that UNCONDITIONAL will not lose PUBLIC even if teleported.

About monotonicity:

The rules for monotonicity of access modes will be strictly applied.
All transforms that create lookups from previous ones will never
produce lookups with additional mode bits set. For excludeModes
this is trivially true. For Lookup.in, it is true in a complex way, as
modes are shed as you "teleport" farther away from the old LC
to the new LC.

The design principle of monotonicity of accessible types and members
(which I mentioned earlier) is stressed by the case of teleporting
between modules, where (you might think) that PUBLIC in module M1
should continue as PUBLIC in modules M2. But that is not monotonic.
The bytecode behaviors involving cross-module readability depend on
the read edges of MODULE(LC).

I think the best path is pretty simple: If you teleport between modules,
you lose all bits, unless UNCONDITIONAL is set, in which case you are
also allowed to keep PUBLIC (since you were already ignoring the read
graph).

It's more obvious that teleporting between modules should immediately
drop QUALIFIED. FTR, you could check the read graph and the exports
and if the two LC's had exactly the same read edges, you could justify
keeping the PUBLIC bit set, and similarly for the QUALIFIED bit, if you
check for equal qualified exports. But I think that's too much complexity
for too little benefit. Basically, only the publicLookup is able to leap
between modules. Which seems just fine to me.

— John
Alex Buckley
2015-12-19 00:44:51 UTC
Permalink
Post by John Rose
I think I would prefer case 2. The user model is PUBLIC is the
weakest (non-empty) access mode available to bytecode behaviors. As
such it respects the LC's position in the module graph, and
excludes module-private, package-private, and class-private.
UNCONDITIONAL is the special thing provided by publicLookup, which
ignores the module graph. Then PACKAGE opens up the LC's package,
MODULE opens up the LC's module, and PRIVATE opens up the LC itself
(plus its nestmates). Feels pretty good, especially since MODULE
and PACKAGE continue to have a parallel sense of restriction.
What do you think?
So I caught you in the hall and we talked, and this seems agreeable
to us both, perhaps with a name change to UNCONDITIONAL, and also a
distinctionbetween PUBLIC and QUALIFIED (as you originally proposed).
- Always, any type T is accessible to itself, when T = LC.
- PACKAGE mode: Any type T is accessible (within its own package), when
PACKAGE(T) = PACKAGE(LC).
- MODULE mode: A public type T is accessible (within or beyond its
package), when MODULE(T) = MODULE(LC).
- QUALIFIED mode: A public type T is accessible beyond its module, when IS_CE(T, LC),
where IS_CE(T, LC) = IS_CONDITIONALLY_EXPORTED(PACKAGE(T),
MODULE(LC)) and MODULE(LC) READS MODULE(T).
- PUBLIC mode: A public type T is accessible beyond its module friends when IS_UE(T, LC),
where IS_UE(T, LC) = IS_UNCONDITIONALLY_EXPORTED(PACKAGE(T)) and
MODULE(LC) READS MODULE(T).
These conditions can be tested independently. PACKAGE implies MODULE,
but everything else is disjoint.
- UNCONDITIONAL: In this mode, a type T is accessible if
IS_UNCONDITIONALLY_EXPORTED(PACKAGE(T)), regardless of LC.
- PRIVATE/PROTECTED: These protection modes apply only to non-types (JVM
does not enforce "private" on classes).
- NOACCESS: This is not a mode but the absence of any combination of
modes; no access is allowed.
So let's recap full power lookups:

- Start with an arbitrary class in an arbitrary module calling
MethodHandles.Lookup.lookup() to get a "full power" lookup object L. L's
lookup modes are PUBLIC + QUALIFIED + MODULE + PROTECTED + PACKAGE +
PRIVATE.

- The arbitrary class obtains a Class object representing class A, then
calls L.in(A). If L's lookup class cannot access A (for example, A is
package-private in a different package than L's lookup class), then the
resulting lookup object has lookup mode NOACCESS. Otherwise:

-- If A is in a different module than L's lookup class, then the
resulting lookup object has lookup mode NOACCESS.

-- If A is in the same module as L's lookup class, but a different
package, then the resulting lookup object has lookup modes no greater
than PUBLIC + QUALIFIED + MODULE.

-- If A is in the same module as L's lookup class, and in the same
package, but A is a different class than L's lookup class, then the
resulting lookup object has lookup modes no greater than PUBLIC +
QUALIFIED + MODULE + PACKAGE.

-- If A is nested in the same package member as L's lookup class,
then the resulting lookup object has lookup modes no greater than PUBLIC
+ QUALIFIED + MODULE + PACKAGE + PRIVATE.

-- If A is the same class as L's lookup class, then the resulting
lookup object has the same lookup modes as L.
Post by John Rose
The publicLookup should have UNCONDITIONAL and PUBLIC set. An
original full-power lookup does *not* have UNCONDITIONAL set, just
PUBLIC. The purpose of UNCONDITIONAL is to allow publicLookup to be
unconcerned (as documented) about its LC. We can restore LC to be
java.lang.Object.
Since PUBLIC is just UNCONDITIONAL with a concern for readability, it's
surprising that publicLookup cares about PUBLIC.

Before saying any more about that, let me take a small detour. I seem to
recall an intent to specify the public lookup object as representing an
(undisclosed) lookup class in the unnamed module ... if so, then since
the unnamed module reads all named modules by decree, we have
UNCONDITIONAL+PUBLIC as trivially equal in access power to PUBLIC, and
we don't need UNCONDITIONAL at all ... if not, then publicLookup could
be UNCONDITIONAL only.

Returning from the detour ... does the public lookup object have PUBLIC
solely so that it can teleport to give new lookup objects which drop
UNCONDITIONAL but still retain the interesting PUBLIC mode?

Proceeding to walk through a publicLookup:

- Start with an arbitrary class in an arbitrary module calling
MethodHandles.Lookup.publicLookup() to get a public lookup object PL.
PL's lookup mode is UNCONDITIONAL [+ PUBLIC?].

- The arbitrary class obtains a Class object representing class A, then
calls PL.in(A). If PL's lookup class cannot access A [I'm not sure what
PL's lookup class is, but it seems plausible that it can't access A],
then the resulting lookup object has lookup mode NOACCESS. Otherwise:

-- If A is in a different module than PL's lookup class, then the
resulting lookup object has lookup mode UNCONDITIONAL. ???

-- If A is in the same module as PL's lookup class, then the resulting
lookup object has lookup mode PUBLIC. ???
Post by John Rose
As an odd use case, a stripped lookup with only PACKAGE modes
will be able to see any package-mate T of LC, and any package-private
API point T.M, but it won't be able to query anything *outside* of the
package of T. Unfortunately, it also won't be able to query any public
member T.M, unless the PUBLIC bit is present. So I suppose stripping
MODULE and QUALIFIED, leaving PUBLIC and PACKAGE, would
provide useful access to T.M even if M were public.
I think there's an "old" meaning of PUBLIC floating around here. Now
that PUBLIC pertains to public types _in unconditionally exported
packages_, it shouldn't relate to intra-package access, since for such
access you don't care if the package is exported. It's now MODULE that
accesses public types within the module, so arguably, access to a public
member M of package-mate T should be possible with lookup modes
MODULE+PACKAGE, not PUBLIC+PACKAGE.

Alex
John Rose
2015-12-19 06:32:36 UTC
Permalink
...
- Start with an arbitrary class in an arbitrary module calling MethodHandles.Lookup.lookup() to get a "full power" lookup object L. L's lookup modes are PUBLIC + QUALIFIED + MODULE + PROTECTED + PACKAGE + PRIVATE.
-- If A is in a different module than L's lookup class, then the resulting lookup object has lookup mode NOACCESS.
-- If A is in the same module as L's lookup class, but a different package, then the resulting lookup object has lookup modes no greater than PUBLIC + QUALIFIED + MODULE.
-- If A is in the same module as L's lookup class, and in the same package, but A is a different class than L's lookup class, then the resulting lookup object has lookup modes no greater than PUBLIC + QUALIFIED + MODULE + PACKAGE.
-- If A is nested in the same package member as L's lookup class, then the resulting lookup object has lookup modes no greater than PUBLIC + QUALIFIED + MODULE + PACKAGE + PRIVATE.
-- If A is the same class as L's lookup class, then the resulting lookup object has the same lookup modes as L.
I agree; that's just how they teleport.
Post by John Rose
The publicLookup should have UNCONDITIONAL and PUBLIC set. An
original full-power lookup does *not* have UNCONDITIONAL set, just
PUBLIC. The purpose of UNCONDITIONAL is to allow publicLookup to be
unconcerned (as documented) about its LC. We can restore LC to be
java.lang.Object.
Since PUBLIC is just UNCONDITIONAL with a concern for readability, it's surprising that publicLookup cares about PUBLIC.
The PUBLIC bit is along for the ride. It's meaningless when accompanied by UNCONDITIONAL, and can stay on, meaningfully, after UNCONDITIONAL drops off.

(Actually, PUBLIC is also required when you are looking up a *member* C.M of class C using publicLookup. Both C and C.M had better be marked "public". And you had better have the PUBLIC mode bit set so you can make use of that fact. The C.M is not marked "unconditional".)
Before saying any more about that, let me take a small detour. I seem to recall an intent to specify the public lookup object as representing an (undisclosed) lookup class in the unnamed module ... if so, then since the unnamed module reads all named modules by decree, we have UNCONDITIONAL+PUBLIC as trivially equal in access power to PUBLIC, and we don't need UNCONDITIONAL at all ... if not, then publicLookup could be UNCONDITIONAL only.
True; this uses that odd feature of the unnamed module. It's clever, but surprising.

With UNCONDITIONAL, though, we don't need to use that feature, and can compatibly retain the JDK7/8 behavior of PL.LC == Object.

Also, without UNCONDITIONAL, you can't teleport publicLookup to another module (you drop to NOACCESS).

That is a mildly bad thing, because Lookup.findClass assumes flexible teleportation.

The use case with PL is:
Q: I want to find an unconditionally readable class named "Foo", from the viewpoint (classloader & module) of the class Bar whose Class object Bar.class I possess. How?
A: publicLookup().in(Bar.class).findClass("Foo").

Basically, adding UNCONDITIONAL allows Lookup to manage the magic directly, instead of leaning on the unnamed module, with more power to boot.

UNCONDITIONAL also tracks the fact that the Lookup object came from publicLookup, not full-power-lookup. If not a mode bit, how else will be do it?
Returning from the detour ... does the public lookup object have PUBLIC solely so that it can teleport to give new lookup objects which drop UNCONDITIONAL but still retain the interesting PUBLIC mode?
Yes; see above for an example.
- Start with an arbitrary class in an arbitrary module calling MethodHandles.Lookup.publicLookup() to get a public lookup object PL. PL's lookup mode is UNCONDITIONAL [+ PUBLIC?].
Yes, UNCONDITIONAL + PUBLIC.
- The arbitrary class obtains a Class object representing class A, then calls PL.in(A). If PL's lookup class cannot access A [I'm not sure what PL's lookup class is, but it seems plausible that it can't access A], then the resulting lookup object has lookup mode NOACCESS.
If PACKAGE(A) is unconditionally exported by its module, then all is well, since the UNCONDITIONAL bit enables the required read.
-- If A is in a different module than PL's lookup class, then the resulting lookup object has lookup mode UNCONDITIONAL. ???
Still UNCONDITIONAL + PUBLIC. When the teleported lookup is used, it also ignores read edges.
-- If A is in the same module as PL's lookup class, then the resulting lookup object has lookup mode PUBLIC. ???
If PACKAGE(A) is unconditionally exported by its module, then the above rule applies.

If PACKAGE(A) is otherwise (mod-private or qual-export) then… A was not accessible to PL in the first place, so NOACCESS.

So a teleportation of PL either retains all rights or loses all rights.

It's also possible to teleport PL to some module M1 and then strip the result of UNCONDITIONAL, by saying restrictModes(PUBLIC)(.

At that point the lookup is marooned in M1, and will only ever see the unconditional exports via M1 READS M2.
Post by John Rose
As an odd use case, a stripped lookup with only PACKAGE modes
will be able to see any package-mate T of LC, and any package-private
API point T.M, but it won't be able to query anything *outside* of the
package of T. Unfortunately, it also won't be able to query any public
member T.M, unless the PUBLIC bit is present. So I suppose stripping
MODULE and QUALIFIED, leaving PUBLIC and PACKAGE, would
provide useful access to T.M even if M were public.
I think there's an "old" meaning of PUBLIC floating around here. Now that PUBLIC pertains to public types _in unconditionally exported packages_, it shouldn't relate to intra-package access, since for such access you don't care if the package is exported. It's now MODULE that accesses public types within the module, so arguably, access to a public member M of package-mate T should be possible with lookup modes MODULE+PACKAGE, not PUBLIC+PACKAGE.
Yes, that's arguable, but it makes my brain hurt to argue it. It
means that, when access-checking a findStatic of C.M, if M is public,
we have to ask if we have MODULE access, not PUBLIC access. This is
sort of consistent with the deeper meaning of M's "public" bit, but
will require endless explanation. Meanwhile, if we (mis-)use the
PUBLIC bit when accessing a "public" M, nobody gets hurt, since all
natural lookups with MODULE also have PUBLIC.

If you use restrictModes to drop PUBLIC and keep MODULE, you get an
artificial lookup which stays at home in its module but never looks
outside. No lookup today operates this way, but it's logical and
might be useful (with findClass). But if you apply this artificial
lookup to findStatic(C, "M") on a MODULE-private C, do you really
have a right to access C.M? Given that the docs clearly say that
the lookupModes bits modulate access to *both* C and M?

Hence, as I said, I prefer to apply those bits as mechanically as possible.

— John

Alan Bateman
2015-12-11 08:14:53 UTC
Permalink
Post by stanislav lukyanov
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
A.in(B).in(C).in(D) would have access to D's exported members and D's
'requires public' - same as A.
The MODULE bit is for access to "module private" members, it has to be
dropped when hopping to a lookup class in another module.

In the A.in(B).in(C).in(D) example then each hop will preserve or
degrade access. The modules for A, B, C and D may read very different
sets of modules. The lookup class and mode bits aren't enough to
represent this intersection so this is why access degrades quickly (or
immediately) to 0. We have of course looked at preserving a snapshot of
the intersection but this complicates things greatly and would diverge
from bytecode behavior.
Post by stanislav lukyanov
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).
I think it shouldn't allow something just because reflection API is
able to do the trick anyway.
After all, it doesn't go easy with method access checks despite we
have Method.setAccessible
It might be a surprise but changing the readability graph at run-time
via addReads changes the VM and bytecode behavior, it's not tied to core
reflection. So yes, as things currently stand, then addReads will
increase readability and therefore the types that can be accessed. Using
addReads cannot be used to break encapsulation of course.
Post by stanislav lukyanov
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.
You're right. This wording was mostly correct in early prototyping but
isn't right now. It needs to be specified to be in an unnamed module, it
can't be in a named module (at least not unless we get to the point
where a named module can read all other modules).

-Alan
Loading...