On 07/27/2013 09:01 PM, Nick Williams wrote:
All,
In the last two months, there have been a number of discussions surrounding
stack traces, Classes on the stack trace, and caller classes [1], [2], [3].
These are all related discussions and the solution to them is equally related,
so I wanted to consolidate it all into this one discussion where I hope we can
finalize on a solution and get it implemented for Java 8.
In a nut shell, here are the underlying needs that I have seen expressed
through many, many messages:
- Some code needs to get the Class of the caller of the current method,
skipping any reflection methods.
- Some code needs to get the Class of the caller /n/ stack frames before the
current method, skipping any reflection methods.
- Some code needs to get the current stack trace, populated with Classes,
Executables, file names, line numbers, and native flags instead of the String
class names and String method names in StackTraceElement. This /should/ include
any reflection methods, just like StackTraceElement[]s.
- Some code needs to get the stack trace from when a Throwable was created,
populated with Classes, Executables, file names, line numbers, and native flags
instead of the String class names and String method names in StackTraceElement.
This /should/ include any reflection methods, just like StackTraceElement[]s.
- There needs to be a reflection way to achieve all of this since some
libraries (e.g., Log4j) need to be compiled against Java 6 but run on 7 and 8
(and thus can't use @CallerSensitive).
I believe the solutions to these needs are all related. Importantly, I think it is
very important that action be taken in Java 8 due to the changes made to
sun.reflect.Reflection#getCallerClass(...). While we all understand that relying
on private sun.* APIs is not safe, the fact is that many people have relied on
sun.reflect.Reflection#getCallerClass(...) due to the fact that there is simply no
other way to do this in the standard API. This includes Log4j 2, Logback, SLF4j,
and Groovy, some features of which will stop working correctly in Java 7 >= u25.
Hi,
The needs described above may seem related, but from what I see in
this commit:
http://hg.openjdk.java.net/jdk8/tl/jdk/rev/da6addef956e
my observations are as following (please comment if I missed or
misunderstood anything):
sun.reflect.Reflection.getCallerClass(int) is/was used internally in
JDK more or less for different purposes than outside the JDK. Inside
it was used basically for implementing security-sensitive checks like
optimizations for public methods which can avoid calling
SecurityManager API wen called from withing JDK classes. It was used
for security-unrelated purposes too, like for example
Class.forName(String) or ResourceBundle.getBundle(String). All
internal JDK uses do share one common thing though: it is very
important that the right direct caller of a caller-sensitive method
is established, since any failure to do so can have devastating
effect on security or correctness.
The API taking an "int" to count the frames between the top of the
call-stack to the indirect caller was convenient, but too fragile to
support such important use cases. Every time some code was
refactored, there was danger that some call-frame was inadvertently
inserted or removed. So I think it was decided to "cripple" the API
to only support obtaining the immediate caller of the method making
call to the Reflection.getCallerClass() and all uses modified
accordingly to make the internal JDK code more robust to refactorings.
And there's also MethodHandles which are early-bound. Meaning that
the caller is established and bound when the MethodHandle instance is
looked-up. The "lookupClass" which is associated with the Lookup
object and used in permission checks when obtaining MHs is also used
as the bound caller class when the MH is invoked. Now there's a method:
java.lang.invoke.MethodHandles.Lookup {
public java.lang.invoke.MethodHandles.Lookup
in(java.lang.Class<?> requestedLookupClass)
that returns a Lookup object which reports a different "lookupClass"
and has capabilities to lookup MHs which are combined from
capabilities of the original Lookup object and new lookupClass
(tipicaly less, never more). Most of such Lookup objects are
prevented from looking-up MHs for caller-sensitive methods, since
they could be used to "pose" as a caller that is not the one having
obtained the MH and therefore gain access to restricted resource, for
example:
MethodHandle mh = MethodHandles.lookup().in(Object.class).lookupXXX(....)
...such mh could be used to pose as being called from withing Object
if allowed to be obtained for caller-sensitive methods. So here comes
@CallerSensitive annotation to mark such methods and prevent such
lookups (before that - in JDK7, all internal caller-sensitive methods
were hand-maintained in a list).
So this is, what I think, the background and rationale for changing
the API.
For outside JDK use, I think there are two main needs, which are
actually distinct:
a) the caller-sensitive methods
b) anything else that is not caller-sensitive, but wants to fiddle
with the call-stack
For caller-sensitive methods, the approach taken with new
Reflection.getCallerClass() is the right one, I think. There's no
need to support a fragile API when caller-sensitivity is concerned,
so the lack of "int" parameter, combined with annotation for marking
such methods is correct approach, I think. The refactorings to
support this change in JDK show that this API is adequate. The
"surface" public API methods must capture the caller class and pass
it down the internal API where it can be used.
So what would give Groovy or other language runtimes headaches when
all there was was a parameter-less getCallerClass() API? Aren't the
intermediate frames inserted by those runtimes controlled by the
runtimes? Couldn't the "surface" runtime-inserted methods capture the
caller and pass it down? I guess the problem is supporting calling
the caller-sensitive methods like Class.forName(String) and such
which don't have the overloaded variant taking caller Class or
ClassLoader as an argument...
John Rose suggested to "capture" the caller in the "surface" method
and bind it with a MethodHandle and then pass such MH down the
runtime API and finally call that method via MH. For that to work,
two problems would have to be resolved first:
1) the runtime-inserted "surface" method would have to be annotated
with @CallerSensitive so that illegal "posers" could be prevented
2) this "surface" method would have to be given permission to "pose
as" the caller class when looking-up the MethodHandle of the target
caller-sensitive method.
The 1st part is not a problem I think, but the 2nd part is a problem.
What makes the runtime-inserted "surface" method so special that it
can be allowed to "pose" as its caller?
Now that is the question for mlvm-dev mailing list: Isn't preventing
almost all Lookup objects obtained by
Lookup.in(RequestedLookupClass.class) from obtaining MHs of
@CallerSensitive methods too restrictive?
Currently classes are only allowed to "pose as" it's nest-mate
classes - the classes that share the same outermost enclosing class,
since the check to look-up @CallerSensitive methods is based on the
ability to look-up PRIVATE members. So the ability to "pose as"
another class when binding caller already exists even for
@CallerSensitive methods, just the restriction is too conservative,
isn't it?
Perhaps a class that is visible from the calling class could be
allowed to look-up MHs of @CallerSensitive methods and "pose" as the
calling class, bearing all other security checks for combined
abilities have passed. For example, why wouldn't class A be allowed
to "pose as" class B if they are loaded by the same ClassLoader or if
class B is loaded by a ClassLoader that directly or indirectly
delegates to the ClassLoader of class A?
These are my thoughts about caller-sensitivity and why I think it
requires special restricted API. Anything else that needs to examine
the whole call-stack is a separate need that is not infected by the
strict constraints of caller-sensitivity and for that purpose an API
like the one presented below (StackTraceFrame) is a good
starting-point, maybe it just doesn't need the static
getCallerFrame() method which suggests that it's use is for
implementing caller-sensitive methods.
Regards, Peter
I would point out that this could all easily be solved simply by adding a
getElementClass() method to StackTraceElement, but there was strong opposition
to this, largely due to serialization issues. Since that is apparently not an
option, I propose the following API, based on the various discussions in the
last two months, StackTraceElement, and the API that .NET provides to achieve
the same needs as listed above:
CallerSensitive.java:
package java.lang;
/** Previously private API, now public */
public @interface CallerSensitive {
...
}
StackTraceFrame.java:
package java.lang;
import java.util.Objects.
public final class StackTraceFrame {
private final Class<?> declaringClass;
private final Executable executable;
private final String fileName;
private final int lineNumber;
public StackTraceFrame(Class<?> declaringClass, Executable executable,
String fileName, int lineNumber) {
this.declaringClass = Objects.requireNonNull(declaringClass, "Declaring
class is null");
this.executable = Objects.requireNonNull(executable, "Executable is
null");
this.fileName = fileName;
this.lineNumber = lineNumber;
}
public Class<?> getDeclaringClass() {
return this.declaringClass;
}
public Executable getExecutable() {
return this.executable;
}
public String getFileName() {
return this.fileName;
}
public int getLineNumber() {
return this.lineNumber;
}
public boolean isNative() {
return this.lineNumber == -2;
}
public String toString() { /* Same as StackTraceElement */ }
public boolean equals() { /* Ditto */ }
public int hashCode() { /* Ditto */ }
/** Uses @CallerSensitive */
public static native StackTraceFrame getCallerFrame();
/** Works like Java < 7u25 sun.reflect.Reflection#getCallerClass() */
public static native StackTraceFrame getCallerFrame(int skipFrames);
public static native StackTraceFrame[] getCurrentStackTrace();
}
Throwable.java:
package java.lang;
...
public class Throwable {
...
public synchronized Throwable fillInStackTraceFrames() { ... }
private native Throwable fillInStackTraceFrames(int dummy);
public StackTraceFrame[] getStackTraceFrames() {
return this.getOurStackTraceFrames().clone();
}
private synchronized StackTraceFrame[] getOurStackTraceFrames() { ... }
...
}
Furthermore, I propose that we restore the behavior of
sun.reflect.Reflection#getCallerClass(int) /just for Java 7/ since the proposed
above solution cannot be added to Java 7.
I would love if we could quickly coalesce around this solution or a derivative
thereof so that it can be implemented before Feature Complete. The absence of
any replacement or alternative for sun.reflect.Reflection#getCallerClass(int)
will be a serious issue in Java 8 that will cause hardships for many projects.
[1]http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-June/018049.html
[2]http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-June/018349.html,http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-July/019098.html
[3]http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-July/018855.html