From 0d29eee9c5609bc14ec45bfca6b487d174817176 Mon Sep 17 00:00:00 2001
From: roman khapov <r.khapov@ya.ru>
Date: Fri, 12 Jun 2026 06:53:57 +0000
Subject: [PATCH] Add timeout options for LDAP authentication connections

Previously, if the LDAP server became unreachable during
authentication, a backend process could hang undefined amount
of time while waiting for a response, holding a connection slot.

This patch adds two new pg_hba.conf options: ldapnetworktimeout, which controls
the timeout for individual network I/O operations, and ldaptimeout,
which controls the timeout for a complete LDAP operation such as a
search or bind.  When available, also set TCP user timeout from the
tcp_user_timeout GUC.

Signed-off-by: roman khapov <r.khapov@ya.ru>
---
 doc/src/sgml/client-auth.sgml    | 39 ++++++++++++++++++
 src/backend/libpq/auth.c         | 68 ++++++++++++++++++++++++++++++--
 src/backend/libpq/hba.c          | 42 ++++++++++++++++++++
 src/backend/utils/adt/hbafuncs.c | 20 +++++++++-
 src/include/libpq/hba.h          |  2 +
 5 files changed, 165 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index e4e65f8feb1..09e21746370 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -1841,6 +1841,45 @@ omicron         bryanh                  guest1
        </para>
       </listitem>
      </varlistentry>
+     <varlistentry>
+      <term><literal>ldapnetworktimeout</literal></term>
+      <listitem>
+       <para>
+        Maximum time in seconds to wait for a response from the LDAP
+        server when establishing a connection or waiting for data on an
+        existing connection.  A value of 0 disables the timeout.
+        If not specified, the behavior depends on the LDAP client library
+        configuration (typically no timeout, meaning authentication can
+        hang indefinitely if the LDAP server is unreachable).
+       </para>
+       <para>
+        Note that on Linux, this option interacts with the
+        <xref linkend="guc-tcp-user-timeout"/> parameter, which provides
+        an additional TCP-level timeout.
+       </para>
+      </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>ldaptimeout</literal></term>
+     <listitem>
+      <para>
+       Maximum time in seconds to wait for the completion of a
+       synchronous LDAP operation, such as a search or bind request.
+       A value of 0 disables the timeout.  If not specified, the
+       behavior depends on the LDAP client library configuration
+       (typically no timeout).
+      </para>
+      <para>
+       Note that <literal>ldapnetworktimeout</literal> and
+       <literal>ldaptimeout</literal> control different aspects of
+       timeouts: <literal>ldapnetworktimeout</literal> applies to
+       individual network I/O operations, while
+       <literal>ldaptimeout</literal> applies to the overall duration of
+       an LDAP operation.
+      </para>
+     </listitem>
+    </varlistentry>
     </variablelist>
    </para>
 
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2af5615e54a..8c6a9685f56 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2234,6 +2234,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 	int			ldapversion = LDAP_VERSION3;
 	int			r;
 
+#ifdef LDAP_OPT_TCP_USER_TIMEOUT
+	int				tcp_usr_timeout;
+#endif
+#ifdef WIN32
+	ULONG			option;
+#else
+	struct timeval	tv;
+#endif
+
 	scheme = port->hba->ldapscheme;
 	if (scheme == NULL)
 		scheme = "ldap";
@@ -2375,10 +2384,58 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
-		ldap_unbind(*ldap);
-		return STATUS_ERROR;
+		goto error_cleanup;
+	}
+
+#ifdef WIN32
+	option = (ULONG) port->hba->ldapnetworktimeout;
+	if (port->hba->ldapnetworktimeout != LDAP_NO_LIMIT
+		&& (r = ldap_set_option(*ldap, LDAP_OPT_SEND_TIMEOUT, &option)) != LDAP_SUCCESS)
+#else
+	tv.tv_sec = port->hba->ldapnetworktimeout;
+	tv.tv_usec = 0;
+	if (port->hba->ldapnetworktimeout != -1
+		&& (r = ldap_set_option(*ldap, LDAP_OPT_NETWORK_TIMEOUT, &tv)) != LDAP_SUCCESS)
+#endif
+	{
+		ereport(LOG,
+				(errmsg("could not set LDAP network timeout: %s",
+						ldap_err2string(r)),
+				 errdetail_for_ldap(*ldap)));
+		goto error_cleanup;
+	}
+
+#ifdef WIN32
+	option = (ULONG) port->hba->ldaptimeout;
+	if (port->hba->ldaptimeout != LDAP_NO_LIMIT
+		&& (r = ldap_set_option(*ldap, LDAP_OPT_TIMELIMIT, &option)) != LDAP_SUCCESS)
+#else
+	tv.tv_sec = port->hba->ldaptimeout;
+	tv.tv_usec = 0;
+	if (port->hba->ldaptimeout != -1
+		&& (r = ldap_set_option(*ldap, LDAP_OPT_TIMEOUT, &tv)) != LDAP_SUCCESS)
+#endif
+	{
+		ereport(LOG,
+				(errmsg("could not set LDAP timeout: %s",
+						ldap_err2string(r)),
+				 errdetail_for_ldap(*ldap)));
+		goto error_cleanup;
 	}
 
+#ifdef LDAP_OPT_TCP_USER_TIMEOUT
+	tcp_usr_timeout = pq_gettcpusertimeout(port);
+	if (tcp_usr_timeout > 0
+		&& (r = ldap_set_option(*ldap, LDAP_OPT_TCP_USER_TIMEOUT, &tcp_usr_timeout)) != LDAP_SUCCESS)
+	{
+		ereport(LOG,
+				(errmsg("could not set LDAP tcp user timeout: %s",
+						ldap_err2string(r)),
+				 errdetail_for_ldap(*ldap)));
+		goto error_cleanup;
+	}
+#endif
+
 	if (port->hba->ldaptls)
 	{
 #ifndef WIN32
@@ -2391,12 +2448,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
-			ldap_unbind(*ldap);
-			return STATUS_ERROR;
+			goto error_cleanup;
 		}
 	}
 
 	return STATUS_OK;
+
+error_cleanup:
+	ldap_unbind(*ldap);
+	return STATUS_ERROR;
 }
 
 /* Placeholders recognized by FormatSearchFilter.  For now just one. */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d47eab2cba0..e937f8df303 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -1343,6 +1343,14 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
+#ifndef WIN32
+	parsedline->ldapnetworktimeout = -1;
+	parsedline->ldaptimeout = -1;
+#else
+	parsedline->ldapnetworktimeout = LDAP_NO_LIMIT;
+	parsedline->ldaptimeout = LDAP_NO_LIMIT;
+#endif
+
 	/* Check the record type. */
 	Assert(tok_line->fields != NIL);
 	field = list_head(tok_line->fields);
@@ -2002,6 +2010,8 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 {
 	int			line_num = hbaline->linenumber;
 	char	   *file_name = hbaline->sourcefile;
+	char	   *endp;
+	long		long_val;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -2196,6 +2206,38 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 			return false;
 		}
 	}
+	else if (strcmp(name, "ldapnetworktimeout") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapnetworktimeout", "ldap");
+		long_val = strtol(val, &endp, 10);
+		if (endp == val || long_val > INT_MAX || long_val < 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid LDAP network timeout number: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, file_name)));
+			*err_msg = psprintf("invalid LDAP network timeout number: \"%s\"", val);
+			return false;
+		}
+		hbaline->ldapnetworktimeout = (int) long_val;
+	}
+	else if (strcmp(name, "ldaptimeout") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldaptimeout", "ldap");
+		long_val = strtol(val, &endp, 10);
+		if (endp == val || long_val > INT_MAX || long_val < 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid LDAP timeout number: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, file_name)));
+			*err_msg = psprintf("invalid LDAP timeout number: \"%s\"", val);
+			return false;
+		}
+		hbaline->ldaptimeout = (int) long_val;
+	}
 	else if (strcmp(name, "ldapbinddn") == 0)
 	{
 		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index bd23eda3f79..67cf0aa37bb 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -39,12 +39,12 @@ static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 /*
  * This macro specifies the maximum number of authentication options
  * that are possible with any given authentication method that is supported.
- * Currently LDAP supports 12, and there are 3 that are not dependent on
+ * Currently LDAP supports 14, and there are 3 that are not dependent on
  * the auth method here.  It may not actually be possible to set all of them
  * at the same time, but we'll set the macro value high enough to be
  * conservative and avoid warnings from static analysis tools.
  */
-#define MAX_HBA_OPTIONS 15
+#define MAX_HBA_OPTIONS 17
 
 /*
  * Create a text array listing the options specified in the HBA line.
@@ -91,6 +91,22 @@ get_hba_options(HbaLine *hba)
 			options[noptions++] =
 				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
 
+#ifdef WIN32
+		if (hba->ldapnetworktimeout != LDAP_NO_LIMIT)
+#else
+		if (hba->ldapnetworktimeout != -1)
+#endif
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapnetworktimeout=%d", hba->ldapnetworktimeout));
+
+#ifdef WIN32
+		if (hba->ldaptimeout != LDAP_NO_LIMIT)
+#else
+		if (hba->ldaptimeout != -1)
+#endif
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldaptimeout=%d", hba->ldaptimeout));
+
 		if (hba->ldapscheme)
 			options[noptions++] =
 				CStringGetTextDatum(psprintf("ldapscheme=%s", hba->ldapscheme));
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 4aa6258a345..dc0a886f3ad 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -113,6 +113,8 @@ typedef struct HbaLine
 	char	   *ldapscheme;
 	char	   *ldapserver;
 	int			ldapport;
+	int			ldapnetworktimeout;
+	int			ldaptimeout;
 	char	   *ldapbinddn;
 	char	   *ldapbindpasswd;
 	char	   *ldapsearchattribute;
-- 
2.43.0

