File tomcat-9.0-CVE-2021-30640.patch of Package tomcat.23021

Index: apache-tomcat-9.0.36-src/build.properties.default
===================================================================
--- apache-tomcat-9.0.36-src.orig/build.properties.default
+++ apache-tomcat-9.0.36-src/build.properties.default
@@ -248,6 +248,15 @@ objenesis.home=${base.path}/objenesis-${
 objenesis.jar=${objenesis.home}/objenesis-${objenesis.version}.jar
 objenesis.loc=${base-maven.loc}/org/objenesis/objenesis/${objenesis.version}/objenesis-${objenesis.version}.jar
 
+# ----- UnboundID, used by unit tests, version 5.1.4 or later -----
+unboundid.version=5.1.4
+unboundid.checksum.enabled=true
+unboundid.checksum.algorithm=SHA-512
+unboundid.checksum.value=04cf7f59eddebdd5b51e5be55021f9d9c667cca6101eac954e7a8d5b51f4c23372cd8f041640157f082435a166b75d85e79252b516130ede7d966dae6d3eae67
+unboundid.home=${base.path}/unboundid-${unboundid.version}
+unboundid.jar=${unboundid.home}/unboundid-ldapsdk-${unboundid.version}.jar
+unboundid.loc=${base-maven.loc}/com/unboundid/unboundid-ldapsdk/${unboundid.version}/unboundid-ldapsdk-${unboundid.version}.jar
+
 # ----- Checkstyle, version 6.16 or later -----
 checkstyle.version=8.22
 checkstyle.checksum.enabled=true
Index: apache-tomcat-9.0.36-src/build.xml
===================================================================
--- apache-tomcat-9.0.36-src.orig/build.xml
+++ apache-tomcat-9.0.36-src/build.xml
@@ -2862,6 +2862,15 @@ skip.installer property in build.propert
       <param name="checksum.value" value="${objenesis.checksum.value}"/>
     </antcall>
 
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${unboundid.loc}"/>
+      <param name="destfile" value="${unboundid.jar}"/>
+      <param name="destdir" value="${unboundid.home}"/>
+      <param name="checksum.enabled" value="${unboundid.checksum.enabled}"/>
+      <param name="checksum.algorithm" value="${unboundid.checksum.algorithm}"/>
+      <param name="checksum.value" value="${unboundid.checksum.value}"/>
+    </antcall>
+
   </target>
 
   <target name="download-cobertura"
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/JNDIRealm.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/realm/JNDIRealm.java
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/JNDIRealm.java
@@ -32,6 +32,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import javax.naming.AuthenticationException;
 import javax.naming.CommunicationException;
@@ -61,6 +63,7 @@ import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSocketFactory;
 
 import org.apache.catalina.LifecycleException;
+import org.apache.tomcat.util.collections.SynchronizedStack;
 import org.ietf.jgss.GSSCredential;
 import org.ietf.jgss.GSSName;
 
@@ -166,10 +169,6 @@ import org.ietf.jgss.GSSName;
  *     directory server itself.</li>
  * </ul>
  *
- * <p><strong>TODO</strong> - Support connection pooling (including message
- * format objects) so that <code>authenticate()</code> does not have to be
- * synchronized.</p>
- *
  * <p><strong>WARNING</strong> - There is a reported bug against the Netscape
  * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to
  * successfully authenticated a non-existing user. The
@@ -182,7 +181,6 @@ import org.ietf.jgss.GSSName;
  */
 public class JNDIRealm extends RealmBase {
 
-
     // ----------------------------------------------------- Instance Variables
 
     /**
@@ -195,25 +193,16 @@ public class JNDIRealm extends RealmBase
      */
     protected String connectionName = null;
 
-
     /**
      * The connection password for the server we will contact.
      */
     protected String connectionPassword = null;
 
-
     /**
      * The connection URL for the server we will contact.
      */
     protected String connectionURL = null;
 
-
-    /**
-     * The directory context linking us to our directory server.
-     */
-    protected DirContext context = null;
-
-
     /**
      * The JNDI context factory used to acquire our InitialContext.  By
      * default, assumes use of an LDAP server using the standard JNDI LDAP
@@ -221,7 +210,6 @@ public class JNDIRealm extends RealmBase
      */
     protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
 
-
     /**
      * How aliases should be dereferenced during search operations.
      */
@@ -233,14 +221,12 @@ public class JNDIRealm extends RealmBase
      */
     public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases";
 
-
     /**
      * The protocol that will be used in the communication with the
      * directory server.
      */
     protected String protocol = null;
 
-
     /**
      * Should we ignore PartialResultExceptions when iterating over NamingEnumerations?
      * Microsoft Active Directory often returns referrals, which lead
@@ -250,7 +236,6 @@ public class JNDIRealm extends RealmBase
      */
     protected boolean adCompat = false;
 
-
     /**
      * How should we handle referrals?  Microsoft Active Directory often returns
      * referrals. If you need to follow them set referrals to "follow".
@@ -259,20 +244,17 @@ public class JNDIRealm extends RealmBase
      */
     protected String referrals = null;
 
-
     /**
      * The base element for user searches.
      */
     protected String userBase = "";
 
-
     /**
      * The message format used to search for a user, with "{0}" marking
      * the spot where the username goes.
      */
     protected String userSearch = null;
 
-
     /**
      * When searching for users, should the search be performed as the user
      * currently being authenticated? If false, {@link #connectionName} and
@@ -281,20 +263,11 @@ public class JNDIRealm extends RealmBase
      */
     private boolean userSearchAsUser = false;
 
-
-    /**
-     * The MessageFormat object associated with the current
-     * <code>userSearch</code>.
-     */
-    protected MessageFormat userSearchFormat = null;
-
-
     /**
      * Should we search the entire subtree for matching users?
      */
     protected boolean userSubtree = false;
 
-
     /**
      * The attribute name used to retrieve the user password.
      */
@@ -308,7 +281,6 @@ public class JNDIRealm extends RealmBase
      */
     protected String userRoleAttribute = null;
 
-
     /**
      * A string of LDAP user patterns or paths, ":"-separated
      * These will be used to form the distinguished name of a
@@ -319,7 +291,6 @@ public class JNDIRealm extends RealmBase
      */
     protected String[] userPatternArray = null;
 
-
     /**
      * The message format used to form the distinguished name of a
      * user, with "{0}" marking the spot where the specified username
@@ -327,46 +298,22 @@ public class JNDIRealm extends RealmBase
      */
     protected String userPattern = null;
 
-
-    /**
-     * An array of MessageFormat objects associated with the current
-     * <code>userPatternArray</code>.
-     */
-    protected MessageFormat[] userPatternFormatArray = null;
-
     /**
      * The base element for role searches.
      */
     protected String roleBase = "";
 
-
-    /**
-     * The MessageFormat object associated with the current
-     * <code>roleBase</code>.
-     */
-    protected MessageFormat roleBaseFormat = null;
-
-
-    /**
-     * The MessageFormat object associated with the current
-     * <code>roleSearch</code>.
-     */
-    protected MessageFormat roleFormat = null;
-
-
     /**
      * The name of an attribute in the user's entry containing
      * roles for that user
      */
     protected String userRoleName = null;
 
-
     /**
      * The name of the attribute containing roles held elsewhere
      */
     protected String roleName = null;
 
-
     /**
      * The message format used to select roles for a user, with "{0}" marking
      * the spot where the distinguished name of the user goes. The "{1}"
@@ -374,7 +321,6 @@ public class JNDIRealm extends RealmBase
      */
     protected String roleSearch = null;
 
-
     /**
      * Should we search the entire subtree for matching memberships?
      */
@@ -409,7 +355,6 @@ public class JNDIRealm extends RealmBase
      */
     protected String commonRole = null;
 
-
     /**
      * The timeout, in milliseconds, to use when trying to create a connection
      * to the directory. The default is 5000 (5 seconds).
@@ -434,14 +379,12 @@ public class JNDIRealm extends RealmBase
      */
     protected int timeLimit = 0;
 
-
     /**
      * Should delegated credentials from the SPNEGO authenticator be used if
      * available
      */
     protected boolean useDelegatedCredential = true;
 
-
     /**
      * The QOP that should be used for the connection to the LDAP server after
      * authentication. This value is used to set the
@@ -499,6 +442,33 @@ public class JNDIRealm extends RealmBase
 
     private boolean forceDnHexEscape = false;
 
+    /**
+     * Non pooled connection to our directory server.
+     */
+    protected JNDIConnection singleConnection = new JNDIConnection();
+
+    /**
+     * The lock to ensure single connection thread safety.
+     */
+    protected final Lock singleConnectionLock = new ReentrantLock();
+
+    /**
+     * Connection pool.
+     */
+    protected SynchronizedStack<JNDIConnection> connectionPool = null;
+
+    /**
+     * The pool size limit. If 1, pooling is not used.
+     */
+    protected int connectionPoolSize = 1;
+
+    /**
+     * Whether to use context ClassLoader or default ClassLoader.
+     * True means use context ClassLoader, and True is the default
+     * value.
+     */
+    protected boolean useContextClassLoader = true;
+
 
     // ------------------------------------------------------------- Properties
 
@@ -506,37 +476,35 @@ public class JNDIRealm extends RealmBase
         return forceDnHexEscape;
     }
 
+
     public void setForceDnHexEscape(boolean forceDnHexEscape) {
         this.forceDnHexEscape = forceDnHexEscape;
     }
 
+
     /**
      * @return the type of authentication to use.
      */
     public String getAuthentication() {
-
         return authentication;
-
     }
 
+
     /**
      * Set the type of authentication to use.
      *
      * @param authentication The authentication
      */
     public void setAuthentication(String authentication) {
-
         this.authentication = authentication;
-
     }
 
+
     /**
      * @return the connection username for this Realm.
      */
     public String getConnectionName() {
-
         return this.connectionName;
-
     }
 
 
@@ -546,9 +514,7 @@ public class JNDIRealm extends RealmBase
      * @param connectionName The new connection username
      */
     public void setConnectionName(String connectionName) {
-
         this.connectionName = connectionName;
-
     }
 
 
@@ -556,9 +522,7 @@ public class JNDIRealm extends RealmBase
      * @return the connection password for this Realm.
      */
     public String getConnectionPassword() {
-
         return this.connectionPassword;
-
     }
 
 
@@ -568,9 +532,7 @@ public class JNDIRealm extends RealmBase
      * @param connectionPassword The new connection password
      */
     public void setConnectionPassword(String connectionPassword) {
-
         this.connectionPassword = connectionPassword;
-
     }
 
 
@@ -578,9 +540,7 @@ public class JNDIRealm extends RealmBase
      * @return the connection URL for this Realm.
      */
     public String getConnectionURL() {
-
         return this.connectionURL;
-
     }
 
 
@@ -590,9 +550,7 @@ public class JNDIRealm extends RealmBase
      * @param connectionURL The new connection URL
      */
     public void setConnectionURL(String connectionURL) {
-
         this.connectionURL = connectionURL;
-
     }
 
 
@@ -600,9 +558,7 @@ public class JNDIRealm extends RealmBase
      * @return the JNDI context factory for this Realm.
      */
     public String getContextFactory() {
-
         return this.contextFactory;
-
     }
 
 
@@ -612,11 +568,10 @@ public class JNDIRealm extends RealmBase
      * @param contextFactory The new context factory
      */
     public void setContextFactory(String contextFactory) {
-
         this.contextFactory = contextFactory;
-
     }
 
+
     /**
      * @return the derefAliases setting to be used.
      */
@@ -624,33 +579,32 @@ public class JNDIRealm extends RealmBase
         return derefAliases;
     }
 
+
     /**
      * Set the value for derefAliases to be used when searching the directory.
      *
      * @param derefAliases New value of property derefAliases.
      */
     public void setDerefAliases(java.lang.String derefAliases) {
-      this.derefAliases = derefAliases;
+        this.derefAliases = derefAliases;
     }
 
+
     /**
      * @return the protocol to be used.
      */
     public String getProtocol() {
-
         return protocol;
-
     }
 
+
     /**
      * Set the protocol for this Realm.
      *
      * @param protocol The new protocol.
      */
     public void setProtocol(String protocol) {
-
         this.protocol = protocol;
-
     }
 
 
@@ -694,9 +648,7 @@ public class JNDIRealm extends RealmBase
      * @return the base element for user searches.
      */
     public String getUserBase() {
-
         return this.userBase;
-
     }
 
 
@@ -706,9 +658,7 @@ public class JNDIRealm extends RealmBase
      * @param userBase The new base element
      */
     public void setUserBase(String userBase) {
-
         this.userBase = userBase;
-
     }
 
 
@@ -716,9 +666,7 @@ public class JNDIRealm extends RealmBase
      * @return the message format pattern for selecting users in this Realm.
      */
     public String getUserSearch() {
-
         return this.userSearch;
-
     }
 
 
@@ -728,13 +676,8 @@ public class JNDIRealm extends RealmBase
      * @param userSearch The new user search pattern
      */
     public void setUserSearch(String userSearch) {
-
         this.userSearch = userSearch;
-        if (userSearch == null)
-            userSearchFormat = null;
-        else
-            userSearchFormat = new MessageFormat(userSearch);
-
+        singleConnection = create();
     }
 
 
@@ -752,9 +695,7 @@ public class JNDIRealm extends RealmBase
      * @return the "search subtree for users" flag.
      */
     public boolean getUserSubtree() {
-
         return this.userSubtree;
-
     }
 
 
@@ -764,9 +705,7 @@ public class JNDIRealm extends RealmBase
      * @param userSubtree The new search flag
      */
     public void setUserSubtree(boolean userSubtree) {
-
         this.userSubtree = userSubtree;
-
     }
 
 
@@ -774,7 +713,6 @@ public class JNDIRealm extends RealmBase
      * @return the user role name attribute name for this Realm.
      */
     public String getUserRoleName() {
-
         return userRoleName;
     }
 
@@ -785,9 +723,7 @@ public class JNDIRealm extends RealmBase
      * @param userRoleName The new userRole name attribute name
      */
     public void setUserRoleName(String userRoleName) {
-
         this.userRoleName = userRoleName;
-
     }
 
 
@@ -795,9 +731,7 @@ public class JNDIRealm extends RealmBase
      * @return the base element for role searches.
      */
     public String getRoleBase() {
-
         return this.roleBase;
-
     }
 
 
@@ -807,13 +741,8 @@ public class JNDIRealm extends RealmBase
      * @param roleBase The new base element
      */
     public void setRoleBase(String roleBase) {
-
         this.roleBase = roleBase;
-        if (roleBase == null)
-            roleBaseFormat = null;
-        else
-            roleBaseFormat = new MessageFormat(roleBase);
-
+        singleConnection = create();
     }
 
 
@@ -821,9 +750,7 @@ public class JNDIRealm extends RealmBase
      * @return the role name attribute name for this Realm.
      */
     public String getRoleName() {
-
         return this.roleName;
-
     }
 
 
@@ -833,9 +760,7 @@ public class JNDIRealm extends RealmBase
      * @param roleName The new role name attribute name
      */
     public void setRoleName(String roleName) {
-
         this.roleName = roleName;
-
     }
 
 
@@ -843,9 +768,7 @@ public class JNDIRealm extends RealmBase
      * @return the message format pattern for selecting roles in this Realm.
      */
     public String getRoleSearch() {
-
         return this.roleSearch;
-
     }
 
 
@@ -855,13 +778,8 @@ public class JNDIRealm extends RealmBase
      * @param roleSearch The new role search pattern
      */
     public void setRoleSearch(String roleSearch) {
-
         this.roleSearch = roleSearch;
-        if (roleSearch == null)
-            roleFormat = null;
-        else
-            roleFormat = new MessageFormat(roleSearch);
-
+        singleConnection = create();
     }
 
 
@@ -879,9 +797,7 @@ public class JNDIRealm extends RealmBase
      * @return the "search subtree for roles" flag.
      */
     public boolean getRoleSubtree() {
-
         return this.roleSubtree;
-
     }
 
 
@@ -891,18 +807,15 @@ public class JNDIRealm extends RealmBase
      * @param roleSubtree The new search flag
      */
     public void setRoleSubtree(boolean roleSubtree) {
-
         this.roleSubtree = roleSubtree;
-
     }
 
+
     /**
      * @return the "The nested group search flag" flag.
      */
     public boolean getRoleNested() {
-
         return this.roleNested;
-
     }
 
 
@@ -912,9 +825,7 @@ public class JNDIRealm extends RealmBase
      * @param roleNested The nested group search flag
      */
     public void setRoleNested(boolean roleNested) {
-
         this.roleNested = roleNested;
-
     }
 
 
@@ -922,9 +833,7 @@ public class JNDIRealm extends RealmBase
      * @return the password attribute used to retrieve the user password.
      */
     public String getUserPassword() {
-
         return this.userPassword;
-
     }
 
 
@@ -934,9 +843,7 @@ public class JNDIRealm extends RealmBase
      * @param userPassword The new password attribute
      */
     public void setUserPassword(String userPassword) {
-
         this.userPassword = userPassword;
-
     }
 
 
@@ -944,6 +851,7 @@ public class JNDIRealm extends RealmBase
         return userRoleAttribute;
     }
 
+
     public void setUserRoleAttribute(String userRoleAttribute) {
         this.userRoleAttribute = userRoleAttribute;
     }
@@ -952,14 +860,10 @@ public class JNDIRealm extends RealmBase
      * @return the message format pattern for selecting users in this Realm.
      */
     public String getUserPattern() {
-
         return this.userPattern;
-
     }
 
 
-
-
     /**
      * Set the message format pattern for selecting users in this Realm.
      * This may be one simple pattern, or multiple patterns to be tried,
@@ -971,18 +875,12 @@ public class JNDIRealm extends RealmBase
      * @param userPattern The new user pattern
      */
     public void setUserPattern(String userPattern) {
-
         this.userPattern = userPattern;
-        if (userPattern == null)
+        if (userPattern == null) {
             userPatternArray = null;
-        else {
+        } else {
             userPatternArray = parseUserPatternString(userPattern);
-            int len = this.userPatternArray.length;
-            userPatternFormatArray = new MessageFormat[len];
-            for (int i=0; i < len; i++) {
-                userPatternFormatArray[i] =
-                    new MessageFormat(userPatternArray[i]);
-            }
+            singleConnection = create();
         }
     }
 
@@ -993,9 +891,7 @@ public class JNDIRealm extends RealmBase
      * @return Value of property alternateURL.
      */
     public String getAlternateURL() {
-
         return this.alternateURL;
-
     }
 
 
@@ -1005,9 +901,7 @@ public class JNDIRealm extends RealmBase
      * @param alternateURL New value of property alternateURL.
      */
     public void setAlternateURL(String alternateURL) {
-
         this.alternateURL = alternateURL;
-
     }
 
 
@@ -1015,9 +909,7 @@ public class JNDIRealm extends RealmBase
      * @return the common role
      */
     public String getCommonRole() {
-
         return commonRole;
-
     }
 
 
@@ -1027,9 +919,7 @@ public class JNDIRealm extends RealmBase
      * @param commonRole The common role
      */
     public void setCommonRole(String commonRole) {
-
         this.commonRole = commonRole;
-
     }
 
 
@@ -1037,9 +927,7 @@ public class JNDIRealm extends RealmBase
      * @return the connection timeout.
      */
     public String getConnectionTimeout() {
-
         return connectionTimeout;
-
     }
 
 
@@ -1049,18 +937,15 @@ public class JNDIRealm extends RealmBase
      * @param timeout The new connection timeout
      */
     public void setConnectionTimeout(String timeout) {
-
         this.connectionTimeout = timeout;
-
     }
 
+
     /**
      * @return the read timeout.
      */
     public String getReadTimeout() {
-
         return readTimeout;
-
     }
 
 
@@ -1070,9 +955,7 @@ public class JNDIRealm extends RealmBase
      * @param timeout The new read timeout
      */
     public void setReadTimeout(String timeout) {
-
         this.readTimeout = timeout;
-
     }
 
 
@@ -1100,6 +983,7 @@ public class JNDIRealm extends RealmBase
         return useDelegatedCredential;
     }
 
+
     public void setUseDelegatedCredential(boolean useDelegatedCredential) {
         this.useDelegatedCredential = useDelegatedCredential;
     }
@@ -1109,6 +993,7 @@ public class JNDIRealm extends RealmBase
         return spnegoDelegationQop;
     }
 
+
     public void setSpnegoDelegationQop(String spnegoDelegationQop) {
         this.spnegoDelegationQop = spnegoDelegationQop;
     }
@@ -1121,6 +1006,7 @@ public class JNDIRealm extends RealmBase
         return useStartTls;
     }
 
+
     /**
      * Flag whether StartTLS should be used when connecting to the ldap server
      *
@@ -1132,6 +1018,7 @@ public class JNDIRealm extends RealmBase
         this.useStartTls = useStartTls;
     }
 
+
     /**
      * @return list of the allowed cipher suites when connections are made using
      *         StartTLS
@@ -1151,6 +1038,7 @@ public class JNDIRealm extends RealmBase
         return this.cipherSuitesArray;
     }
 
+
     /**
      * Set the allowed cipher suites when opening a connection using StartTLS.
      * The cipher suites are expected as a comma separated list.
@@ -1162,6 +1050,25 @@ public class JNDIRealm extends RealmBase
         this.cipherSuites = suites;
     }
 
+
+    /**
+     * @return the connection pool size, or the default value 1 if pooling
+     *   is disabled
+     */
+    public int getConnectionPoolSize() {
+        return connectionPoolSize;
+    }
+
+
+    /**
+     * Set the connection pool size
+     * @param connectionPoolSize the new pool size
+     */
+    public void setConnectionPoolSize(int connectionPoolSize) {
+        this.connectionPoolSize = connectionPoolSize;
+    }
+
+
     /**
      * @return name of the {@link HostnameVerifier} class used for connections
      *         using StartTLS, or the empty string, if the default verifier
@@ -1174,6 +1081,7 @@ public class JNDIRealm extends RealmBase
         return this.hostnameVerifier.getClass().getCanonicalName();
     }
 
+
     /**
      * Set the {@link HostnameVerifier} to be used when opening connections
      * using StartTLS. An instance of the given class name will be constructed
@@ -1190,6 +1098,7 @@ public class JNDIRealm extends RealmBase
         }
     }
 
+
     /**
      * @return the {@link HostnameVerifier} to use for peer certificate
      *         verification when opening connections using StartTLS.
@@ -1198,8 +1107,7 @@ public class JNDIRealm extends RealmBase
         if (this.hostnameVerifier != null) {
             return this.hostnameVerifier;
         }
-        if (this.hostNameVerifierClassName == null
-                || hostNameVerifierClassName.equals("")) {
+        if (this.hostNameVerifierClassName == null || hostNameVerifierClassName.equals("")) {
             return null;
         }
         try {
@@ -1219,6 +1127,7 @@ public class JNDIRealm extends RealmBase
         }
     }
 
+
     /**
      * Set the {@link SSLSocketFactory} to be used when opening connections
      * using StartTLS. An instance of the factory with the given name will be
@@ -1232,6 +1141,7 @@ public class JNDIRealm extends RealmBase
         this.sslSocketFactoryClassName = factoryClassName;
     }
 
+
     /**
      * Set the ssl protocol to be used for connections using StartTLS.
      *
@@ -1242,6 +1152,7 @@ public class JNDIRealm extends RealmBase
         this.sslProtocol = protocol;
     }
 
+
     /**
      * @return the list of supported ssl protocols by the default
      *         {@link SSLContext}
@@ -1255,12 +1166,36 @@ public class JNDIRealm extends RealmBase
         }
     }
 
+
     private Object constructInstance(String className)
             throws ReflectiveOperationException {
         Class<?> clazz = Class.forName(className);
         return clazz.getConstructor().newInstance();
     }
 
+
+    /**
+     * Sets whether to use the context or default ClassLoader.
+     * True means use context ClassLoader.
+     *
+     * @param useContext True means use context ClassLoader
+     */
+    public void setUseContextClassLoader(boolean useContext) {
+        useContextClassLoader = useContext;
+    }
+
+
+    /**
+     * Returns whether to use the context or default ClassLoader.
+     * True means to use the context ClassLoader.
+     *
+     * @return The value of useContextClassLoader
+     */
+    public boolean isUseContextClassLoader() {
+        return useContextClassLoader;
+    }
+
+
     // ---------------------------------------------------------- Realm Methods
 
     /**
@@ -1280,20 +1215,21 @@ public class JNDIRealm extends RealmBase
     @Override
     public Principal authenticate(String username, String credentials) {
 
-        DirContext context = null;
+        JNDIConnection connection = null;
         Principal principal = null;
 
         try {
 
             // Ensure that we have a directory context available
-            context = open();
+            connection = get();
 
-            // Occasionally the directory context will timeout.  Try one more
-            // time before giving up.
             try {
 
+                // Occasionally the directory context will timeout.  Try one more
+                // time before giving up.
+
                 // Authenticate the specified username if possible
-                principal = authenticate(context, username, credentials);
+                principal = authenticate(connection, username, credentials);
 
             } catch (NullPointerException | NamingException e) {
                 /*
@@ -1315,19 +1251,19 @@ public class JNDIRealm extends RealmBase
                 containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
 
                 // close the connection so we know it will be reopened.
-                if (context != null)
-                    close(context);
+                close(connection);
+                closePooledConnections();
 
                 // open a new directory context.
-                context = open();
+                connection = get();
 
                 // Try the authentication again.
-                principal = authenticate(context, username, credentials);
+                principal = authenticate(connection, username, credentials);
             }
 
 
             // Release this context
-            release(context);
+            release(connection);
 
             // Return the authenticated Principal (if any)
             return principal;
@@ -1337,31 +1273,24 @@ public class JNDIRealm extends RealmBase
             // Log the problem for posterity
             containerLog.error(sm.getString("jndiRealm.exception"), e);
 
-            // Close the connection so that it gets reopened next time
-            if (context != null)
-                close(context);
+            // close the connection so we know it will be reopened.
+            close(connection);
+            closePooledConnections();
 
             // Return "not authenticated" for this request
-            if (containerLog.isDebugEnabled())
+            if (containerLog.isDebugEnabled()) {
                 containerLog.debug("Returning null principal.");
+            }
             return null;
-
         }
-
     }
 
 
-    // -------------------------------------------------------- Package Methods
-
-
-    // ------------------------------------------------------ Protected Methods
-
-
     /**
      * Return the Principal associated with the specified username and
      * credentials, if there is one; otherwise return <code>null</code>.
      *
-     * @param context The directory context
+     * @param connection The directory context
      * @param username Username of the Principal to look up
      * @param credentials Password or other credentials to use in
      *  authenticating this username
@@ -1369,30 +1298,26 @@ public class JNDIRealm extends RealmBase
      *
      * @exception NamingException if a directory server error occurs
      */
-    public synchronized Principal authenticate(DirContext context,
-                                               String username,
-                                               String credentials)
-        throws NamingException {
-
-        if (username == null || username.equals("")
-            || credentials == null || credentials.equals("")) {
-            if (containerLog.isDebugEnabled())
+    public Principal authenticate(JNDIConnection connection, String username, String credentials)
+            throws NamingException {
+
+        if (username == null || username.equals("") || credentials == null || credentials.equals("")) {
+            if (containerLog.isDebugEnabled()) {
                 containerLog.debug("username null or empty: returning null principal.");
+            }
             return null;
         }
 
         if (userPatternArray != null) {
-            for (int curUserPattern = 0;
-                 curUserPattern < userPatternFormatArray.length;
-                 curUserPattern++) {
+            for (int curUserPattern = 0; curUserPattern < userPatternArray.length; curUserPattern++) {
                 // Retrieve user information
-                User user = getUser(context, username, credentials, curUserPattern);
+                User user = getUser(connection, username, credentials, curUserPattern);
                 if (user != null) {
                     try {
                         // Check the user's credentials
-                        if (checkCredentials(context, user, credentials)) {
+                        if (checkCredentials(connection.context, user, credentials)) {
                             // Search for additional roles
-                            List<String> roles = getRoles(context, user);
+                            List<String> roles = getRoles(connection, user);
                             if (containerLog.isDebugEnabled()) {
                                 containerLog.debug("Found roles: " + roles.toString());
                             }
@@ -1411,16 +1336,18 @@ public class JNDIRealm extends RealmBase
             return null;
         } else {
             // Retrieve user information
-            User user = getUser(context, username, credentials);
-            if (user == null)
+            User user = getUser(connection, username, credentials);
+            if (user == null) {
                 return null;
+            }
 
             // Check the user's credentials
-            if (!checkCredentials(context, user, credentials))
+            if (!checkCredentials(connection.context, user, credentials)) {
                 return null;
+            }
 
             // Search for additional roles
-            List<String> roles = getRoles(context, user);
+            List<String> roles = getRoles(connection, user);
             if (containerLog.isDebugEnabled()) {
                 containerLog.debug("Found roles: " + roles.toString());
             }
@@ -1431,22 +1358,22 @@ public class JNDIRealm extends RealmBase
     }
 
 
+    // ------------------------------------------------------ Protected Methods
+
     /**
      * Return a User object containing information about the user
      * with the specified username, if found in the directory;
      * otherwise return <code>null</code>.
      *
-     * @param context The directory context
+     * @param connection The directory context
      * @param username Username to be looked up
      * @return the User object
      * @exception NamingException if a directory server error occurs
      *
-     * @see #getUser(DirContext, String, String, int)
+     * @see #getUser(JNDIConnection, String, String, int)
      */
-    protected User getUser(DirContext context, String username)
-        throws NamingException {
-
-        return getUser(context, username, null, -1);
+    protected User getUser(JNDIConnection connection, String username) throws NamingException {
+        return getUser(connection, username, null, -1);
     }
 
 
@@ -1455,18 +1382,16 @@ public class JNDIRealm extends RealmBase
      * with the specified username, if found in the directory;
      * otherwise return <code>null</code>.
      *
-     * @param context The directory context
+     * @param connection The directory context
      * @param username Username to be looked up
      * @param credentials User credentials (optional)
      * @return the User object
      * @exception NamingException if a directory server error occurs
      *
-     * @see #getUser(DirContext, String, String, int)
+     * @see #getUser(JNDIConnection, String, String, int)
      */
-    protected User getUser(DirContext context, String username, String credentials)
-        throws NamingException {
-
-        return getUser(context, username, credentials, -1);
+    protected User getUser(JNDIConnection connection, String username, String credentials) throws NamingException {
+        return getUser(connection, username, credentials, -1);
     }
 
 
@@ -1481,25 +1406,26 @@ public class JNDIRealm extends RealmBase
      * configuration attribute is specified, all values of that
      * attribute are retrieved from the directory entry.
      *
-     * @param context The directory context
+     * @param connection The directory context
      * @param username Username to be looked up
      * @param credentials User credentials (optional)
      * @param curUserPattern Index into userPatternFormatArray
      * @return the User object
      * @exception NamingException if a directory server error occurs
      */
-    protected User getUser(DirContext context, String username,
-                           String credentials, int curUserPattern)
-        throws NamingException {
+    protected User getUser(JNDIConnection connection, String username, String credentials, int curUserPattern)
+            throws NamingException {
 
         User user = null;
 
         // Get attributes to retrieve from user entry
         List<String> list = new ArrayList<>();
-        if (userPassword != null)
+        if (userPassword != null) {
             list.add(userPassword);
-        if (userRoleName != null)
+        }
+        if (userRoleName != null) {
             list.add(userRoleName);
+        }
         if (userRoleAttribute != null) {
             list.add(userRoleAttribute);
         }
@@ -1507,8 +1433,8 @@ public class JNDIRealm extends RealmBase
         list.toArray(attrIds);
 
         // Use pattern or search for user entry
-        if (userPatternFormatArray != null && curUserPattern >= 0) {
-            user = getUserByPattern(context, username, credentials, attrIds, curUserPattern);
+        if (userPatternArray != null && curUserPattern >= 0) {
+            user = getUserByPattern(connection, username, credentials, attrIds, curUserPattern);
             if (containerLog.isDebugEnabled()) {
                 containerLog.debug("Found user by pattern [" + user + "]");
             }
@@ -1516,12 +1442,12 @@ public class JNDIRealm extends RealmBase
             boolean thisUserSearchAsUser = isUserSearchAsUser();
             try {
                 if (thisUserSearchAsUser) {
-                    userCredentialsAdd(context, username, credentials);
+                    userCredentialsAdd(connection.context, username, credentials);
                 }
-                user = getUserBySearch(context, username, attrIds);
+                user = getUserBySearch(connection, username, attrIds);
             } finally {
                 if (thisUserSearchAsUser) {
-                    userCredentialsRemove(context);
+                    userCredentialsRemove(connection.context);
                 }
             }
             if (containerLog.isDebugEnabled()) {
@@ -1531,8 +1457,7 @@ public class JNDIRealm extends RealmBase
         if (userPassword == null && credentials != null && user != null) {
             // The password is available. Insert it since it may be required for
             // role searches.
-            return new User(user.getUserName(), user.getDN(), credentials,
-                    user.getRoles(), user.getUserRoleId());
+            return new User(user.getUserName(), user.getDN(), credentials, user.getRoles(), user.getUserRoleId());
         }
 
         return user;
@@ -1552,11 +1477,8 @@ public class JNDIRealm extends RealmBase
      * @return the User object
      * @exception NamingException if a directory server error occurs
      */
-    protected User getUserByPattern(DirContext context,
-                                    String username,
-                                    String[] attrIds,
-                                    String dn)
-        throws NamingException {
+    protected User getUserByPattern(DirContext context, String username, String[] attrIds, String dn)
+            throws NamingException {
 
         // If no attributes are requested, no need to look for them
         if (attrIds == null || attrIds.length == 0) {
@@ -1570,13 +1492,15 @@ public class JNDIRealm extends RealmBase
         } catch (NameNotFoundException e) {
             return null;
         }
-        if (attrs == null)
+        if (attrs == null) {
             return null;
+        }
 
         // Retrieve value of userPassword
         String password = null;
-        if (userPassword != null)
+        if (userPassword != null) {
             password = getAttributeValue(userPassword, attrs);
+        }
 
         String userRoleAttrValue = null;
         if (userRoleAttribute != null) {
@@ -1585,8 +1509,9 @@ public class JNDIRealm extends RealmBase
 
         // Retrieve values of userRoleName attribute
         ArrayList<String> roles = null;
-        if (userRoleName != null)
+        if (userRoleName != null) {
             roles = addAttributeValues(userRoleName, attrs, roles);
+        }
 
         return new User(username, dn, password, roles, userRoleAttrValue);
     }
@@ -1598,7 +1523,7 @@ public class JNDIRealm extends RealmBase
      * username and return a User object; otherwise return
      * <code>null</code>.
      *
-     * @param context The directory context
+     * @param connection The directory context
      * @param username The username
      * @param credentials User credentials (optional)
      * @param attrIds String[]containing names of attributes to
@@ -1607,34 +1532,34 @@ public class JNDIRealm extends RealmBase
      * @exception NamingException if a directory server error occurs
      * @see #getUserByPattern(DirContext, String, String[], String)
      */
-    protected User getUserByPattern(DirContext context,
-                                    String username,
-                                    String credentials,
-                                    String[] attrIds,
-                                    int curUserPattern)
-        throws NamingException {
+    protected User getUserByPattern(JNDIConnection connection, String username, String credentials, String[] attrIds,
+            int curUserPattern) throws NamingException {
 
         User user = null;
 
-        if (username == null || userPatternFormatArray[curUserPattern] == null)
+        if (username == null || userPatternArray[curUserPattern] == null) {
             return null;
+        }
 
-        // Form the dn from the user pattern
-        String dn = userPatternFormatArray[curUserPattern].format(new String[] { username });
+        // Form the DistinguishedName from the user pattern.
+        // Escape in case username contains a character with special meaning in
+        // an attribute value.
+        String dn = connection.userPatternFormatArray[curUserPattern].format(
+                new String[] { doAttributeValueEscaping(username) });
 
         try {
-            user = getUserByPattern(context, username, attrIds, dn);
+            user = getUserByPattern(connection.context, username, attrIds, dn);
         } catch (NameNotFoundException e) {
             return null;
         } catch (NamingException e) {
             // If the getUserByPattern() call fails, try it again with the
             // credentials of the user that we're searching for
             try {
-                userCredentialsAdd(context, dn, credentials);
+                userCredentialsAdd(connection.context, dn, credentials);
 
-                user = getUserByPattern(context, username, attrIds, dn);
+                user = getUserByPattern(connection.context, username, attrIds, dn);
             } finally {
-                userCredentialsRemove(context);
+                userCredentialsRemove(connection.context);
             }
         }
         return user;
@@ -1646,22 +1571,23 @@ public class JNDIRealm extends RealmBase
      * information about the user with the specified username, if
      * found in the directory; otherwise return <code>null</code>.
      *
-     * @param context The directory context
+     * @param connection The directory context
      * @param username The username
      * @param attrIds String[]containing names of attributes to retrieve.
      * @return the User object
      * @exception NamingException if a directory server error occurs
      */
-    protected User getUserBySearch(DirContext context,
-                                   String username,
-                                   String[] attrIds)
-        throws NamingException {
+    protected User getUserBySearch(JNDIConnection connection, String username, String[] attrIds)
+            throws NamingException {
 
-        if (username == null || userSearchFormat == null)
+        if (username == null || connection.userSearchFormat == null) {
             return null;
+        }
 
         // Form the search filter
-        String filter = userSearchFormat.format(new String[] { username });
+        // Escape in case username contains a character with special meaning in
+        // a search filter.
+        String filter = connection.userSearchFormat.format(new String[] { doFilterEscaping(username) });
 
         // Set up the search controls
         SearchControls constraints = new SearchControls();
@@ -1676,12 +1602,12 @@ public class JNDIRealm extends RealmBase
         constraints.setTimeLimit(timeLimit);
 
         // Specify the attributes to be retrieved
-        if (attrIds == null)
+        if (attrIds == null) {
             attrIds = new String[0];
+        }
         constraints.setReturningAttributes(attrIds);
 
-        NamingEnumeration<SearchResult> results =
-            context.search(userBase, filter, constraints);
+        NamingEnumeration<SearchResult> results = connection.context.search(userBase, filter, constraints);
 
         try {
             // Fail if no entries found
@@ -1690,10 +1616,11 @@ public class JNDIRealm extends RealmBase
                     return null;
                 }
             } catch (PartialResultException ex) {
-                if (!adCompat)
+                if (!adCompat) {
                     throw ex;
-                else
+                } else {
                     return null;
+                }
             }
 
             // Get result for the first entry found
@@ -1702,29 +1629,34 @@ public class JNDIRealm extends RealmBase
             // Check no further entries were found
             try {
                 if (results.hasMore()) {
-                    if(containerLog.isInfoEnabled())
-                        containerLog.info("username " + username + " has multiple entries");
+                    if (containerLog.isInfoEnabled()) {
+                        containerLog.info(sm.getString("jndiRealm.multipleEntries", username));
+                    }
                     return null;
                 }
             } catch (PartialResultException ex) {
-                if (!adCompat)
+                if (!adCompat) {
                     throw ex;
+                }
             }
 
-            String dn = getDistinguishedName(context, userBase, result);
+            String dn = getDistinguishedName(connection.context, userBase, result);
 
-            if (containerLog.isTraceEnabled())
+            if (containerLog.isTraceEnabled()) {
                 containerLog.trace("  entry found for " + username + " with dn " + dn);
+            }
 
             // Get the entry's attributes
             Attributes attrs = result.getAttributes();
-            if (attrs == null)
+            if (attrs == null) {
                 return null;
+            }
 
             // Retrieve value of userPassword
             String password = null;
-            if (userPassword != null)
+            if (userPassword != null) {
                 password = getAttributeValue(userPassword, attrs);
+            }
 
             String userRoleAttrValue = null;
             if (userRoleAttribute != null) {
@@ -1733,8 +1665,9 @@ public class JNDIRealm extends RealmBase
 
             // Retrieve values of userRoleName attribute
             ArrayList<String> roles = null;
-            if (userRoleName != null)
+            if (userRoleName != null) {
                 roles = addAttributeValues(userRoleName, attrs, roles);
+            }
 
             return new User(username, dn, password, roles, userRoleAttrValue);
         } finally {
@@ -1760,30 +1693,25 @@ public class JNDIRealm extends RealmBase
      * @return <code>true</code> if the credentials are validated
      * @exception NamingException if a directory server error occurs
      */
-    protected boolean checkCredentials(DirContext context,
-                                     User user,
-                                     String credentials)
-         throws NamingException {
-
-         boolean validated = false;
-
-         if (userPassword == null) {
-             validated = bindAsUser(context, user, credentials);
-         } else {
-             validated = compareCredentials(context, user, credentials);
-         }
-
-         if (containerLog.isTraceEnabled()) {
-             if (validated) {
-                 containerLog.trace(sm.getString("jndiRealm.authenticateSuccess",
-                                  user.getUserName()));
-             } else {
-                 containerLog.trace(sm.getString("jndiRealm.authenticateFailure",
-                                  user.getUserName()));
-             }
-         }
-         return validated;
-     }
+    protected boolean checkCredentials(DirContext context, User user, String credentials) throws NamingException {
+
+        boolean validated = false;
+
+        if (userPassword == null) {
+            validated = bindAsUser(context, user, credentials);
+        } else {
+            validated = compareCredentials(context, user, credentials);
+        }
+
+        if (containerLog.isTraceEnabled()) {
+            if (validated) {
+                containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", user.getUserName()));
+            } else {
+                containerLog.trace(sm.getString("jndiRealm.authenticateFailure", user.getUserName()));
+            }
+        }
+        return validated;
+    }
 
 
     /**
@@ -1796,17 +1724,15 @@ public class JNDIRealm extends RealmBase
      * @return <code>true</code> if the credentials are validated
      * @exception NamingException if a directory server error occurs
      */
-    protected boolean compareCredentials(DirContext context,
-                                         User info,
-                                         String credentials)
-        throws NamingException {
-
+    protected boolean compareCredentials(DirContext context, User info, String credentials) throws NamingException {
         // Validate the credentials specified by the user
-        if (containerLog.isTraceEnabled())
+        if (containerLog.isTraceEnabled()) {
             containerLog.trace("  validating credentials");
+        }
 
-        if (info == null || credentials == null)
+        if (info == null || credentials == null) {
             return false;
+        }
 
         String password = info.getPassword();
 
@@ -1823,21 +1749,22 @@ public class JNDIRealm extends RealmBase
      * @return <code>true</code> if the credentials are validated
      * @exception NamingException if a directory server error occurs
      */
-     protected boolean bindAsUser(DirContext context,
-                                  User user,
-                                  String credentials)
-         throws NamingException {
-
-         if (credentials == null || user == null)
-             return false;
-
-         String dn = user.getDN();
-         if (dn == null)
-             return false;
-
-         // Validate the credentials specified by the user
-         if (containerLog.isTraceEnabled()) {
-             containerLog.trace("  validating credentials by binding as the user");
+    protected boolean bindAsUser(DirContext context, User user, String credentials) throws NamingException {
+
+        if (credentials == null || user == null) {
+            return false;
+        }
+
+        // This is returned from the directory so will be attribute value
+        // escaped if required
+        String dn = user.getDN();
+        if (dn == null) {
+            return false;
+        }
+
+        // Validate the credentials specified by the user
+        if (containerLog.isTraceEnabled()) {
+            containerLog.trace("  validating credentials by binding as the user");
         }
 
         userCredentialsAdd(context, dn, credentials);
@@ -1862,74 +1789,79 @@ public class JNDIRealm extends RealmBase
         return validated;
     }
 
-     /**
-      * Configure the context to use the provided credentials for
-      * authentication.
-      *
-      * @param context      DirContext to configure
-      * @param dn           Distinguished name of user
-      * @param credentials  Credentials of user
-      * @exception NamingException if a directory server error occurs
-      */
-    private void userCredentialsAdd(DirContext context, String dn,
-            String credentials) throws NamingException {
+
+    /**
+     * Configure the context to use the provided credentials for
+     * authentication.
+     *
+     * @param context      DirContext to configure
+     * @param dn           Distinguished name of user
+     * @param credentials  Credentials of user
+     * @exception NamingException if a directory server error occurs
+     */
+    private void userCredentialsAdd(DirContext context, String dn, String credentials) throws NamingException {
         // Set up security environment to bind as the user
         context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
         context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
     }
 
+
     /**
      * Configure the context to use {@link #connectionName} and
      * {@link #connectionPassword} if specified or an anonymous connection if
      * those attributes are not specified.
      *
-      * @param context      DirContext to configure
-      * @exception NamingException if a directory server error occurs
+     * @param context      DirContext to configure
+     * @exception NamingException if a directory server error occurs
      */
-    private void userCredentialsRemove(DirContext context)
-            throws NamingException {
+    private void userCredentialsRemove(DirContext context) throws NamingException {
         // Restore the original security environment
         if (connectionName != null) {
-            context.addToEnvironment(Context.SECURITY_PRINCIPAL,
-                                     connectionName);
+            context.addToEnvironment(Context.SECURITY_PRINCIPAL, connectionName);
         } else {
             context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
         }
 
         if (connectionPassword != null) {
-            context.addToEnvironment(Context.SECURITY_CREDENTIALS,
-                                     connectionPassword);
+            context.addToEnvironment(Context.SECURITY_CREDENTIALS, connectionPassword);
         } else {
             context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
         }
     }
 
+
     /**
      * Return a List of roles associated with the given User.  Any
      * roles present in the user's directory entry are supplemented by
      * a directory search. If no roles are associated with this user,
      * a zero-length List is returned.
      *
-     * @param context The directory context we are searching
+     * @param connection The directory context we are searching
      * @param user The User to be checked
      * @return the list of role names
      * @exception NamingException if a directory server error occurs
      */
-    protected List<String> getRoles(DirContext context, User user)
-        throws NamingException {
+    protected List<String> getRoles(JNDIConnection connection, User user) throws NamingException {
 
-        if (user == null)
+        if (user == null) {
             return null;
+        }
 
+        // This is returned from the directory so will be attribute value
+        // escaped if required
         String dn = user.getDN();
+        // This is the name the user provided to the authentication process so
+        // it will not be escaped
         String username = user.getUserName();
         String userRoleId = user.getUserRoleId();
 
-        if (dn == null || username == null)
+        if (dn == null || username == null) {
             return null;
+        }
 
-        if (containerLog.isTraceEnabled())
+        if (containerLog.isTraceEnabled()) {
             containerLog.trace("  getRoles(" + dn + ")");
+        }
 
         // Start with roles retrieved from the user entry
         List<String> list = new ArrayList<>();
@@ -1937,8 +1869,9 @@ public class JNDIRealm extends RealmBase
         if (userRoles != null) {
             list.addAll(userRoles);
         }
-        if (commonRole != null)
+        if (commonRole != null) {
             list.add(commonRole);
+        }
 
         if (containerLog.isTraceEnabled()) {
             containerLog.trace("  Found " + list.size() + " user internal roles");
@@ -1946,54 +1879,66 @@ public class JNDIRealm extends RealmBase
         }
 
         // Are we configured to do role searches?
-        if ((roleFormat == null) || (roleName == null))
+        if ((connection.roleFormat == null) || (roleName == null)) {
             return list;
+        }
 
-        // Set up parameters for an appropriate search
-        String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId });
+        // Set up parameters for an appropriate search filter
+        // The dn is already attribute value escaped but the others are not
+        // This is a filter so all input will require filter escaping
+        String filter = connection.roleFormat.format(new String[] {
+                doFilterEscaping(dn),
+                doFilterEscaping(doAttributeValueEscaping(username)),
+                doFilterEscaping(doAttributeValueEscaping(userRoleId)) });
         SearchControls controls = new SearchControls();
-        if (roleSubtree)
+        if (roleSubtree) {
             controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
-        else
+        } else {
             controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+        }
         controls.setReturningAttributes(new String[] {roleName});
 
         String base = null;
-        if (roleBaseFormat != null) {
-            NameParser np = context.getNameParser("");
+        if (connection.roleBaseFormat != null) {
+            NameParser np = connection.context.getNameParser("");
             Name name = np.parse(dn);
             String nameParts[] = new String[name.size()];
             for (int i = 0; i < name.size(); i++) {
-                nameParts[i] = name.get(i);
+                // May have been returned with \<char> escaping rather than
+                // \<hex><hex>. Make sure it is \<hex><hex>.
+                nameParts[i] =  convertToHexEscape(name.get(i));
             }
-            base = roleBaseFormat.format(nameParts);
+            base = connection.roleBaseFormat.format(nameParts);
         } else {
             base = "";
         }
 
         // Perform the configured search and process the results
-        NamingEnumeration<SearchResult> results = searchAsUser(context, user, base, filter, controls,
+        NamingEnumeration<SearchResult> results = searchAsUser(connection.context, user, base, filter, controls,
                 isRoleSearchAsUser());
 
-        if (results == null)
+        if (results == null) {
             return list;  // Should never happen, but just in case ...
+        }
 
         Map<String, String> groupMap = new HashMap<>();
         try {
             while (results.hasMore()) {
                 SearchResult result = results.next();
                 Attributes attrs = result.getAttributes();
-                if (attrs == null)
+                if (attrs == null) {
                     continue;
-                String dname = getDistinguishedName(context, roleBase, result);
+                }
+                String dname = getDistinguishedName(connection.context, base, result);
                 String name = getAttributeValue(roleName, attrs);
                 if (name != null && dname != null) {
                     groupMap.put(dname, name);
                 }
             }
         } catch (PartialResultException ex) {
-            if (!adCompat)
+            if (!adCompat) {
                 throw ex;
+            }
         } finally {
             results.close();
         }
@@ -2018,23 +1963,29 @@ public class JNDIRealm extends RealmBase
                 Map<String, String> newThisRound = new HashMap<>(); // Stores the groups we find in this iteration
 
                 for (Entry<String, String> group : newGroups.entrySet()) {
-                    filter = roleFormat.format(new String[] { doRFC2254Encoding(group.getKey()),
-                            group.getValue(), group.getValue() });
+                    // Group key is already value escaped if required
+                    // Group value is not value escaped
+                    // Everything needs to be filter escaped
+                    filter = connection.roleFormat.format(new String[] {
+                            doFilterEscaping(group.getKey()),
+                            doFilterEscaping(doAttributeValueEscaping(group.getValue())),
+                            doFilterEscaping(doAttributeValueEscaping(group.getValue())) });
 
                     if (containerLog.isTraceEnabled()) {
-                        containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter);
+                        containerLog.trace("Perform a nested group search with base "+ roleBase +
+                                " and filter " + filter);
                     }
 
-                    results = searchAsUser(context, user, roleBase, filter, controls,
-                            isRoleSearchAsUser());
+                    results = searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser());
 
                     try {
                         while (results.hasMore()) {
                             SearchResult result = results.next();
                             Attributes attrs = result.getAttributes();
-                            if (attrs == null)
+                            if (attrs == null) {
                                 continue;
-                            String dname = getDistinguishedName(context, roleBase, result);
+                            }
+                            String dname = getDistinguishedName(connection.context, roleBase, result);
                             String name = getAttributeValue(roleName, attrs);
                             if (name != null && dname != null && !groupMap.keySet().contains(dname)) {
                                 groupMap.put(dname, name);
@@ -2043,12 +1994,12 @@ public class JNDIRealm extends RealmBase
                                 if (containerLog.isTraceEnabled()) {
                                     containerLog.trace("  Found nested role " + dname + " -> " + name);
                                 }
-
                             }
-                         }
+                        }
                     } catch (PartialResultException ex) {
-                        if (!adCompat)
+                        if (!adCompat) {
                             throw ex;
+                        }
                     } finally {
                         results.close();
                     }
@@ -2062,6 +2013,7 @@ public class JNDIRealm extends RealmBase
         return list;
     }
 
+
     /**
      * Perform the search on the context as the {@code dn}, when
      * {@code searchAsUser} is {@code true}, otherwise search the context with
@@ -2084,8 +2036,7 @@ public class JNDIRealm extends RealmBase
      * @throws NamingException
      *             if a directory server error occurs
      */
-    private NamingEnumeration<SearchResult> searchAsUser(DirContext context,
-            User user, String base, String filter,
+    private NamingEnumeration<SearchResult> searchAsUser(DirContext context, User user, String base, String filter,
             SearchControls controls, boolean searchAsUser) throws NamingException {
         NamingEnumeration<SearchResult> results;
         try {
@@ -2110,26 +2061,30 @@ public class JNDIRealm extends RealmBase
      * @return the attribute value
      * @exception NamingException if a directory server error occurs
      */
-    private String getAttributeValue(String attrId, Attributes attrs)
-        throws NamingException {
+    private String getAttributeValue(String attrId, Attributes attrs) throws NamingException {
 
-        if (containerLog.isTraceEnabled())
+        if (containerLog.isTraceEnabled()) {
             containerLog.trace("  retrieving attribute " + attrId);
+        }
 
-        if (attrId == null || attrs == null)
+        if (attrId == null || attrs == null) {
             return null;
+        }
 
         Attribute attr = attrs.get(attrId);
-        if (attr == null)
+        if (attr == null) {
             return null;
+        }
         Object value = attr.get();
-        if (value == null)
+        if (value == null) {
             return null;
+        }
         String valueString = null;
-        if (value instanceof byte[])
+        if (value instanceof byte[]) {
             valueString = new String((byte[]) value);
-        else
+        } else {
             valueString = value.toString();
+        }
 
         return valueString;
     }
@@ -2144,20 +2099,22 @@ public class JNDIRealm extends RealmBase
      * @return the list of attribute values
      * @exception NamingException if a directory server error occurs
      */
-    private ArrayList<String> addAttributeValues(String attrId,
-                                         Attributes attrs,
-                                         ArrayList<String> values)
-        throws NamingException{
+    private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values)
+            throws NamingException {
 
-        if (containerLog.isTraceEnabled())
+        if (containerLog.isTraceEnabled()) {
             containerLog.trace("  retrieving values for attribute " + attrId);
-        if (attrId == null || attrs == null)
+        }
+        if (attrId == null || attrs == null) {
             return values;
-        if (values == null)
+        }
+        if (values == null) {
             values = new ArrayList<>();
+        }
         Attribute attr = attrs.get(attrId);
-        if (attr == null)
+        if (attr == null) {
             return values;
+        }
         NamingEnumeration<?> e = attr.getAll();
         try {
             while(e.hasMore()) {
@@ -2165,8 +2122,9 @@ public class JNDIRealm extends RealmBase
                 values.add(value);
             }
         } catch (PartialResultException ex) {
-            if (!adCompat)
+            if (!adCompat) {
                 throw ex;
+            }
         } finally {
             e.close();
         }
@@ -2177,13 +2135,17 @@ public class JNDIRealm extends RealmBase
     /**
      * Close any open connection to the directory server for this Realm.
      *
-     * @param context The directory context to be closed
+     * @param connection The directory context to be closed
      */
-    protected void close(DirContext context) {
+    protected void close(JNDIConnection connection) {
 
         // Do nothing if there is no opened connection
-        if (context == null)
+        if (connection == null || connection.context == null) {
+            if (connectionPool == null) {
+                singleConnectionLock.unlock();
+            }
             return;
+        }
 
         // Close tls startResponse if used
         if (tls != null) {
@@ -2195,14 +2157,34 @@ public class JNDIRealm extends RealmBase
         }
         // Close our opened connection
         try {
-            if (containerLog.isDebugEnabled())
+            if (containerLog.isDebugEnabled()) {
                 containerLog.debug("Closing directory context");
-            context.close();
+            }
+            connection.context.close();
         } catch (NamingException e) {
             containerLog.error(sm.getString("jndiRealm.close"), e);
         }
-        this.context = null;
+        connection.context = null;
+        // The lock will be reacquired before any manipulation of the connection
+        if (connectionPool == null) {
+            singleConnectionLock.unlock();
+        }
+    }
+
 
+    /**
+     * Close all pooled connections.
+     */
+    protected void closePooledConnections() {
+        if (connectionPool != null) {
+            // Close any pooled connections as they might be bad as well
+            synchronized (connectionPool) {
+                JNDIConnection connection = null;
+                while ((connection = connectionPool.pop()) != null) {
+                    close(connection);
+                }
+            }
+        }
     }
 
 
@@ -2218,8 +2200,34 @@ public class JNDIRealm extends RealmBase
             return null;
         }
 
+        JNDIConnection connection = null;
+        User user = null;
         try {
-            User user = getUser(open(), username, null);
+            // Ensure that we have a directory context available
+            connection = get();
+
+            // Occasionally the directory context will timeout.  Try one more
+            // time before giving up.
+            try {
+                user = getUser(connection, username, null);
+            } catch (NullPointerException | NamingException e) {
+                // log the exception so we know it's there.
+                containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
+
+                // close the connection so we know it will be reopened.
+                close(connection);
+                closePooledConnections();
+
+                // open a new directory context.
+                connection = get();
+
+                // Try the authentication again.
+                user = getUser(connection, username, null);
+            }
+
+            // Release this context
+            release(connection);
+
             if (user == null) {
                 // User should be found...
                 return null;
@@ -2228,11 +2236,13 @@ public class JNDIRealm extends RealmBase
                 return user.getPassword();
             }
         } catch (NamingException e) {
+            // Log the problem for posterity
+            containerLog.error(sm.getString("jndiRealm.exception"), e);
             return null;
         }
-
     }
 
+
     /**
      * Get the principal associated with the specified certificate.
      * @param username The user name
@@ -2243,9 +2253,9 @@ public class JNDIRealm extends RealmBase
         return getPrincipal(username, null);
     }
 
+
     @Override
-    protected Principal getPrincipal(GSSName gssName,
-            GSSCredential gssCredential) {
+    protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) {
         String name = gssName.toString();
 
         if (isStripRealmForGss()) {
@@ -2259,120 +2269,102 @@ public class JNDIRealm extends RealmBase
         return getPrincipal(name, gssCredential);
     }
 
+
     @Override
-    protected Principal getPrincipal(String username,
-            GSSCredential gssCredential) {
+    protected Principal getPrincipal(String username, GSSCredential gssCredential) {
 
-        DirContext context = null;
+        JNDIConnection connection = null;
         Principal principal = null;
 
         try {
-
             // Ensure that we have a directory context available
-            context = open();
+            connection = get();
 
             // Occasionally the directory context will timeout.  Try one more
             // time before giving up.
             try {
 
                 // Authenticate the specified username if possible
-                principal = getPrincipal(context, username, gssCredential);
+                principal = getPrincipal(connection, username, gssCredential);
 
             } catch (CommunicationException | ServiceUnavailableException e) {
-
                 // log the exception so we know it's there.
                 containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
 
                 // close the connection so we know it will be reopened.
-                if (context != null)
-                    close(context);
+                close(connection);
+                closePooledConnections();
 
                 // open a new directory context.
-                context = open();
+                connection = get();
 
                 // Try the authentication again.
-                principal = getPrincipal(context, username, gssCredential);
-
+                principal = getPrincipal(connection, username, gssCredential);
             }
 
-
             // Release this context
-            release(context);
+            release(connection);
 
             // Return the authenticated Principal (if any)
             return principal;
 
         } catch (NamingException e) {
-
             // Log the problem for posterity
             containerLog.error(sm.getString("jndiRealm.exception"), e);
 
-            // Close the connection so that it gets reopened next time
-            if (context != null)
-                close(context);
-
             // Return "not authenticated" for this request
             return null;
-
         }
-
-
     }
 
 
     /**
      * Get the principal associated with the specified certificate.
-     * @param context The directory context
+     * @param connection The directory context
      * @param username The user name
      * @param gssCredential The credentials
      * @return the Principal associated with the given certificate.
      * @exception NamingException if a directory server error occurs
      */
-    protected synchronized Principal getPrincipal(DirContext context,
-            String username, GSSCredential gssCredential)
-        throws NamingException {
+    protected Principal getPrincipal(JNDIConnection connection, String username, GSSCredential gssCredential)
+            throws NamingException {
 
         User user = null;
         List<String> roles = null;
         Hashtable<?, ?> preservedEnvironment = null;
+        DirContext context = connection.context;
 
         try {
             if (gssCredential != null && isUseDelegatedCredential()) {
                 // Preserve the current context environment parameters
                 preservedEnvironment = context.getEnvironment();
                 // Set up context
-                context.addToEnvironment(
-                        Context.SECURITY_AUTHENTICATION, "GSSAPI");
-                context.addToEnvironment(
-                        "javax.security.sasl.server.authentication", "true");
-                context.addToEnvironment(
-                        "javax.security.sasl.qop", spnegoDelegationQop);
+                context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "GSSAPI");
+                context.addToEnvironment("javax.security.sasl.server.authentication", "true");
+                context.addToEnvironment("javax.security.sasl.qop", spnegoDelegationQop);
                 // Note: Subject already set in SPNEGO authenticator so no need
                 //       for Subject.doAs() here
             }
-            user = getUser(context, username);
+            user = getUser(connection, username);
             if (user != null) {
-                roles = getRoles(context, user);
+                roles = getRoles(connection, user);
             }
         } finally {
             if (gssCredential != null && isUseDelegatedCredential()) {
-                restoreEnvironmentParameter(context,
-                        Context.SECURITY_AUTHENTICATION, preservedEnvironment);
-                restoreEnvironmentParameter(context,
-                        "javax.security.sasl.server.authentication", preservedEnvironment);
-                restoreEnvironmentParameter(context, "javax.security.sasl.qop",
-                        preservedEnvironment);
+                restoreEnvironmentParameter(context, Context.SECURITY_AUTHENTICATION, preservedEnvironment);
+                restoreEnvironmentParameter(context, "javax.security.sasl.server.authentication", preservedEnvironment);
+                restoreEnvironmentParameter(context, "javax.security.sasl.qop", preservedEnvironment);
             }
         }
 
         if (user != null) {
-            return new GenericPrincipal(user.getUserName(), user.getPassword(),
-                    roles, null, null, gssCredential);
+            return new GenericPrincipal(user.getUserName(), user.getPassword(), roles, null, null, gssCredential);
         }
 
         return null;
     }
 
+
     private void restoreEnvironmentParameter(DirContext context,
             String parameterName, Hashtable<?, ?> preservedEnvironment) {
         try {
@@ -2386,55 +2378,118 @@ public class JNDIRealm extends RealmBase
         }
     }
 
+
     /**
      * Open (if necessary) and return a connection to the configured
      * directory server for this Realm.
-     * @return the directory context
+     * @return the connection
      * @exception NamingException if a directory server error occurs
      */
-    protected DirContext open() throws NamingException {
+    protected JNDIConnection get() throws NamingException {
+        JNDIConnection connection = null;
+        // Use the pool if available, otherwise use the single connection
+        if (connectionPool != null) {
+            connection = connectionPool.pop();
+            if (connection == null) {
+                connection = create();
+            }
+        } else {
+            singleConnectionLock.lock();
+            connection = singleConnection;
+        }
+        if (connection.context == null) {
+            open(connection);
+        }
+        return connection;
+    }
 
-        // Do nothing if there is a directory server connection already open
-        if (context != null)
-            return context;
 
-        try {
+    /**
+     * Release our use of this connection so that it can be recycled.
+     *
+     * @param connection The directory context to release
+     */
+    protected void release(JNDIConnection connection) {
+        if (connectionPool != null) {
+            if (!connectionPool.push(connection)) {
+                // Any connection that doesn't end back to the pool must be closed
+                close(connection);
+            }
+        } else {
+            singleConnectionLock.unlock();
+        }
+    }
 
-            // Ensure that we have a directory context available
-            context = createDirContext(getDirectoryContextEnvironment());
 
+    /**
+     * Create a new connection wrapper, along with the
+     * message formats.
+     * @return the new connection
+     */
+    protected JNDIConnection create() {
+        JNDIConnection connection = new JNDIConnection();
+        if (userSearch != null) {
+            connection.userSearchFormat = new MessageFormat(userSearch);
+        }
+        if (userPattern != null) {
+            int len = userPatternArray.length;
+            connection.userPatternFormatArray = new MessageFormat[len];
+            for (int i = 0; i < len; i++) {
+                connection.userPatternFormatArray[i] = new MessageFormat(userPatternArray[i]);
+            }
+        }
+        if (roleBase != null) {
+            connection.roleBaseFormat = new MessageFormat(roleBase);
+        }
+        if (roleSearch != null) {
+            connection.roleFormat = new MessageFormat(roleSearch);
+        }
+        return connection;
+    }
+
+
+    /**
+     * Create a new connection to the directory server.
+     * @param connection The directory server connection wrapper
+     * @throws NamingException if a directory server error occurs
+     */
+    protected void open(JNDIConnection connection) throws NamingException {
+        ClassLoader ocl = null;
+        try {
+            if (!isUseContextClassLoader()) {
+                ocl = Thread.currentThread().getContextClassLoader();
+                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
+            }
+            // Ensure that we have a directory context available
+            connection.context = createDirContext(getDirectoryContextEnvironment());
         } catch (Exception e) {
             if (alternateURL == null || alternateURL.length() == 0) {
                 // No alternate URL. Re-throw the exception.
                 throw e;
             }
-
             connectionAttempt = 1;
-
             // log the first exception.
             containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
-
             // Try connecting to the alternate url.
-            context = createDirContext(getDirectoryContextEnvironment());
-
+            connection.context = createDirContext(getDirectoryContextEnvironment());
         } finally {
-
             // reset it in case the connection times out.
             // the primary may come back.
             connectionAttempt = 0;
-
+            if (!isUseContextClassLoader()) {
+                Thread.currentThread().setContextClassLoader(ocl);
+            }
         }
-
-        return context;
-
     }
 
+
     @Override
     public boolean isAvailable() {
         // Simple best effort check
-        return (context != null);
+        return (connectionPool != null || singleConnection.context != null);
     }
 
+
     private DirContext createDirContext(Hashtable<String, String> env) throws NamingException {
         if (useStartTls) {
             return createTlsDirContext(env);
@@ -2443,13 +2498,13 @@ public class JNDIRealm extends RealmBase
         }
     }
 
+
     private SSLSocketFactory getSSLSocketFactory() {
         if (sslSocketFactory != null) {
             return sslSocketFactory;
         }
         final SSLSocketFactory result;
-        if (this.sslSocketFactoryClassName != null
-                && !sslSocketFactoryClassName.trim().equals("")) {
+        if (this.sslSocketFactoryClassName != null && !sslSocketFactoryClassName.trim().equals("")) {
             result = createSSLSocketFactoryFromClassName(this.sslSocketFactoryClassName);
         } else {
             result = createSSLContextFactoryFromProtocol(sslProtocol);
@@ -2458,6 +2513,7 @@ public class JNDIRealm extends RealmBase
         return result;
     }
 
+
     private SSLSocketFactory createSSLSocketFactoryFromClassName(String className) {
         try {
             Object o = constructInstance(className);
@@ -2475,6 +2531,7 @@ public class JNDIRealm extends RealmBase
         }
     }
 
+
     private SSLSocketFactory createSSLContextFactoryFromProtocol(String protocol) {
         try {
             SSLContext sslContext;
@@ -2486,14 +2543,13 @@ public class JNDIRealm extends RealmBase
             }
             return sslContext.getSocketFactory();
         } catch (NoSuchAlgorithmException | KeyManagementException e) {
-            List<String> allowedProtocols = Arrays
-                    .asList(getSupportedSslProtocols());
-            throw new IllegalArgumentException(
-                    sm.getString("jndiRealm.invalidSslProtocol", protocol,
-                            allowedProtocols), e);
+            List<String> allowedProtocols = Arrays.asList(getSupportedSslProtocols());
+            throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslProtocol",
+                    protocol, allowedProtocols), e);
         }
     }
 
+
     /**
      * Create a tls enabled LdapContext and set the StartTlsResponse tls
      * instance variable.
@@ -2504,12 +2560,10 @@ public class JNDIRealm extends RealmBase
      * @throws NamingException
      *             when something goes wrong while negotiating the connection
      */
-    private DirContext createTlsDirContext(
-            Hashtable<String, String> env) throws NamingException {
+    private DirContext createTlsDirContext(Hashtable<String, String> env) throws NamingException {
         Map<String, Object> savedEnv = new HashMap<>();
-        for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION,
-                Context.SECURITY_CREDENTIALS, Context.SECURITY_PRINCIPAL,
-                Context.SECURITY_PROTOCOL)) {
+        for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, Context.SECURITY_CREDENTIALS,
+                Context.SECURITY_PRINCIPAL, Context.SECURITY_PROTOCOL)) {
             Object entry = env.remove(key);
             if (entry != null) {
                 savedEnv.put(key, entry);
@@ -2518,8 +2572,7 @@ public class JNDIRealm extends RealmBase
         LdapContext result = null;
         try {
             result = new InitialLdapContext(env, null);
-            tls = (StartTlsResponse) result
-                    .extendedOperation(new StartTlsRequest());
+            tls = (StartTlsResponse) result.extendedOperation(new StartTlsRequest());
             if (getHostnameVerifier() != null) {
                 tls.setHostnameVerifier(getHostnameVerifier());
             }
@@ -2528,22 +2581,21 @@ public class JNDIRealm extends RealmBase
             }
             try {
                 SSLSession negotiate = tls.negotiate(getSSLSocketFactory());
-                containerLog.debug(sm.getString("jndiRealm.negotiatedTls",
-                        negotiate.getProtocol()));
+                containerLog.debug(sm.getString("jndiRealm.negotiatedTls", negotiate.getProtocol()));
             } catch (IOException e) {
                 throw new NamingException(e.getMessage());
             }
         } finally {
             if (result != null) {
                 for (Map.Entry<String, Object> savedEntry : savedEnv.entrySet()) {
-                    result.addToEnvironment(savedEntry.getKey(),
-                            savedEntry.getValue());
+                    result.addToEnvironment(savedEntry.getKey(), savedEntry.getValue());
                 }
             }
         }
         return result;
     }
 
+
     /**
      * Create our directory context configuration.
      *
@@ -2554,52 +2606,48 @@ public class JNDIRealm extends RealmBase
         Hashtable<String,String> env = new Hashtable<>();
 
         // Configure our directory context environment.
-        if (containerLog.isDebugEnabled() && connectionAttempt == 0)
+        if (containerLog.isDebugEnabled() && connectionAttempt == 0) {
             containerLog.debug("Connecting to URL " + connectionURL);
-        else if (containerLog.isDebugEnabled() && connectionAttempt > 0)
+        } else if (containerLog.isDebugEnabled() && connectionAttempt > 0) {
             containerLog.debug("Connecting to URL " + alternateURL);
+        }
         env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
-        if (connectionName != null)
+        if (connectionName != null) {
             env.put(Context.SECURITY_PRINCIPAL, connectionName);
-        if (connectionPassword != null)
+        }
+        if (connectionPassword != null) {
             env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
-        if (connectionURL != null && connectionAttempt == 0)
+        }
+        if (connectionURL != null && connectionAttempt == 0) {
             env.put(Context.PROVIDER_URL, connectionURL);
-        else if (alternateURL != null && connectionAttempt > 0)
+        } else if (alternateURL != null && connectionAttempt > 0) {
             env.put(Context.PROVIDER_URL, alternateURL);
-        if (authentication != null)
+        }
+        if (authentication != null) {
             env.put(Context.SECURITY_AUTHENTICATION, authentication);
-        if (protocol != null)
+        }
+        if (protocol != null) {
             env.put(Context.SECURITY_PROTOCOL, protocol);
-        if (referrals != null)
+        }
+        if (referrals != null) {
             env.put(Context.REFERRAL, referrals);
-        if (derefAliases != null)
+        }
+        if (derefAliases != null) {
             env.put(JNDIRealm.DEREF_ALIASES, derefAliases);
-        if (connectionTimeout != null)
+        }
+        if (connectionTimeout != null) {
             env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout);
-        if (readTimeout != null)
+        }
+        if (readTimeout != null) {
             env.put("com.sun.jndi.ldap.read.timeout", readTimeout);
+        }
 
         return env;
-
-    }
-
-
-    /**
-     * Release our use of this connection so that it can be recycled.
-     *
-     * @param context The directory context to release
-     */
-    protected void release(DirContext context) {
-
-        // NO-OP since we are not pooling anything
-
     }
 
 
     // ------------------------------------------------------ Lifecycle Methods
 
-
     /**
      * Prepare for the beginning of active use of the public methods of this
      * component and implement the requirements of
@@ -2611,15 +2659,22 @@ public class JNDIRealm extends RealmBase
     @Override
     protected void startInternal() throws LifecycleException {
 
+        if (connectionPoolSize != 1) {
+            connectionPool = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, connectionPoolSize);
+        }
+
         // Check to see if the connection to the directory can be opened
+        JNDIConnection connection = null;
         try {
-            open();
+            connection = get();
         } catch (NamingException e) {
             // A failure here is not fatal as the directory may be unavailable
             // now but available later. Unavailability of the directory is not
             // fatal once the Realm has started so there is no reason for it to
             // be fatal when the Realm starts.
             containerLog.error(sm.getString("jndiRealm.open"), e);
+        } finally {
+            release(connection);
         }
 
         super.startInternal();
@@ -2634,16 +2689,20 @@ public class JNDIRealm extends RealmBase
      * @exception LifecycleException if this component detects a fatal error
      *  that needs to be reported
      */
-     @Override
+    @Override
     protected void stopInternal() throws LifecycleException {
-
         super.stopInternal();
-
         // Close any open directory server connection
-        close(this.context);
-
+        if (connectionPool == null) {
+            singleConnectionLock.lock();
+            close(singleConnection);
+        } else {
+            closePooledConnections();
+            connectionPool = null;
+        }
     }
 
+
     /**
      * Given a string containing LDAP patterns for user locations (separated by
      * parentheses in a pseudo-LDAP search string format -
@@ -2678,8 +2737,7 @@ public class JNDIRealm extends RealmBase
                 while (userPatternString.charAt(endParenLoc - 1) == '\\') {
                     endParenLoc = userPatternString.indexOf(')', endParenLoc+1);
                 }
-                String nextPathPart = userPatternString.substring
-                    (startParenLoc+1, endParenLoc);
+                String nextPathPart = userPatternString.substring(startParenLoc+1, endParenLoc);
                 pathList.add(nextPathPart);
                 startingPoint = endParenLoc+1;
                 startParenLoc = userPatternString.indexOf('(', startingPoint);
@@ -2687,7 +2745,6 @@ public class JNDIRealm extends RealmBase
             return pathList.toArray(new String[] {});
         }
         return null;
-
     }
 
 
@@ -2702,10 +2759,36 @@ public class JNDIRealm extends RealmBase
      *     )  -&gt; \29
      *     \  -&gt; \5c
      *     \0 -&gt; \00
+     *
      * @param inString string to escape according to RFC 2254 guidelines
+     *
      * @return String the escaped/encoded result
+     *
+     * @deprecated Will be removed in Tomcat 10.1.x onwards
      */
+    @Deprecated
     protected String doRFC2254Encoding(String inString) {
+        return doFilterEscaping(inString);
+    }
+
+
+    /**
+     * Given an LDAP search string, returns the string with certain characters
+     * escaped according to RFC 2254 guidelines.
+     * The character mapping is as follows:
+     *     char -&gt;  Replacement
+     *    ---------------------------
+     *     *  -&gt; \2a
+     *     (  -&gt; \28
+     *     )  -&gt; \29
+     *     \  -&gt; \5c
+     *     \0 -&gt; \00
+     *
+     * @param inString string to escape according to RFC 2254 guidelines
+     *
+     * @return String the escaped/encoded result
+     */
+    protected String doFilterEscaping(String inString) {
         StringBuilder buf = new StringBuilder(inString.length());
         for (int i = 0; i < inString.length(); i++) {
             char c = inString.charAt(i);
@@ -2743,47 +2826,42 @@ public class JNDIRealm extends RealmBase
      * @return String containing the distinguished name
      * @exception NamingException if a directory server error occurs
      */
-    protected String getDistinguishedName(DirContext context, String base,
-            SearchResult result) throws NamingException {
+    protected String getDistinguishedName(DirContext context, String base, SearchResult result) throws NamingException {
         // Get the entry's distinguished name.  For relative results, this means
         // we need to composite a name with the base name, the context name, and
         // the result name.  For non-relative names, use the returned name.
         String resultName = result.getName();
         Name name;
         if (result.isRelative()) {
-           if (containerLog.isTraceEnabled()) {
-               containerLog.trace("  search returned relative name: " + resultName);
-           }
-           NameParser parser = context.getNameParser("");
-           Name contextName = parser.parse(context.getNameInNamespace());
-           Name baseName = parser.parse(base);
+            if (containerLog.isTraceEnabled()) {
+                containerLog.trace("  search returned relative name: " + resultName);
+            }
+            NameParser parser = context.getNameParser("");
+            Name contextName = parser.parse(context.getNameInNamespace());
+            Name baseName = parser.parse(base);
 
-           // Bugzilla 32269
-           Name entryName = parser.parse(new CompositeName(resultName).get(0));
+            // Bugzilla 32269
+            Name entryName = parser.parse(new CompositeName(resultName).get(0));
 
-           name = contextName.addAll(baseName);
-           name = name.addAll(entryName);
+            name = contextName.addAll(baseName);
+            name = name.addAll(entryName);
         } else {
-           if (containerLog.isTraceEnabled()) {
-               containerLog.trace("  search returned absolute name: " + resultName);
-           }
-           try {
-               // Normalize the name by running it through the name parser.
-               NameParser parser = context.getNameParser("");
-               URI userNameUri = new URI(resultName);
-               String pathComponent = userNameUri.getPath();
-               // Should not ever have an empty path component, since that is /{DN}
-               if (pathComponent.length() < 1 ) {
-                   throw new InvalidNameException(
-                           "Search returned unparseable absolute name: " +
-                           resultName );
-               }
-               name = parser.parse(pathComponent.substring(1));
-           } catch ( URISyntaxException e ) {
-               throw new InvalidNameException(
-                       "Search returned unparseable absolute name: " +
-                       resultName );
-           }
+            if (containerLog.isTraceEnabled()) {
+                containerLog.trace("  search returned absolute name: " + resultName);
+            }
+            try {
+                // Normalize the name by running it through the name parser.
+                NameParser parser = context.getNameParser("");
+                URI userNameUri = new URI(resultName);
+                String pathComponent = userNameUri.getPath();
+                // Should not ever have an empty path component, since that is /{DN}
+                if (pathComponent.length() < 1 ) {
+                    throw new InvalidNameException("Search returned unparseable absolute name: " + resultName);
+                }
+                name = parser.parse(pathComponent.substring(1));
+            } catch ( URISyntaxException e ) {
+                throw new InvalidNameException("Search returned unparseable absolute name: " + resultName);
+            }
         }
 
         if (getForceDnHexEscape()) {
@@ -2795,6 +2873,78 @@ public class JNDIRealm extends RealmBase
     }
 
 
+    /**
+     * Implements the necessary escaping to represent an attribute value as a
+     * String as per RFC 4514.
+     *
+     * @param input The original attribute value
+     * @return      The string representation of the attribute value
+     */
+    protected String doAttributeValueEscaping(String input) {
+        int len = input.length();
+        StringBuilder result = new StringBuilder();
+
+        for (int i = 0; i < len; i++) {
+            char c = input.charAt(i);
+            switch (c) {
+                case ' ': {
+                    if (i == 0 || i == (len -1)) {
+                        result.append("\\20");
+                    } else {
+                        result.append(c);
+                    }
+                    break;
+                }
+                case '#': {
+                    if (i == 0 ) {
+                        result.append("\\23");
+                    } else {
+                        result.append(c);
+                    }
+                    break;
+                }
+                case '\"': {
+                    result.append("\\22");
+                    break;
+                }
+                case '+': {
+                    result.append("\\2B");
+                    break;
+                }
+                case ',': {
+                    result.append("\\2C");
+                    break;
+                }
+                case ';': {
+                    result.append("\\3B");
+                    break;
+                }
+                case '<': {
+                    result.append("\\3C");
+                    break;
+                }
+                case '>': {
+                    result.append("\\3E");
+                    break;
+                }
+                case '\\': {
+                    result.append("\\5C");
+                    break;
+                }
+                case '\u0000': {
+                    result.append("\\00");
+                    break;
+                }
+                default:
+                    result.append(c);
+            }
+
+        }
+
+        return result.toString();
+    }
+
+
     protected static String convertToHexEscape(String input) {
         if (input.indexOf('\\') == -1) {
             // No escaping present. Return original.
@@ -2871,7 +3021,7 @@ public class JNDIRealm extends RealmBase
     }
 
 
-    // ------------------------------------------------------ Private Classes
+    // ------------------------------------------------------ Protected Classes
 
     /**
      * A protected class representing a User
@@ -2884,9 +3034,7 @@ public class JNDIRealm extends RealmBase
         private final List<String> roles;
         private final String userRoleId;
 
-
-        public User(String username, String dn, String password,
-                List<String> roles, String userRoleId) {
+        public User(String username, String dn, String password, List<String> roles, String userRoleId) {
             this.username = username;
             this.dn = dn;
             this.password = password;
@@ -2918,5 +3066,41 @@ public class JNDIRealm extends RealmBase
             return userRoleId;
         }
     }
-}
 
+
+    /**
+     * Class holding the connection to the directory plus the associated
+     * non thread safe message formats.
+     */
+    protected static class JNDIConnection {
+
+        /**
+         * The MessageFormat object associated with the current
+         * <code>userSearch</code>.
+         */
+        protected MessageFormat userSearchFormat = null;
+
+        /**
+         * An array of MessageFormat objects associated with the current
+         * <code>userPatternArray</code>.
+         */
+        protected MessageFormat[] userPatternFormatArray = null;
+
+        /**
+         * The MessageFormat object associated with the current
+         * <code>roleBase</code>.
+         */
+        protected MessageFormat roleBaseFormat = null;
+
+        /**
+         * The MessageFormat object associated with the current
+         * <code>roleSearch</code>.
+         */
+        protected MessageFormat roleFormat = null;
+
+        /**
+         * The directory context linking us to our directory server.
+         */
+        protected DirContext context = null;
+    }
+}
Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java
===================================================================
--- /dev/null
+++ apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.realm;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+import org.apache.juli.logging.LogFactory;
+
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.sdk.AddRequest;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPResult;
+import com.unboundid.ldap.sdk.ResultCode;
+
+@RunWith(Parameterized.class)
+public class TestJNDIRealmIntegration {
+
+    private static final String USER_PATTERN = "cn={0},ou=people,dc=example,dc=com";
+    private static final String USER_SEARCH = "cn={0}";
+    private static final String USER_BASE = "ou=people,dc=example,dc=com";
+    private static final String ROLE_SEARCH_A = "member={0}";
+    private static final String ROLE_SEARCH_B = "member=cn={1},ou=people,dc=example,dc=com";
+    private static final String ROLE_SEARCH_C = "member=cn={2},ou=people,dc=example,dc=com";
+    private static final String ROLE_BASE = "ou=people,dc=example,dc=com";
+
+    private static InMemoryDirectoryServer ldapServer;
+
+    @Parameterized.Parameters(name = "{index}: user[{5}], pwd[{6}]")
+    public static Collection<Object[]> parameters() {
+        List<Object[]> parameterSets = new ArrayList<>();
+        for (String roleSearch : new String[] { ROLE_SEARCH_A, ROLE_SEARCH_B, ROLE_SEARCH_C }) {
+            addUsers(USER_PATTERN, null, null, roleSearch, ROLE_BASE, parameterSets);
+            addUsers(null, USER_SEARCH, USER_BASE, roleSearch, ROLE_BASE, parameterSets);
+        }
+        parameterSets.add(new Object[] { "cn={0},ou=s\\;ub,ou=people,dc=example,dc=com", null, null, ROLE_SEARCH_A,
+                "{3},ou=people,dc=example,dc=com", "testsub", "test", new String[] {"TestGroup4"} });
+        return parameterSets;
+    }
+
+
+    private static void addUsers(String userPattern, String userSearch, String userBase, String roleSearch,
+            String roleBase, List<Object[]> parameterSets) {
+        parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase,
+                "test", "test", new String[] {"TestGroup"} });
+        parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase,
+                "t;", "test", new String[] {"TestGroup"} });
+        parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase,
+                "t*", "test", new String[] {"TestGroup"} });
+        parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase,
+                "t=", "test", new String[] {"Test<Group*2", "Test>Group*3"} });
+    }
+
+
+    @Parameter(0)
+    public String realmConfigUserPattern;
+    @Parameter(1)
+    public String realmConfigUserSearch;
+    @Parameter(2)
+    public String realmConfigUserBase;
+    @Parameter(3)
+    public String realmConfigRoleSearch;
+    @Parameter(4)
+    public String realmConfigRoleBase;
+    @Parameter(5)
+    public String username;
+    @Parameter(6)
+    public String credentials;
+    @Parameter(7)
+    public String[] groups;
+
+    @Test
+    public void testAuthenication() throws Exception {
+        JNDIRealm realm = new JNDIRealm();
+        realm.containerLog = LogFactory.getLog(TestJNDIRealmIntegration.class);
+
+        realm.setConnectionURL("ldap://localhost:" + ldapServer.getListenPort());
+        realm.setUserPattern(realmConfigUserPattern);
+        realm.setUserSearch(realmConfigUserSearch);
+        realm.setUserBase(realmConfigUserBase);
+        realm.setUserRoleAttribute("cn");
+        realm.setRoleName("cn");
+        realm.setRoleBase(realmConfigRoleBase);
+        realm.setRoleSearch(realmConfigRoleSearch);
+        realm.setRoleNested(true);
+
+        GenericPrincipal p = (GenericPrincipal) realm.authenticate(username, credentials);
+
+        Assert.assertNotNull(p);
+        Assert.assertEquals(username, p.name);
+
+        Set<String> actualGroups = new HashSet<>(Arrays.asList(p.getRoles()));
+        Set<String> expectedGroups  = new HashSet<>(Arrays.asList(groups));
+
+        Assert.assertEquals(expectedGroups.size(), actualGroups.size());
+        Set<String> tmp = new HashSet<>();
+        tmp.addAll(expectedGroups);
+        tmp.removeAll(actualGroups);
+        Assert.assertEquals(0, tmp.size());
+    }
+
+
+    @BeforeClass
+    public static void createLDAP() throws Exception {
+        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
+        config.addAdditionalBindCredentials("cn=admin", "password");
+        ldapServer = new InMemoryDirectoryServer(config);
+
+        ldapServer.startListening();
+
+        try (LDAPConnection conn =  ldapServer.getConnection()) {
+
+            // Note: Only the DNs need attribute value escaping
+            AddRequest addBase = new AddRequest(
+                    "dn: dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: domain",
+                    "dc: example");
+            LDAPResult result = conn.processOperation(addBase);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addPeople = new AddRequest(
+                    "dn: ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: organizationalUnit");
+            result = conn.processOperation(addPeople);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addUserTest = new AddRequest(
+                    "dn: cn=test,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: person",
+                    "objectClass: organizationalPerson",
+                    "cn: test",
+                    "sn: Test",
+                    "userPassword: test");
+            result = conn.processOperation(addUserTest);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addUserTestSemicolon = new AddRequest(
+                    "dn: cn=t\\;,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: person",
+                    "objectClass: organizationalPerson",
+                    "cn: t;",
+                    "sn: Tsemicolon",
+                    "userPassword: test");
+            result = conn.processOperation(addUserTestSemicolon);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addUserTestAsterisk = new AddRequest(
+                    "dn: cn=t*,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: person",
+                    "objectClass: organizationalPerson",
+                    "cn: t*",
+                    "sn: Tasterisk",
+                    "userPassword: test");
+            result = conn.processOperation(addUserTestAsterisk);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addUserTestEquals = new AddRequest(
+                    "dn: cn=t\\=,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: person",
+                    "objectClass: organizationalPerson",
+                    "cn: t=",
+                    "sn: Tequals",
+                    "userPassword: test");
+            result = conn.processOperation(addUserTestEquals);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addGroupTest = new AddRequest(
+                    "dn: cn=TestGroup,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: groupOfNames",
+                    "cn: TestGroup",
+                    "member: cn=test,ou=people,dc=example,dc=com",
+                    "member: cn=t\\;,ou=people,dc=example,dc=com",
+                    "member: cn=t\\*,ou=people,dc=example,dc=com");
+            result = conn.processOperation(addGroupTest);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addGroupTest2 = new AddRequest(
+                    "dn: cn=Test\\<Group*2,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: groupOfNames",
+                    "cn: Test<Group*2",
+                    "member: cn=t\\=,ou=people,dc=example,dc=com");
+            result = conn.processOperation(addGroupTest2);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addGroupTest3 = new AddRequest(
+                    "dn: cn=Test\\>Group*3,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: groupOfNames",
+                    "cn: Test>Group*3",
+                    "member: cn=Test\\<Group*2,ou=people,dc=example,dc=com");
+            result = conn.processOperation(addGroupTest3);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addPeopleSub = new AddRequest(
+                    "dn: ou=s\\;ub,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: organizationalUnit");
+            result = conn.processOperation(addPeopleSub);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addUserTestSub = new AddRequest(
+                    "dn: cn=testsub,ou=s\\;ub,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: person",
+                    "objectClass: organizationalPerson",
+                    "cn: testsub",
+                    "sn: Testsub",
+                    "userPassword: test");
+            result = conn.processOperation(addUserTestSub);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addGroupTest4 = new AddRequest(
+                    "dn: cn=TestGroup4,ou=s\\;ub,ou=people,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: groupOfNames",
+                    "cn: TestGroup4",
+                    "member: cn=testsub,ou=s\\;ub,ou=people,dc=example,dc=com");
+            result = conn.processOperation(addGroupTest4);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+        }
+    }
+
+
+    @AfterClass
+    public static void destroyLDAP() {
+        ldapServer.shutDown(true);
+    }
+}
Index: apache-tomcat-9.0.36-src/webapps/docs/changelog.xml
===================================================================
--- apache-tomcat-9.0.36-src.orig/webapps/docs/changelog.xml
+++ apache-tomcat-9.0.36-src/webapps/docs/changelog.xml
@@ -85,6 +85,10 @@
         <bug>64871</bug>: Log a warning if Tomcat blocks access to a file
         because it uses symlinks. (markt)
       </add>
+      <scode>
+        Expand coverage of unit tests for JNDIRealm using the UnboundID LDAP SDK
+        for Java. (markt)
+      </scode>
     </changelog>
   </subsection>
   <subsection name="Coyote">
@@ -141,6 +145,10 @@
         Make handling of OpenSSL read errors more robust when plain text data is
         reported to be available to read. (markt)
       </fix>
+      <fix>
+        <bug>65033</bug>: Fix JNDI realm error handling when connecting to a
+        failed server when pooling was not enabled. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Other">
@@ -236,6 +244,11 @@
       <add>
         Improve validation of storage location when using FileStore. (markt)
       </add>
+      <fix>
+        JNDIRealm connections should only be created with the container
+        classloader as the thread context classloader, just like for the JAAS
+        realm. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">
@@ -3915,6 +3928,12 @@
         subsequently incremented when using asynchronous processing over HTTP/2.
         (markt)
       </fix>
+      <fix>
+        Fix JNDIRealm pooling problems retrying on another bad connection. Any
+        retries are made on a new connection, just like with the single
+        connection scenario. Also remove all connections from the pool after
+        an error. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Jasper">
@@ -9657,6 +9676,9 @@
         Update the internal fork of Commons Codec to r1725746 (1.9 plus
         additional fixes). (markt)
       </update>
+      <update>
+        Add connection pooling to JNDI realm. (remm)
+      </update>
     </changelog>
   </subsection>
 </section>
Index: apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/LocalStrings.properties
===================================================================
--- apache-tomcat-9.0.36-src.orig/java/org/apache/catalina/realm/LocalStrings.properties
+++ apache-tomcat-9.0.36-src/java/org/apache/catalina/realm/LocalStrings.properties
@@ -77,6 +77,7 @@ jndiRealm.exception.retry=Exception perf
 jndiRealm.invalidHostnameVerifier=[{0}] not a valid class name for a HostnameVerifier
 jndiRealm.invalidSslProtocol=Given protocol [{0}] is invalid. It has to be one of [{1}]
 jndiRealm.invalidSslSocketFactory=[{0}] not a valid class name for an SSLSocketFactory
+jndiRealm.multipleEntries=User name [{0}] has multiple entries
 jndiRealm.negotiatedTls=Negotiated tls connection using protocol [{0}]
 jndiRealm.open=Exception opening directory server connection
 jndiRealm.tlsClose=Exception closing tls response
Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealm.java
===================================================================
--- apache-tomcat-9.0.36-src.orig/test/org/apache/catalina/realm/TestJNDIRealm.java
+++ apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealm.java
@@ -109,6 +109,27 @@ public class TestJNDIRealm {
         Assert.assertEquals(ha1(), ((GenericPrincipal)principal).getPassword());
     }
 
+    volatile int count = 0;
+
+    @Test
+    public void testErrorRealm() throws Exception {
+        Context context = new TesterContext();
+        JNDIRealm realm = new JNDIRealm();
+        realm.setContainer(context);
+        realm.setUserSearch("");
+        // Connect to something that will fail
+        realm.setConnectionURL("ldap://127.0.0.1:12345");
+        realm.start();
+
+        count = 0;
+        (new Thread(() -> { realm.authenticate("foo", "bar"); count++; })).start();
+        (new Thread(() -> { realm.authenticate("foo", "bar"); count++; })).start();
+        (new Thread(() -> { realm.authenticate("foo", "bar"); count++; })).start();
+        Thread.sleep(10);
+
+        Assert.assertEquals(3, count);
+    }
+
 
     private JNDIRealm buildRealm(String password) throws javax.naming.NamingException,
             NoSuchFieldException, IllegalAccessException, LifecycleException {
@@ -117,9 +138,12 @@ public class TestJNDIRealm {
         realm.setContainer(context);
         realm.setUserSearch("");
 
-        Field field = JNDIRealm.class.getDeclaredField("context");
+        // Usually everything is created in create() but that's not the case here
+        Field field = JNDIRealm.class.getDeclaredField("singleConnection");
         field.setAccessible(true);
-        field.set(realm, mockDirContext(mockSearchResults(password)));
+        Field field2 = JNDIRealm.JNDIConnection.class.getDeclaredField("context");
+        field2.setAccessible(true);
+        field2.set(field.get(realm), mockDirContext(mockSearchResults(password)));
 
         realm.start();
 
Index: apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java
===================================================================
--- /dev/null
+++ apache-tomcat-9.0.36-src/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.realm;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+@RunWith(Parameterized.class)
+public class TestJNDIRealmAttributeValueEscape {
+
+    @Parameterized.Parameters(name = "{index}: in[{0}], out[{1}]")
+    public static Collection<Object[]> parameters() {
+        List<Object[]> parameterSets = new ArrayList<>();
+
+        // No escaping required
+        parameterSets.add(new String[] { "none", "none" });
+        // Simple cases (same order as RFC 4512 section 2)
+        // Each appearing at the beginning, middle and ent
+        parameterSets.add(new String[] { " test", "\\20test" });
+        parameterSets.add(new String[] { "te st", "te st" });
+        parameterSets.add(new String[] { "test ", "test\\20" });
+        parameterSets.add(new String[] { "#test", "\\23test" });
+        parameterSets.add(new String[] { "te#st", "te#st" });
+        parameterSets.add(new String[] { "test#", "test#" });
+        parameterSets.add(new String[] { "\"test", "\\22test" });
+        parameterSets.add(new String[] { "te\"st", "te\\22st" });
+        parameterSets.add(new String[] { "test\"", "test\\22" });
+        parameterSets.add(new String[] { "+test", "\\2Btest" });
+        parameterSets.add(new String[] { "te+st", "te\\2Bst" });
+        parameterSets.add(new String[] { "test+", "test\\2B" });
+        parameterSets.add(new String[] { ",test", "\\2Ctest" });
+        parameterSets.add(new String[] { "te,st", "te\\2Cst" });
+        parameterSets.add(new String[] { "test,", "test\\2C" });
+        parameterSets.add(new String[] { ";test", "\\3Btest" });
+        parameterSets.add(new String[] { "te;st", "te\\3Bst" });
+        parameterSets.add(new String[] { "test;", "test\\3B" });
+        parameterSets.add(new String[] { "<test", "\\3Ctest" });
+        parameterSets.add(new String[] { "te<st", "te\\3Cst" });
+        parameterSets.add(new String[] { "test<", "test\\3C" });
+        parameterSets.add(new String[] { ">test", "\\3Etest" });
+        parameterSets.add(new String[] { "te>st", "te\\3Est" });
+        parameterSets.add(new String[] { "test>", "test\\3E" });
+        parameterSets.add(new String[] { "\\test", "\\5Ctest" });
+        parameterSets.add(new String[] { "te\\st", "te\\5Cst" });
+        parameterSets.add(new String[] { "test\\", "test\\5C" });
+        parameterSets.add(new String[] { "\u0000test", "\\00test" });
+        parameterSets.add(new String[] { "te\u0000st", "te\\00st" });
+        parameterSets.add(new String[] { "test\u0000", "test\\00" });
+        return parameterSets;
+    }
+
+
+    @Parameter(0)
+    public String in;
+    @Parameter(1)
+    public String out;
+
+    private JNDIRealm realm = new JNDIRealm();
+
+    @Test
+    public void testConvertToHexEscape() throws Exception {
+        String result = realm.doAttributeValueEscaping(in);
+        Assert.assertEquals(out, result);
+    }
+}
Index: apache-tomcat-9.0.36-src/webapps/docs/config/realm.xml
===================================================================
--- apache-tomcat-9.0.36-src.orig/webapps/docs/config/realm.xml
+++ apache-tomcat-9.0.36-src/webapps/docs/config/realm.xml
@@ -433,6 +433,13 @@
         property.</p>
       </attribute>
 
+      <attribute name="connectionPoolSize" required="false">
+        <p>The JNDI realm can use a pool of connections to the directory server
+        to avoid blocking on a single connection. This attribute value is the
+        maximum pool size. If not specified, it will use <code>1</code>, which
+        means a single connection will be used.</p>
+      </attribute>
+
       <attribute name="connectionTimeout" required="false">
         <p>The timeout in milliseconds to use when establishing the connection
         to the LDAP directory. If not specified, a value of 5000 (5 seconds) is
@@ -622,6 +629,13 @@
            specified, the default value of <code>302</code> is used.</p>
       </attribute>
 
+      <attribute name="useContextClassLoader" required="false">
+        <p>Instructs JNDIRealm to use the context class loader when opening the
+        connection for the JNDI provider. The default value is
+        <code>true</code>. To load classes using the container's classloader,
+        specify <code>false</code>.</p>
+      </attribute>
+
       <attribute name="useDelegatedCredential" required="false">
         <p>When the JNDIRealm is used with the SPNEGO authenticator, delegated
         credentials for the user may be available. If such credentials are
openSUSE Build Service is sponsored by