Classes on the stack trace (was: getElementClass/StackTraceElement, was: @CallerSensitive public API, was: sun.reflect.Reflection.getCallerClass)
Peter Levart
peter.levart at gmail.com
Tue Jul 30 05:17:50 PDT 2013
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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://mail.openjdk.java.net/pipermail/mlvm-dev/attachments/20130730/b0773aac/attachment.html
More information about the mlvm-dev
mailing list