Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8205330

InitialDirContext ctor sometimes throws NPE if the server has sent a disconnection

XMLWordPrintable

    • b11
    • x86_64
    • linux
    • Not verified

        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


              vtewari Vyom Tewari
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              11 Start watching this issue

                Created:
                Updated:
                Resolved: