JDK-8205330 : InitialDirContext ctor sometimes throws NPE if the server has sent a disconnection
  • Type: Bug
  • Component: core-libs
  • Sub-Component: javax.naming
  • Affected Version: 8,10,11
  • Priority: P2
  • Status: Closed
  • Resolution: Fixed
  • OS: linux
  • CPU: x86_64
  • Submitted: 2018-06-16
  • Updated: 2019-03-19
  • Resolved: 2018-09-11
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 12 JDK 8 Other
11.0.2Fixed 12 b11Fixed 8u201Fixed openjdk7uFixed
Related Reports
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
Reproduced on two separate systems:

Ubuntu 16.04.4 LTS -:
* Java 1.8.0_171-8u171
* Java 18.3 (build 10.0.1+10)

Mac OS X 10.13.5
* Java 1.8.0_171-b1



A DESCRIPTION OF THE PROBLEM :
If a connection has already been established and then the LDAP directory server sends an (unsolicited) "Notice of Disconnection", the  client's processing of this LDAP message can race with an application thread calling  new InitialDirContext() to authenticate user credentials (i.e.bind).  There is an unlucky timing which leads to a NullPointerException, which is in contravention of InitialDirContext API.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the program provided below.

The program (Main):

1) starts an embedded ldap server which additionally produces "Notice of Disconnection" LDAP messages after each successful bind.
2) starts a client using the java.naming API which authenticates (binds) in a loop


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Program should run indefinetly without failure.
ACTUAL -
Program fails with the following NullPointerException:

java.lang.NullPointerException
	at com.sun.jndi.ldap.LdapClient.authenticate(LdapClient.java:300)
	at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2791)
	at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:319)
	at com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:192)
	at com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:210)
	at com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:153)
	at com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:83)
	at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:684)
	at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:313)
	at javax.naming.InitialContext.init(InitialContext.java:244)
	at javax.naming.InitialContext.<init>(InitialContext.java:216)
	at javax.naming.directory.InitialDirContext.<init>(InitialDirContext.java:101)
	at LdapConnector.bind(LdapConnector.java:48)
	at Main.main(Main.java:42)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:282)
	at java.lang.Thread.run(Thread.java:748)

---------- BEGIN SOURCE ----------
The program is available here:  https://github.com/k-wall/ldapinitialdircontextnpe

The program comprises three classes:

Main:
====

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main
{
    private static final Logger LOG = LoggerFactory.getLogger(Main.class);

    public static void main(String[] argv) throws Exception
    {
        int successfulAuthCount = 0;
        try(EmbeddedLdapServer directory = new EmbeddedLdapServer("o=sevenSeas"))
        {
            directory.start();
            directory.applyLdif("test.ldif");

            LdapConnector ldapConnector = new LdapConnector("localhost", directory.getBoundPort());
            String principal = "cn=Horatio Hornblower,ou=people,o=sevenSeas";
            String secret = "secret";

            while(true)
            {
                ldapConnector.bind(principal, secret);
                successfulAuthCount++;
                if (successfulAuthCount % 100 == 0)
                {
                    LOG.info("Successfully LDAP binds {}", successfulAuthCount);
                }
            }
        }
        finally
        {
            LOG.info("Number of successful LDAP binds {}", successfulAuthCount);
        }
    }
}

LdapConnector:
=============
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.InitialDirContext;

public class LdapConnector {

    private final String _host;
    private final int _port;

    LdapConnector(final String host, final int port) {
        _host = host;
        _port = port;
    }

    public void bind(final String principal, final String password) throws NamingException
    {
        Hashtable<String, Object> env = new Hashtable<>();
        env.put("com.sun.jndi.ldap.connect.pool", "true");
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, String.format("ldap://%s:%d/", _host, _port));
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, principal);
        env.put(Context.SECURITY_CREDENTIALS, password);
        //env.put("com.sun.jndi.ldap.trace.ber", System.err);

        InitialDirContext initialDirContext = new InitialDirContext(env);
        initialDirContext.close();
    }
}


import java.io.File;
import java.net.InetSocketAddress;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;

import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.extended.NoticeOfDisconnect;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.InstanceLayout;
import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
import org.apache.directory.server.core.partition.impl.avl.AvlPartition;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.LdapSession;
import org.apache.directory.server.ldap.LdapSessionManager;
import org.apache.directory.server.ldap.handlers.request.BindRequestHandler;
import org.apache.directory.server.ldap.handlers.response.BindResponseHandler;
import org.apache.directory.server.protocol.shared.store.LdifFileLoader;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.mina.core.session.IoSession;

public class EmbeddedLdapServer implements AutoCloseable
{
    private static final String INSTANCE_NAME = "sevenSeas";

    private DirectoryService _directoryService;
    private LdapServer _ldapService;

    private int _boundPort;

    public EmbeddedLdapServer(final String baseDn) throws Exception
    {
        init(baseDn);
    }

    private void init(final String baseDn) throws Exception
    {
        DefaultDirectoryServiceFactory factory = new DefaultDirectoryServiceFactory();
        factory.init(INSTANCE_NAME);

        _directoryService = factory.getDirectoryService();
        _directoryService.getChangeLog().setEnabled(false);

        File dir =  Files.createTempDirectory(INSTANCE_NAME).toFile();
        InstanceLayout il = new InstanceLayout(dir);
        _directoryService.setInstanceLayout(il);

        AvlPartition partition = new AvlPartition(_directoryService.getSchemaManager());
        partition.setId(INSTANCE_NAME);
        partition.setSuffixDn(new Dn(_directoryService.getSchemaManager(), baseDn));
        _directoryService.addPartition(partition);


        _ldapService = new LdapServer();
        _ldapService.setTransports(new TcpTransport(0));
        _ldapService.setDirectoryService(_directoryService);

        _ldapService.setBindHandlers(new BindRequestHandler(), new BindResponseHandler()
        {
            @Override
            public void handle(final LdapSession ldapSession, final BindResponse bindResponse)
            {
                // Produce a 1.3.6.1.4.1.1466.20036 (notice of disconnect).
                final IoSession ioSession = ldapSession.getIoSession();
                final LdapSessionManager ldapSessionManager = ldapSession.getLdapServer().getLdapSessionManager();
                ioSession.write(NoticeOfDisconnect.UNAVAILABLE);
                ldapSessionManager.removeLdapSession(ioSession);
            }
        });
   }

    public int getBoundPort()
    {
        return _boundPort;
    }

    public void start() throws Exception
    {
        if (_ldapService.isStarted())
        {
            throw new IllegalStateException("Service already running");
        }

        _directoryService.startup();
        _ldapService.start();
        _boundPort = ((InetSocketAddress) _ldapService.getTransports()[0].getAcceptor().getLocalAddress()).getPort();

    }

    public void applyLdif(final String ldif) throws Exception
    {
        File tmp = File.createTempFile("test", ".ldif");
        Files.copy(Thread.currentThread().getContextClassLoader().getResourceAsStream(ldif), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING);

        LdifFileLoader ldifFileLoader = new LdifFileLoader(_directoryService.getAdminSession(), tmp.getAbsolutePath());
        int rv = ldifFileLoader.execute();
        if (rv == 0)
        {
            throw new IllegalStateException(String.format("Load no entries from LDIF resource : '%s'", ldif));
        }
    }

    @Override
    public void close() throws Exception
    {
        if (!_ldapService.isStarted())
        {
            throw new IllegalStateException("Service is not running");
        }

        try
        {
            _ldapService.stop();
        }
        finally
        {
            _directoryService.shutdown();
        }
    }
}

test.ldif:
======

version: 1

dn: o=sevenSeas
objectClass: organization
objectClass: top
o: sevenSeas

dn: ou=people,o=sevenSeas
objectClass: organizationalUnit
objectClass: top
description: Contains entries which describe persons (seamen)
ou: people

dn: cn=Horatio Hornblower,ou=people,o=sevenSeas
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: top
cn: Horatio Hornblower
description: Capt. Horatio Hornblower, R.N
givenName: Horatio
sn: Hornblower
uid: hhornblo
mail: hhornblo@royalnavy.mod.uk
userPassword: secret

Maven dependencies:

    <dependencies>
        <dependency>
            <groupId>org.apache.directory.server</groupId>
            <artifactId>apacheds-all</artifactId>
            <version>${apache.ds.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.2</version>
        </dependency>
    </dependencies>

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

CUSTOMER SUBMITTED WORKAROUND :
None found.  Restart of the application appears to be the only recourse.

FREQUENCY : often



Comments
Adding `noreg-hard` label, since a regression test would require a server, or at the very minimum a third-party library providing such.
11-09-2018

i reproduce this issue at my local env, and there is race in LdapClient , we need to handle connection close gracefully. I check the code and and we do call ensureOpen() to make sure the underline connection is open but this does not prevent race in code.
27-06-2018

Tested on ubuntu 14.0.1 with the test case provided in the bug report. JDK 8u171 - Fail JDK 10.0.1+10 - Fail JDK 11- Fail java.lang.NullPointerException at java.naming/com.sun.jndi.ldap.LdapClient.authenticate(LdapClient.java:300) at java.naming/com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2795) at java.naming/com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:320) at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:192) at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:210) at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:153) at java.naming/com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:83) at java.naming/javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:730) at java.naming/javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:305) at java.naming/javax.naming.InitialContext.init(InitialContext.java:236) at java.naming/javax.naming.InitialContext.<init>(InitialContext.java:208) at java.naming/javax.naming.directory.InitialDirContext.<init>(InitialDirContext.java:101) at LdapConnector.bind(LdapConnector.java:48) at Main.main(Main.java:42) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:282) at java.base/java.lang.Thread.run(Thread.java:832)
19-06-2018