Is it OK for a program which otherwise runs fine to fail during class verification if running with a security manager because of module access restrictions?

Consider the following small, self-contained program which statically references com.sun.crypto.provider.SunJCE:

import java.security.Provider;
import com.sun.crypto.provider.SunJCE;

public class Tricky {

  public static void main(String args[]) throws Exception {
    try {
      System.out.println(Tricky.class + " (" + Tricky.class.getClassLoader() + ")" + args[0]);
    }
    catch (Exception e) {
      Provider p = new SunJCE();
      System.out.println(p + " (" + p.toString() + ")");
    }
  }
}

This program easily compiles and runs with Oracle/OpenJDK 8:

$ javac Tricky
$ java Tricky ""
class Tricky (sun.misc.Launcher$AppClassLoader@4e0e2f2a)
$ java Tricky
SunJCE version 1.8 (SunJCE version 1.8)

The second invocation (without argument) will cause an exception (when trying to access the first element of the zero length argument array at 'args[0]') which will be caught in the exception handler where we create a new 'SunJCE' object and print its string representation to stdout. The first invocation (with an empty argument) doesn't provoke an exception and will just print the class itself together with its class loader.

When we run the same (jdk8 compiled) program with jdk9, we'll see the following result:

$ jdk9/java Tricky ""
class Tricky (jdk.internal.loader.ClassLoaders$AppClassLoader@ba8a1dc)
$ jdk9/java Tricky
Exception in thread "main" java.lang.IllegalAccessError: class Tricky (in unnamed module @0x3b192d32) cannot access class com.sun.crypto.provider.SunJCE (in module java.base) because module java.base does not export com.sun.crypto.provider to unnamed module @0x3b192d32
  at Tricky.main(Tricky.java:11)
$ jdk9/java --add-exports java.base/com.sun.crypto.provider=ALL-UNNAMED Tricky
SunJCE version 9 (SunJCE version 9)

The first invocation (with an empty argument) still works because we have lazy class loading and 'SunJCE' provider is actually never used. The second invocation (without argument) obviously fails with jdk9 because 'java.base' doesn't export the package com.sun.crypto.provider anymore. This can be fixed by adding '--add-exports java.base/com.sun.crypto.provider=ALL-UNNAMED' to the command line as demonstrated in the third invocation.

So far so good - everything worked as expected till now. Now let's run the same program with the default security manager:

$ java -Djava.security.manager Tricky ""
class Tricky (sun.misc.Launcher$AppClassLoader@4e0e2f2a)
$ java -Djava.security.manager Tricky
SunJCE version 1.8 (SunJCE version 1.8)

The security manager doesn't change anything for jdk8! So let's try with jdk9:

$ jdk9/java -Djava.security.manager Tricky ""
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "accessClassInPackage.com.sun.crypto.provider")
	at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:471)
	at java.base/java.security.AccessController.checkPermission(AccessController.java:894)
	at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:561)
	at java.base/java.lang.SecurityManager.checkPackageAccess(SecurityManager.java:1534)
	at java.base/java.lang.ClassLoader$1.run(ClassLoader.java:671)
	at java.base/java.lang.ClassLoader$1.run(ClassLoader.java:669)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.lang.ClassLoader.checkPackageAccess(ClassLoader.java:669)
	at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
	at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3129)
	at java.base/java.lang.Class.getMethodsRecursive(Class.java:3270)
	at java.base/java.lang.Class.getMethod0(Class.java:3256)
	at java.base/java.lang.Class.getMethod(Class.java:2057)
	at java.base/sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:712)
	at java.base/sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:570)

The first invocation (with an empty argument) crashes with a strange JNI error. The call stack unveils that we haven't even entered the main method of our program. Instead we've crashed in the jdk-internal launcher 'sun.launcher.LauncherHelper' during 'checkAndLoadMain()' with a 'java.security.AccessControlException' because we couldn't access the 'com.sun.crypto.provider' package. That's strange because we shouldn't need to load SunJCE provider for this invocation because of lazy class loading.

A little reasoning and the right Xlog parameter unveils that the exception happens during class verification:

$ jdk9/java -Djava.security.manager -Xlog:verification Tricky ""
...
[1,382s][info][verification] locals: { '[Ljava/lang/String;', 'java/lang/Exception', 'com/sun/crypto/provider/SunJCE' }
[1,382s][info][verification] stack: { 'java/io/PrintStream', 'java/lang/StringBuilder', 'com/sun/crypto/provider/SunJCE' }
[1,382s][info][verification] offset = 77,  opcode = invokevirtual
[1,589s][info][verification] Verification for Tricky has exception pending java.security.AccessControlException 
[1,589s][info][verification] End class verification for: Tricky
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "accessClassInPackage.com.sun.crypto.provider")
...

And indeed, switching off class verification will fix the problem:

$ jdk9/java -Djava.security.manager -Xlog:verification -noverify Tricky ""
class Tricky (jdk.internal.loader.ClassLoaders$AppClassLoader@1b9e1916)

But switching off class verification is not something we usually want to do!

So we can try to use '--add-exports java.base/com.sun.crypto.provider=ALL-UNNAMED' (although this wasn't needed without security manager):

$ jdk9/java -Djava.security.manager --add-exports java.base/com.sun.crypto.provider=ALL-UNNAMED Tricky ""
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "accessClassInPackage.com.sun.crypto.provider")
...

but the result is still the same. If you feel that '--add-exports java.base/com.sun.crypto.provider=ALL-UNNAMED' should have fixed the problem, you're not alone. That issue is tracked under JDK-8174766 [1]. It was discussed in the review [2] for JDK-8055206 [3] which introduced the problem.

Currently, the only way to fix the problem is to add additional permissions through a custom security policy:

my.policy
---------
grant {
  permission java.lang.RuntimePermission "accessClassInPackage.com.sun.crypto.provider";
};

$ jdk9/java -Djava.security.manager -Djava.security.policy=my.policy Tricky ""
class Tricky (jdk.internal.loader.ClassLoaders$AppClassLoader@1b9e1916)

But the real question remains: why does class verification fails with a security manager and succeeds without?

With the right Xlog parameters we can verify, that even without security manager, the 'SunJCE' class is successfully loaded by the verifier for the purpose of verification, and the execution of the program succeeds if we don't reference 'SunJCE' during execution:

$ jdk9/java -Xlog:verification -Xlog:class+load Tricky ""
...
[1,193s][info][verification] locals: { '[Ljava/lang/String;', 'java/lang/Exception', 'com/sun/crypto/provider/SunJCE' }
[1,193s][info][verification] stack: { 'java/io/PrintStream', 'java/lang/StringBuilder', 'com/sun/crypto/provider/SunJCE' }
[1,193s][info][verification] offset = 77,  opcode = invokevirtual
[1,196s][info][class,load  ] java.security.Provider source: jrt:/java.base
[1,197s][info][class,load  ] com.sun.crypto.provider.SunJCE source: jrt:/java.base
...
[1,198s][info][verification] End class verification for: Tricky
...
class Tricky (jdk.internal.loader.ClassLoaders$AppClassLoader@ba8a1dc)

So how does the presence of a security manager changes the class verification process?

With a security manager, there are two additional 'accessClassInPackage' checks during class loading. If we recall the verifier log:

[1,193s][info][verification] locals: { '[Ljava/lang/String;', 'java/lang/Exception', 'com/sun/crypto/provider/SunJCE' }
[1,193s][info][verification] stack: { 'java/io/PrintStream', 'java/lang/StringBuilder', 'com/sun/crypto/provider/SunJCE' }
[1,193s][info][verification] offset = 77,  opcode = invokevirtual
[1,196s][info][class,load  ] java.security.Provider source: jrt:/java.base
[1,197s][info][class,load  ] com.sun.crypto.provider.SunJCE source: jrt:/java.base

we see that the verifier has to prove that 'com/sun/crypto/provider/SunJCE' (which is on top of the stack) can be assigned (i.e. is assign-compatible) to 'java.security.Provider' and it is therefor safe to call 'Provider::toString()' on it. Therefor, in order to load the class 'com.sun.crypto.provider.SunJCE', the verifier calls 'SystemDictionary::resolve_or_fail()' which in the end calls 'ClassLoader.loadClass(_class_name)' on the class loader of the class under verification. If that class is a user class, this will call 'jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass()' in Java 9 because that's the actual application class loader (aka system class loader) and ClassLoaders$AppClassLoader.loadClass() will perform the following check if the VM runs with a security manager:

 @Override
 protected Class loadClass(String cn, boolean resolve) throws ClassNotFoundException
 {
     SecurityManager sm = System.getSecurityManager();
     if (sm != null) {
	 int i = cn.lastIndexOf('.');
	 if (i != -1) {
	     sm.checkPackageAccess(cn.substring(0, i));
	 }
     }
     return super.loadClass(cn, resolve);
 }

Here's the corresponding VM stack trace:

j  java.lang.SecurityManager.checkPackageAccess(Ljava/lang/String;)V+46 java.base@9-internal
j  jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Ljava/lang/String;Z)Ljava/lang/Class;+8 java.base@9-internal
j  java.lang.ClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;+3 java.base@9-internal
v  ~StubRoutines::call_stub
V  [libjvm.so+0xc6c75d]  JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0x6a5
V  [libjvm.so+0x1055bab]  os::os_exception_wrapper(void (*)(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*), JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0x41
V  [libjvm.so+0xc6c0a2]  JavaCalls::call(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0xaa
V  [libjvm.so+0xc6b1b3]  JavaCalls::call_virtual(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)+0x1f1
V  [libjvm.so+0xc6b3f0]  JavaCalls::call_virtual(JavaValue*, Handle, KlassHandle, Symbol*, Symbol*, Handle, Thread*)+0xd2
V  [libjvm.so+0x1201c56]  SystemDictionary::load_instance_class(Symbol*, Handle, Thread*)+0x93c
V  [libjvm.so+0x11feda0]  SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, Thread*)+0x8c0
V  [libjvm.so+0x11fd298]  SystemDictionary::resolve_or_null(Symbol*, Handle, Handle, Thread*)+0x262
V  [libjvm.so+0x11fcca3]  SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, Thread*)+0x45
V  [libjvm.so+0x1286586]  VerificationType::resolve_and_check_assignability(instanceKlassHandle, Symbol*, Symbol*, bool, bool, bool, Thread*)+0x258
V  [libjvm.so+0x128682e]  VerificationType::is_reference_assignable_from(VerificationType const&, ClassVerifier*, bool, Thread*) const+0x212
V  [libjvm.so+0x118fa55]  VerificationType::is_assignable_from(VerificationType const&, ClassVerifier*, bool, Thread*) const+0x191
V  [libjvm.so+0x129a98f]  StackMapFrame::pop_stack(VerificationType, Thread*)+0x81
V  [libjvm.so+0x12978f3]  ClassVerifier::verify_invoke_instructions(RawBytecodeStream*, unsigned int, StackMapFrame*, bool, bool*, VerificationType, constantPoolHandle const&, StackMapTable*, Thread*)+0x1249
V  [libjvm.so+0x129096b]  ClassVerifier::verify_method(methodHandle const&, Thread*)+0x6b71
V  [libjvm.so+0x1289d1a]  ClassVerifier::verify_class(Thread*)+0x12a
V  [libjvm.so+0x1287c9e]  Verifier::verify(instanceKlassHandle, Verifier::Mode, bool, Thread*)+0x2c0
V  [libjvm.so+0xc2b9b0]  InstanceKlass::verify_code(instanceKlassHandle, bool, Thread*)+0x7c
V  [libjvm.so+0xc2c228]  InstanceKlass::link_class_impl(instanceKlassHandle, bool, Thread*)+0x55e
V  [libjvm.so+0xc2bb3d]  InstanceKlass::link_class(Thread*)+0xdf
V  [libjvm.so+0xcf5f80]  get_class_declared_methods_helper(JNIEnv_*, _jclass*, unsigned char, bool, Klass*, Thread*)+0x155
V  [libjvm.so+0xcf6570]  JVM_GetClassDeclaredMethods+0x1d9
j  java.lang.Class.getDeclaredMethods0(Z)[Ljava/lang/reflect/Method;+0 java.base@9-internal
j  java.lang.Class.privateGetDeclaredMethods(Z)[Ljava/lang/reflect/Method;+34 java.base@9-internal
j  java.lang.Class.getMethodsRecursive(Ljava/lang/String;[Ljava/lang/Class;Z)Ljava/lang/PublicMethods$MethodList;+2 java.base@9-internal
j  java.lang.Class.getMethod0(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;+14 java.base@9-internal
j  java.lang.Class.getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;+26 java.base@9-internal
j  sun.launcher.LauncherHelper.validateMainClass(Ljava/lang/Class;)V+12 java.base@9-internal
j  sun.launcher.LauncherHelper.checkAndLoadMain(ZILjava/lang/String;)Ljava/lang/Class;+54 java.base@9-internal

This check succeeds, because class-loading is triggered from sun.launcher.LauncherHelper which is in the 'java.base' package, just like the 'com.sun.crypto.provider.SunJCE' package.

But when running with a security manager, there's also a second check, which is performed in the VM by 'SystemDictionary::resolve_instance_class_or_null()', right before it returns the loaded class, by calling 'SystemDictionary::validate_protection_domain()'. This method in turn calls 'java.lang.ClassLoader.checkPackageAccess()' to validate the package access:

    // Invoked by the VM after loading class with this loader.
    private void checkPackageAccess(Class cls, ProtectionDomain pd) {
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ...
            final String name = cls.getName();
            final int i = name.lastIndexOf('.');
            if (i != -1) {
                AccessController.doPrivileged(new PrivilegedAction<>() {
                    public Void run() {
                        sm.checkPackageAccess(name.substring(0, i));
                        return null;
                    }
                }, new AccessControlContext(new ProtectionDomain[] {pd}));
            }
        }
    }

This check is done with the protection domain of the initial class under verification ('Tricky' in our case) which is from the unnamed module and doesn't have the rights to access the unexported classes from java.base. Therefor the check fails the second time. Here's the corresponding VM stack trace:

j  java.lang.ClassLoader.checkPackageAccess(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V+106 java.base@9-internal
v  ~StubRoutines::call_stub
V  [libjvm.so+0xc6c75d]  JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0x6a5
V  [libjvm.so+0x1055bab]  os::os_exception_wrapper(void (*)(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*), JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0x41
V  [libjvm.so+0xc6c0a2]  JavaCalls::call(JavaValue*, methodHandle const&, JavaCallArguments*, Thread*)+0xaa
V  [libjvm.so+0xc6b6a9]  JavaCalls::call_special(JavaValue*, KlassHandle, Symbol*, Symbol*, JavaCallArguments*, Thread*)+0x173
V  [libjvm.so+0xc6ba04]  JavaCalls::call_special(JavaValue*, Handle, KlassHandle, Symbol*, Symbol*, Handle, Handle, Thread*)+0xf6
V  [libjvm.so+0x11fdcb0]  SystemDictionary::validate_protection_domain(instanceKlassHandle, Handle, Handle, Thread*)+0x22e
V  [libjvm.so+0x11ff40a]  SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, Thread*)+0xf2a
V  [libjvm.so+0x11fd298]  SystemDictionary::resolve_or_null(Symbol*, Handle, Handle, Thread*)+0x262
V  [libjvm.so+0x11fcca3]  SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, Thread*)+0x45
V  [libjvm.so+0x1286586]  VerificationType::resolve_and_check_assignability(instanceKlassHandle, Symbol*, Symbol*, bool, bool, bool, Thread*)+0x258
V  [libjvm.so+0x128682e]  VerificationType::is_reference_assignable_from(VerificationType const&, ClassVerifier*, bool, Thread*) const+0x212
V  [libjvm.so+0x118fa55]  VerificationType::is_assignable_from(VerificationType const&, ClassVerifier*, bool, Thread*) const+0x191
V  [libjvm.so+0x129a98f]  StackMapFrame::pop_stack(VerificationType, Thread*)+0x81
V  [libjvm.so+0x12978f3]  ClassVerifier::verify_invoke_instructions(RawBytecodeStream*, unsigned int, StackMapFrame*, bool, bool*, VerificationType, constantPoolHandle const&, StackMapTable*, Thread*)+0x1249
V  [libjvm.so+0x129096b]  ClassVerifier::verify_method(methodHandle const&, Thread*)+0x6b71
V  [libjvm.so+0x1289d1a]  ClassVerifier::verify_class(Thread*)+0x12a
V  [libjvm.so+0x1287c9e]  Verifier::verify(instanceKlassHandle, Verifier::Mode, bool, Thread*)+0x2c0
V  [libjvm.so+0xc2b9b0]  InstanceKlass::verify_code(instanceKlassHandle, bool, Thread*)+0x7c
V  [libjvm.so+0xc2c228]  InstanceKlass::link_class_impl(instanceKlassHandle, bool, Thread*)+0x55e
V  [libjvm.so+0xc2bb3d]  InstanceKlass::link_class(Thread*)+0xdf
V  [libjvm.so+0xcf5f80]  get_class_declared_methods_helper(JNIEnv_*, _jclass*, unsigned char, bool, Klass*, Thread*)+0x155
V  [libjvm.so+0xcf6570]  JVM_GetClassDeclaredMethods+0x1d9
j  java.lang.Class.getDeclaredMethods0(Z)[Ljava/lang/reflect/Method;+0 java.base@9-internal
j  java.lang.Class.privateGetDeclaredMethods(Z)[Ljava/lang/reflect/Method;+34 java.base@9-internal
j  java.lang.Class.getMethodsRecursive(Ljava/lang/String;[Ljava/lang/Class;Z)Ljava/lang/PublicMethods$MethodList;+2 java.base@9-internal
j  java.lang.Class.getMethod0(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;+14 java.base@9-internal
j  java.lang.Class.getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;+26 java.base@9-internal
j  sun.launcher.LauncherHelper.validateMainClass(Ljava/lang/Class;)V+12 java.base@9-internal
j  sun.launcher.LauncherHelper.checkAndLoadMain(ZILjava/lang/String;)Ljava/lang/Class;+54 java.base@9-internal

It is clear that the verifier sometimes has to load classes in order to accomplish its duty, even if these classes won't be used later on, at run-time.

But the question remains if it is OK for an application to fail just because the verifier is unable to load classes required for verification because of security manager restrictions or if the verifier should run with higher privileges which allow such accesses?

If the answer to this question is "Yes" (i.e. it's OK to fail), the consequence of running with a security manager will be that '--add-exports/--add-opens/--illegal-access=permit' may be not sharp enough knifes to achieve Java 8 backward compatibility. We may also have to grant some additional security permissions to our application classes.

If the answer will be "No" (i.e. it's not OK to fail in the verifier) we may have to fix the VM to elevate the permissions used to load classes from within the verifier.

Any comments?

Thank you and best regards,
Volker

[1] https://bugs.openjdk.java.net/browse/JDK-8174766
[2] http://mail.openjdk.java.net/pipermail/security-dev/2017-January/thread.html#15416
[3] https://bugs.openjdk.java.net/browse/JDK-8055206