Repository navigation
Outbound GSSAPI bind holds the global krb5_lock with no timeout: one hung supplier stalls replication on all of its peers #7879
Description
Issue Description
When one supplier's ns-slapd hangs, replication stops not only to and from that supplier but also between the remaining healthy suppliers. It stays stopped until the hung process is killed. In our case the hang was the retroCL deadlock in #6652: the kernel still accepts TCP connections, but no LDAP operation is ever answered.
On both healthy suppliers (3-supplier full mesh, FreeIPA, GSSAPI replication agreements) we found the same picture:
- One
repl-protthread, the agreement to the hung supplier, was blocked inpoll(fds, 1, -1)(infinite timeout) on its socket to the hung server. On the hung server, that socket had 1839 unread bytes in its Recv-Q, which is the SASL/GSSAPI BindRequest. - The other three
repl-protthreads were all infutex(FUTEX_WAIT_PRIVATE, 2)on one and the same mutex. These include the agreements between the two healthy suppliers, for bothdc=ando=ipaca.
/proc/<pid>/task/<tid>/syscall on supplier 1 (supplier 3 looked the same):
207588 repl-prot 7 0x7f90386ce804 0x1 0xffffffff ... poll(fds, 1, -1); fds[0].fd = 23 = TCP socket to supplier2:389
207589 repl-prot 202 0x7f9038647180 0x80 0x2 ... futex(FUTEX_WAIT_PRIVATE)
207590 repl-prot 202 0x7f9038647180 0x80 0x2 ...
207591 repl-prot 202 0x7f9038647180 0x80 0x2 ...
Result:
- The two healthy suppliers stopped exchanging changes for about 6 hours. Their last sessions were at 23:35 and 00:00, and the RUV max CSNs diverged by about 6 hours.
- This happened silently. The errors log had no entries, and the agreement status still said "Incremental update succeeded" with an old
nsds5replicaLastUpdateEnd.ipa-healthcheckon the healthy suppliers also hung. - As soon as the hung supplier was SIGKILLed (TCP reset), all agreements on both peers resumed within seconds and the RUVs converged.
Code (389-ds-base-3.2.0)
- For GSSAPI,
slapi_ldap_bind()takes the process-widekrb5_lock(ldaputil.c:1171-1195). It holds the lock acrossslapd_ldap_sasl_interactive_bind()(:1199) →ldap_sasl_interactive_bind_s()(:1507), and only releases it at :1217. - The only timeout set on the connection is
LDAP_OPT_NETWORK_TIMEOUT.slapi_ldap_init_ext()sets it fromnsslapd-outbound-ldap-io-timeout(:790, 300000 here), and the replication plugin then overrides it with the agreement'snsds5ReplicaTimeout(repl5_connection.c:1247, 120/300 s here). It covers connect() only.LDAP_OPT_TIMEOUTis never set, soldap_sasl_interactive_bind_s()waits for every server response without a timeout. - So a peer that accepts the TCP connection but never answers blocks the bind forever, with
krb5_lockheld. Every other agreement then blocks onkrb5_lockat its next (re)bind, including the agreements to healthy peers.
We did not have gdb on the healthy suppliers at the time, so we inferred which lock the futex belongs to:
- All agreement threads wait on one heap-allocated lock.
krb5_lockis created lazily withPR_NewLock()viaPR_CallOnce(), by the first thread that does a GSSAPI bind.- The lock and the blocked thread's poll array are in the same malloc arena.
We can capture a backtrace on a peer the next time this happens.
Expected results
An unresponsive peer should only affect the agreements to that peer. An outbound SASL bind should time out, using the agreement timeout or nsslapd-outbound-ldap-io-timeout, and a process-wide lock should not be held across network I/O.
Possible fixes
- Set
LDAP_OPT_TIMEOUTfor outbound SASL binds, or use the asynchronous SASL bind with a timeout. - Do not hold
krb5_lockacross network I/O. There is no separate credential setup step to lock around any more (set_krb5_creds()was removed in Client keytab realm mismanagement #4537): the lock serializes the wholeslapd_ldap_sasl_interactive_bind()call, so narrowing it means separating the Kerberos credential handling from the SASL exchange. Alternatively, use per-connection or per-thread credential caches (gss_krb5_ccache_name()is thread-specific), so the bind does not need the global lock at all.
Package Version and Platform
- Rocky Linux 10.2, kernel 6.12.0-211.el10_2
- 389-ds-base-3.2.0-10.el10_2, openldap-2.6.10-1.el10, cyrus-sasl-gssapi-2.1.28-30.el10_2, krb5-libs-1.21.3-10.el10_2, nspr-4.39.0-5.el10_2
- FreeIPA 4.13.1: 3 suppliers, full mesh, GSSAPI replication agreements for
dc=ando=ipaca
Steps to Reproduce (suggested; we have not run this synthetically yet)
- Set up 3 suppliers with GSSAPI replication agreements (e.g. FreeIPA).
- Make supplier 2 unresponsive without closing its sockets:
kill -STOP $(pidof ns-slapd)on supplier 2. - Make changes on suppliers 1 and 3, and wait until their agreements to supplier 2 time out and reconnect (new GSSAPI bind).
- Changes stop flowing between suppliers 1 and 3.
/proc/<pid>/task/*/syscallshows the poll(-1) + futex pattern above.kill -CONTon supplier 2 releases it.
Additional context
The trigger in our case was the deadlock in #6652. That comment has the backtraces and the timeline.
Activity
Issue 7879 - Outbound SASL bind waits forever while holding krb5_lock (…
Issue 7879 - Outbound SASL bind waits forever while holding krb5_lock (…
Issue 7879 - Outbound SASL bind waits forever while holding krb5_lock (…
Issue 7879 - Outbound SASL bind waits forever while holding krb5_lock (…
Issue 7879 - Outbound SASL bind waits forever while holding krb5_lock (…