JDK-6771287 : JVM sometimes exits before all non-daemon threads quit
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.lang
  • Affected Version: 6u7,6u10
  • Priority: P3
  • Status: Open
  • Resolution: Unresolved
  • OS: windows_xp
  • CPU: x86
  • Submitted: 2008-11-13
  • Updated: 2022-06-19
Related Reports
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.6.0_10-rc2"
Java(TM) SE Runtime Environment (build 1.6.0_10-rc2-b32)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]

A DESCRIPTION OF THE PROBLEM :
I reported this a few days ago, but I didn't realize how serious this was, and I have a much better example for reproducing it now.

Setting the JVM's SecurityManager may cause the JVM to quit before all non-daemon threads have quit.  I'm unsure of exactly what the parameters are that cause this, but for a given SecurityManager, it will seemingly always happen, or never happen.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run test case below.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Should print out some data from the SecurityManager, then slowly print the numbers 1-1000.
ACTUAL -
Prints out data from the SecurityManager, then prints "0" (sometimes), and quits.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
package org.thedreaming.test;

import java.io.FileDescriptor;
import java.net.InetAddress;
import java.security.Permission;


public class SecurityManagerTest {

  public static void main(String[] args) {
    
    final SecurityManager securityManager = new SecurityManager() {
      @Override
      public void checkMemberAccess(Class<?> clazz, int which) {
        System.out.println("" + Thread.currentThread() + ": Checking access for " + clazz + " for " + which);
      }
      
      @Override
      public void checkPackageAccess(String pkg){
        System.out.println("" + Thread.currentThread() + ": Checking access for package " + pkg);
      }

      @Override
      public void checkAccept(String host, int port) {
        System.out.println("" + Thread.currentThread() + ": Accept");
      }

      @Override
      public void checkAccess(Thread t) {
        System.out.println("" + Thread.currentThread() + ": Access " + t);
      }

      @Override
      public void checkAccess(ThreadGroup g) {
        System.out.println("" + Thread.currentThread() + ": Access " + g);
      }

      @Override
      public void checkAwtEventQueueAccess() {
        System.out.println("" + Thread.currentThread() + ": AWT");
      }

      @Override
      public void checkConnect(String host, int port, Object context) {
        System.out.println("" + Thread.currentThread() + ": Connect for context");
      }

      @Override
      public void checkConnect(String host, int port) {
        System.out.println("" + Thread.currentThread() + ": Connect");
      }

      @Override
      public void checkCreateClassLoader() {
        System.out.println("" + Thread.currentThread() + ": Create class loader");
      }

      @Override
      public void checkDelete(String file) {
        System.out.println("" + Thread.currentThread() + ": Checking delete");
      }

      @Override
      public void checkExec(String cmd) {
        System.out.println("" + Thread.currentThread() + ": Checking exec");
      }

      @Override
      public void checkExit(int status) {
        super.checkExit(status);
      }

      @Override
      public void checkLink(String lib) {
        System.out.println("" + Thread.currentThread() + ": Checking link");
      }

      @Override
      public void checkListen(int port) {
        System.out.println("" + Thread.currentThread() + ": Checking listen");
      }

      @Override
      public void checkMulticast(InetAddress maddr) {
        System.out.println("" + Thread.currentThread() + ": Checking multicast");
      }

      @Override
      public void checkPackageDefinition(String pkg) {
        System.out.println("" + Thread.currentThread() + ": Checking pkg def");
      }

      @Override
      public void checkPermission(Permission perm, Object context) {
        System.out.println("" + Thread.currentThread() + ": Checking permission " + perm + " for context");
      }

      @Override
      public void checkPermission(Permission perm) {
        System.out.println("" + Thread.currentThread() + ": Checking permission " + perm);
      }

      @Override
      public void checkPrintJobAccess() {
        System.out.println("" + Thread.currentThread() + ": Checking print job");
      }

      @Override
      public void checkPropertiesAccess() {
        System.out.println("" + Thread.currentThread() + ": Checking property access");
      }

      @Override
      public void checkPropertyAccess(String key) {
        System.out.println("" + Thread.currentThread() + ": Checking property access: " + key);
      }

      @Override
      public void checkRead(FileDescriptor fd) {
        System.out.println("" + Thread.currentThread() + ": Checking read fd " + fd);
      }

      @Override
      public void checkRead(String file, Object context) {
        System.out.println("" + Thread.currentThread() + ": Checking read for context");
      }

      @Override
      public void checkRead(String file) {
        System.out.println("" + Thread.currentThread() + ": Checking read " + file);
      }

      @Override
      public void checkSecurityAccess(String target) {
        System.out.println("" + Thread.currentThread() + ": Checking security access");
      }

      @Override
      public void checkSetFactory() {
        System.out.println("" + Thread.currentThread() + ": Checking set factory");
      }

      @Override
      public void checkSystemClipboardAccess() {
        System.out.println("" + Thread.currentThread() + ": Checking clipboard access");
      }

      @Override
      public boolean checkTopLevelWindow(Object window) {
        System.out.println("" + Thread.currentThread() + ": Checking top level window");
        return super.checkTopLevelWindow(window);
      }

      @Override
      public void checkWrite(FileDescriptor fd) {
        System.out.println("" + Thread.currentThread() + ": Checking write fd " + fd);
      }

      @Override
      public void checkWrite(String file) {
        System.out.println("" + Thread.currentThread() + ": Checking write " + file);
      }
    };
    Thread t = new Thread() {
      public void run() {
        System.setSecurityManager(securityManager);
        
      }
    };
    t.start();
    try {
      t.join();
    } catch (Exception e) {
      e.printStackTrace();
    }
    
    Runnable runnable = new Runnable() {
      public void run() {
        for(int i = 0; i < 1000; i++) {
          System.out.println("" + i);
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            System.out.println("Interrupted");
          }
        }
        System.out.println("Hello World");
        System.out.flush();
      }
    };
    
    Thread thread = new Thread(runnable);
    thread.start();
    System.out.println("Started " + thread.getName() + " " + thread.isDaemon() + " " + thread.isAlive());
  }
  
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
If you can keep track of all the threads you spawn, then "thread.join()" in the main thread for each child thread seems to fix this, but this is not an option in my case.

Comments
One other point to mention on this issue is that Thread construction is significantly changed in JDK 19. If a native thread attaches with JNI AttachCurrentThread then it will not call into a custom security manager's checkAccess method with a partly initialized Thread. This was agreed after security review. See also related issue JDK-8274668.
19-06-2022

Yes, I think should close this too.
02-09-2020

EVALUATION See Public Commments
19-06-2009

PUBLIC COMMENTS The test case can be reduced to just: public class SecurityManagerTest { public static void main(String[] args) { final SecurityManager securityManager = new SecurityManager() { @Override public void checkAccess(ThreadGroup g) { System.out.println("" + Thread.currentThread() + ": Access " + g); } }; System.setSecurityManager(securityManager); } } then run with a fastdebug build and -XX:+TraceExceptions and what you'll find is that "something else" behind the scenes is "crashing" due to a NullPointerException: Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} '<init>' '([C)V' in 'java/lang/String'> at bci 5 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'valueOf' '([C)Ljava/lang/String;' in 'java/lang/String'> at bci 5 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'getName' '()Ljava/lang/String;' in 'java/lang/Thread'> at bci 4 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'toString' '()Ljava/lang/String;' in 'java/lang/Thread'> at bci 74 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'valueOf' '(Ljava/lang/Object;)Ljava/lang/String;' in 'java/lang/String'> at bci 10 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'append' '(Ljava/lang/Object;)Ljava/lang/StringBuilder;' in 'java/lang/StringBuilder'> at bci 2 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'checkAccess' '(Ljava/lang/ThreadGroup;)V' in 'SecurityManagerTest$1'> at bci 18 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'checkAccess' '()V' in 'java/lang/ThreadGroup'> at bci 10 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} 'init' '(Ljava/lang/ThreadGroup;Ljava/lang/Runnable;Ljava/lang/String;J)V' in 'java/lang/Thread'> at bci 50 for thread 0x081f1000 Exception <a 'java/lang/NullPointerException'> (0xc2046118) thrown in interpreter method <{method} '<init>' '(Ljava/lang/ThreadGroup;Ljava/lang/String;)V' in 'java/lang/Thread'> at bci 45 for thread 0x081f1000 Something is trying to create a Thread using the Thread(ThreadGroup g, String n) constructor and when the checkaccess method tries to do it's println - or rather the string concatenation - it gets a NullPointerException when invoking toString() on the current thread. But how can that be? Looking at Thread.toString() we have: public String toString() { ThreadGroup group = getThreadGroup(); if (group != null) { return "Thread[" + getName() + "," + getPriority() + "," + group.getName() + "]"; } else { return "Thread[" + getName() + "," + getPriority() + "," + "" + "]"; } } and it calls getName() which calls String.valueOf(name), where name is a char[] which calls new String(name) which invokes name.length which throws NullPointerException. The problem is that at this stage of the Thread's construction, when checkAccess is called, the thread's name is not set - it is null and so getName().toString() throws NullPointerException. But wait - I hear you say - we're not doing this to the newly created Thread but to Thread.currentThread() so how can this be? There is one situation where this can be and that is when a native thread attaches to the JVM. There is a catch-22 here that to attach to the JVM the native thread needs a Java Thread instance, but to get a Java thread instance it needs to execute Java code, which can only be done by an attached thread. So what happens is that a Thread instance is "faked" for the newly attaching thread and it then calls the Thread constructor to effectively initialize itself. While that constructor executes Thread.currentThread() == this, which is why the getName().toString() fails with a NullPointerException. So where is this happening? When the launcher's main thread finishes the execution of the main method the native thread detaches itself from the VM and then reattaches as the DestroyJavaVM thread whicb is used to wait until all non-daemon threads have terminated and then terminate the JVM. But the attempt to reattach encounters the exception during construction of the Thread instance and so attaching fails and the native thread simply returns due to the error and the whole JVM process terminates - Poof! Is there a fix? Well the problem of how fragile thread attachment is, is well known, but very tricky to fix - see CR 6412693. Once you understand the problem then you can fix the SecurityManager to not do toString() on the current thread; or arrange for the SecurityManager to be installed by a seperate thread (as per the original test case) but after the main thread has detached and reattached (which is a little tricky: join() the main thread then sleep a little and hope it reattached.) In addition, in Hotspot, in jni.cpp in jni_DestroyJavaVM where we do: res = vm->AttachCurrentThread((void **)&env, (void *)&destroyargs); if (res != JNI_OK) { return res; } and silently return, we could instead print a warning that we failed to attach. Or perhaps better the launcher code that calls DestroyJavaVM and does this: /* * Wait for all non-daemon threads to end, then destroy the VM. * This will actually create a trivial new Java waiter thread * named "DestroyJavaVM", but this will be seen as a different * thread from the one that executed main, even though they are * the same C thread. This allows mainThread.join() and * mainThread.isAlive() to work as expected. */ (*vm)->DestroyJavaVM(vm); return ret; should actually check the return code of DestroyJavaVM and it should print a warning if an error occurred. So we already have a VM bug for this that's hard to fix; we should improve the launcher to detect failures here, and for good measure we could make Thread.toString() more robust by watching for having a null name. Anyway this isn't a bug in the SecurityManager so I'm redirecting to classes_lang.
19-06-2009

EVALUATION I have confirmed that this is a bug and the test case is reproduceable. I have also determined that the problem only occurs if you override the checkAccess(ThreadGroup) method of SecurityManager. Further analysis is required to determine what is causing the problem though.
20-11-2008