Classes on the stack trace (was: getElementClass/StackTraceElement, was: @CallerSensitive public API, was: sun.reflect.Reflection.getCallerClass)
Nick Williams
nicholas+openjdk at nicholaswilliams.net
Tue Jul 30 12:32:56 UTC 2013
"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."
Not when the code running on Java 8 was compiled on Java 6 or 7 and thus can't be annotated @CallerSensitive. In these cases, use of any new public API must use reflection (sudo code: If Java 8, use new public caller-sensitive API), so it needs code that it can pass a number into.
Nick
On Jul 30, 2013, at 7:17 AM, Peter Levart wrote:
>
> 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