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 12:17:50 UTC 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




More information about the core-libs-dev mailing list