diff --git a/SOURCES/0088-Issue-7366-Memory-leaks-in-syncrepl-plugin-during-pe.patch b/SOURCES/0088-Issue-7366-Memory-leaks-in-syncrepl-plugin-during-pe.patch new file mode 100644 index 0000000..c6b3f3d --- /dev/null +++ b/SOURCES/0088-Issue-7366-Memory-leaks-in-syncrepl-plugin-during-pe.patch @@ -0,0 +1,399 @@ +From 71583a67249df813098f0cca39b04198b76e6349 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Mon, 30 Mar 2026 22:18:49 -0700 +Subject: [PATCH 01/13] Issue 7366 - Memory leaks in syncrepl plugin during + persistent search operations (#7367) + +Description: When running a syncrepl persistent search as a non-root user, +LeakSanitizer detects memory leaks in the sync repl plugin. +Multiple cleanup paths for SyncRequest had inconsistent +and incomplete resource freeing, leaking pblock contents, filters, +queue nodes, and base DNs. Per-thread operation lists were also leaked +on thread exit due to a missing NSPR thread-private destructor. + +Consolidate all SyncRequest cleanup into sync_request_free() and +register a destructor for per-thread OPERATION_PL_CTX_T lists. + +Add a test for non-root persistent syncrepl search verifying no +OPERATIONS_ERROR or 'Missing aclpb' errors. + +Fixes: https://github.com/389ds/389-ds-base/issues/7366 + +Reviewed by: @progier389 (Thanks!) +--- + .../suites/syncrepl_plugin/basic_test.py | 176 +++++++++++++++++- + ldap/servers/plugins/sync/sync_init.c | 24 ++- + ldap/servers/plugins/sync/sync_persist.c | 103 ++++++---- + 3 files changed, 264 insertions(+), 39 deletions(-) + +diff --git a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py +index cdf35eeaa..ff9be816f 100644 +--- a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py ++++ b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py +@@ -20,7 +20,7 @@ from lib389.idm.group import Groups + from lib389.topologies import topology_st as topology + from lib389.topologies import topology_m2 as topo_m2 + from lib389.paths import Paths +-from lib389.utils import ds_is_older ++from lib389.utils import ds_is_older, is_fips, ensure_bytes + from lib389.plugins import RetroChangelogPlugin, ContentSyncPlugin, ContentSynchronizationPlugin, AutoMembershipPlugin, MemberOfPlugin, MemberOfSharedConfig, AutoMembershipDefinitions, MEPTemplates, MEPConfigs, ManagedEntriesPlugin, MEPTemplate + from lib389._constants import * + +@@ -735,3 +735,177 @@ def test_sync_repl_invalid_cookie(topology, request): + pass + + request.addfinalizer(fin) ++ ++ ++def test_sync_repl_non_root_persist(topology, request): ++ """Test sync_repl persistent search as a non-root user triggers ++ proper ACL evaluation without causing OPERATIONS_ERROR. ++ ++ :id: 97f4be3d-f8c3-45cf-8295-ed58ee7bd371 ++ :setup: Standalone Instance ++ :steps: ++ 1. Enable RetroChangelog and ContentSync plugins ++ 2. Create a test user and set its password ++ 3. Add an ACI granting the test user read access ++ 4. Run a syncrepl persistent search bound as the test user ++ 5. Add entries while the persistent search is active ++ 6. Stop the server to end the sync thread ++ 7. Check that the sync client received results without error ++ 8. Check the error log for 'Missing aclpb' messages ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success - no OPERATIONS_ERROR ++ 5. Success ++ 6. Success ++ 7. The sync client should have received cookies ++ 8. No 'Missing aclpb' errors in the log ++ """ ++ inst = topology.standalone ++ inst.restart() ++ inst.enable_tls() ++ ++ # Enable RetroChangelog ++ rcl = RetroChangelogPlugin(inst) ++ rcl.disable() ++ rcl.enable() ++ rcl.set('nsslapd-attribute', 'nsuniqueid:targetUniqueId') ++ ++ # Enable Content Sync plugin ++ csp = ContentSyncPlugin(inst) ++ csp.enable() ++ ++ inst.restart() ++ ++ # Create the syncrepl user ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ sync_user = users.create_test_user(uid=29999) ++ sync_user.set('userPassword', 'sync_password') ++ SYNC_USER_DN = sync_user.dn ++ ++ # Add ACI granting the sync user read access to the suffix ++ ACI = ( ++ '(targetattr="*")(version 3.0; acl "Sync user read access";' ++ ' allow (read, search, compare)' ++ ' userdn = "ldap:///%s";)' % SYNC_USER_DN ++ ) ++ inst.modify_s(DEFAULT_SUFFIX, ++ [(ldap.MOD_ADD, 'aci', ensure_bytes(ACI))]) ++ ++ # Start a syncrepl persistent search as the non-root user ++ # Using the Sync_persist-like pattern but with non-root bind ++ sync_error = [None] # mutable container to capture errors from thread ++ sync_cookies = [] ++ ++ class NonRootSyncer(ReconnectLDAPObject, SyncreplConsumer): ++ def __init__(self, *args, **kwargs): ++ self.cookie = None ++ self.cookies = [] ++ ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs) ++ ++ def syncrepl_set_cookie(self, cookie): ++ self.cookie = cookie ++ self.cookies.append(cookie.split('#')[2]) ++ ++ def syncrepl_get_cookie(self): ++ return self.cookie ++ ++ def syncrepl_present(self, uuids, refreshDeletes=False): ++ pass ++ ++ def syncrepl_delete(self, uuids): ++ pass ++ ++ def syncrepl_entry(self, dn, attrs, uuid): ++ log.info("NonRootSyncer received entry: %s" % dn) ++ ++ def syncrepl_refreshdone(self): ++ log.info("NonRootSyncer refresh done") ++ ++ def get_cookies(self): ++ return self.cookies ++ ++ def sync_thread_func(): ++ ldap_conn = None ++ try: ++ ldap_conn = NonRootSyncer(inst.toLDAPURL()) ++ ldap_conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND) ++ ldap_conn.set_option(ldap.OPT_X_TLS_CACERTFILE, ++ os.path.join(inst.get_config_dir(), "ca.crt")) ++ if is_fips(): ++ ldap_conn.set_option(ldap.OPT_X_TLS_PROTOCOL_MIN, ++ ldap.OPT_X_TLS_PROTOCOL_TLS1_2) ++ ldap_conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0) ++ ++ # Bind as the non-root user ++ ldap_conn.simple_bind_s(SYNC_USER_DN, 'sync_password') ++ ++ ldap_search = ldap_conn.syncrepl_search( ++ DEFAULT_SUFFIX, ++ ldap.SCOPE_SUBTREE, ++ mode='refreshAndPersist', ++ attrlist=['objectclass', 'cn', 'uid', 'sn'], ++ filterstr='(objectClass=person)', ++ cookie=None ++ ) ++ ++ while ldap_conn.syncrepl_poll(all=1, msgid=ldap_search): ++ pass ++ except ldap.OPERATIONS_ERROR as e: ++ log.error("SyncRepl got OPERATIONS_ERROR: %s" % e) ++ sync_error[0] = e ++ except (ldap.SERVER_DOWN, ldap.CONNECT_ERROR): ++ pass ++ finally: ++ if ldap_conn: ++ sync_cookies.extend(ldap_conn.get_cookies()) ++ ++ t = threading.Thread(target=sync_thread_func) ++ t.daemon = True ++ t.start() ++ time.sleep(5) ++ ++ # Add entries while persistent search is active (as directory manager) ++ test_users = [] ++ for i in range(20001, 20004): ++ test_users.append(users.create_test_user(uid=i)) ++ time.sleep(5) ++ ++ # Stop the server to end the sync thread cleanly ++ inst.stop() ++ t.join(timeout=30) ++ ++ # Verify no OPERATIONS_ERROR was raised ++ assert sync_error[0] is None, \ ++ "SyncRepl persistent search as non-root user got OPERATIONS_ERROR: %s" % sync_error[0] ++ ++ # Verify the sync client received some cookies (entries were synced) ++ assert len(sync_cookies) > 0, \ ++ "SyncRepl persistent search as non-root user received no cookies" ++ ++ # Restart and check error log for the specific aclpb error ++ inst.start() ++ assert not inst.searchErrorsLog('Missing aclpb'), \ ++ "Found 'Missing aclpb' errors in the error log" ++ ++ log.info('test_sync_repl_non_root_persist: PASS') ++ ++ def fin(): ++ inst.restart() ++ try: ++ inst.modify_s(DEFAULT_SUFFIX, ++ [(ldap.MOD_DELETE, 'aci', ensure_bytes(ACI))]) ++ except: ++ pass ++ for user in test_users: ++ try: ++ user.delete() ++ except: ++ pass ++ try: ++ sync_user.delete() ++ except: ++ pass ++ ++ request.addfinalizer(fin) +diff --git a/ldap/servers/plugins/sync/sync_init.c b/ldap/servers/plugins/sync/sync_init.c +index 49c7387bf..28c0edf06 100644 +--- a/ldap/servers/plugins/sync/sync_init.c ++++ b/ldap/servers/plugins/sync/sync_init.c +@@ -20,6 +20,28 @@ static int sync_persist_register_operation_extension(void); + + static PRUintn thread_primary_op; + ++/* ++ * Destructor for the per-thread primary operation list HEAD node. ++ * Called by NSPR when a thread exits, to free the HEAD node ++ * and any remaining linked list entries. ++ */ ++static void ++sync_thread_primary_op_destructor(void *priv) ++{ ++ OPERATION_PL_CTX_T *head = (OPERATION_PL_CTX_T *)priv; ++ if (head) { ++ OPERATION_PL_CTX_T *curr_op = head->next; ++ while (curr_op) { ++ OPERATION_PL_CTX_T *next = curr_op->next; ++ slapi_entry_free(curr_op->entry); ++ slapi_entry_free(curr_op->eprev); ++ slapi_ch_free((void **)&curr_op); ++ curr_op = next; ++ } ++ slapi_ch_free((void **)&head); ++ } ++} ++ + int + sync_init(Slapi_PBlock *pb) + { +@@ -187,7 +209,7 @@ sync_start(Slapi_PBlock *pb) + * only contains one operation. For nested, the list contains the operations + * in the order that they were applied + */ +- PR_NewThreadPrivateIndex(&thread_primary_op, NULL); ++ PR_NewThreadPrivateIndex(&thread_primary_op, sync_thread_primary_op_destructor); + sync_persist_initialize(argc, argv); + + return (0); +diff --git a/ldap/servers/plugins/sync/sync_persist.c b/ldap/servers/plugins/sync/sync_persist.c +index 283607361..a78c273cd 100644 +--- a/ldap/servers/plugins/sync/sync_persist.c ++++ b/ldap/servers/plugins/sync/sync_persist.c +@@ -43,6 +43,69 @@ static void sync_node_free(SyncQueueNode **node); + static int sync_acquire_connection(Slapi_Connection *conn); + static int sync_release_connection(Slapi_PBlock *pb, Slapi_Connection *conn, Slapi_Operation *op, int release); + ++/* ++ * Free all resources owned by a SyncRequest and the request itself. ++ * Caller must remove the request from the list (sync_remove_request) ++ * before calling this, if it was added. ++ */ ++static void ++sync_request_free(SyncRequest **reqp) ++{ ++ SyncRequest *req; ++ SyncQueueNode *qnode, *qnodenext; ++ ++ if (reqp == NULL || *reqp == NULL) { ++ return; ++ } ++ req = *reqp; ++ ++ if (req->req_pblock) { ++ char **attrs_dup = NULL; ++ char *strFilter = NULL; ++ LDAPControl **ctrls = NULL; ++ Slapi_DN *sdn = NULL; ++ ++ slapi_pblock_get(req->req_pblock, SLAPI_SEARCH_TARGET_SDN, &sdn); ++ slapi_sdn_free(&sdn); ++ slapi_pblock_set(req->req_pblock, SLAPI_SEARCH_TARGET_SDN, NULL); ++ ++ slapi_pblock_get(req->req_pblock, SLAPI_SEARCH_ATTRS, &attrs_dup); ++ slapi_ch_array_free(attrs_dup); ++ slapi_pblock_set(req->req_pblock, SLAPI_SEARCH_ATTRS, NULL); ++ ++ slapi_pblock_get(req->req_pblock, SLAPI_SEARCH_STRFILTER, &strFilter); ++ slapi_ch_free((void **)&strFilter); ++ slapi_pblock_set(req->req_pblock, SLAPI_SEARCH_STRFILTER, NULL); ++ ++ slapi_pblock_get(req->req_pblock, SLAPI_REQCONTROLS, &ctrls); ++ if (ctrls) { ++ ldap_controls_free(ctrls); ++ slapi_pblock_set(req->req_pblock, SLAPI_REQCONTROLS, NULL); ++ } ++ ++ slapi_pblock_destroy(req->req_pblock); ++ req->req_pblock = NULL; ++ } ++ ++ slapi_ch_free((void **)&req->req_orig_base); ++ slapi_filter_free(req->req_filter, 1); ++ req->req_filter = NULL; ++ ++ for (qnode = req->ps_eq_head; qnode; qnode = qnodenext) { ++ qnodenext = qnode->sync_next; ++ sync_node_free(&qnode); ++ } ++ req->ps_eq_head = NULL; ++ req->ps_eq_tail = NULL; ++ ++ if (req->req_lock) { ++ PR_DestroyLock(req->req_lock); ++ req->req_lock = NULL; ++ } ++ ++ slapi_ch_free((void **)reqp); ++} ++ + /* This routine appends the operation at the end of the + * per thread pending list of nested operation.. + * being a betxn_preop the pending list has the same order +@@ -664,10 +727,7 @@ sync_persist_add(Slapi_PBlock *pb) + prerr, slapi_pr_strerror(prerr)); + /* Now remove the ps from the list so call the function ps_remove */ + sync_remove_request(req); +- PR_DestroyLock(req->req_lock); +- req->req_lock = NULL; +- slapi_ch_free((void **)&req->req_pblock); +- slapi_ch_free((void **)&req); ++ sync_request_free(&req); + } else { + thread_count++; + return (req->req_tid); +@@ -753,11 +813,7 @@ sync_persist_terminate_all() + /* it frees the structures, just in case it remained connected sync_repl client */ + for (req = sync_request_list->sync_req_head; NULL != req; req = next) { + next = req->req_next; +- slapi_pblock_destroy(req->req_pblock); +- req->req_pblock = NULL; +- PR_DestroyLock(req->req_lock); +- req->req_lock = NULL; +- slapi_ch_free((void **)&req); ++ sync_request_free(&req); + } + slapi_ch_free((void **)&sync_request_list); + } +@@ -1060,34 +1116,7 @@ sync_send_results(void *arg) + done: + /* This client closed the connection or shutdown, free the req */ + sync_remove_request(req); +- PR_DestroyLock(req->req_lock); +- req->req_lock = NULL; +- +- slapi_pblock_get(req->req_pblock, SLAPI_SEARCH_ATTRS, &attrs_dup); +- slapi_ch_array_free(attrs_dup); +- slapi_pblock_set(req->req_pblock, SLAPI_SEARCH_ATTRS, NULL); +- +- slapi_pblock_get(req->req_pblock, SLAPI_SEARCH_STRFILTER, &strFilter); +- slapi_ch_free((void **)&strFilter); +- slapi_pblock_set(req->req_pblock, SLAPI_SEARCH_STRFILTER, NULL); +- +- slapi_pblock_get(req->req_pblock, SLAPI_REQCONTROLS, &ctrls); +- if (ctrls) { +- ldap_controls_free(ctrls); +- slapi_pblock_set(req->req_pblock, SLAPI_REQCONTROLS, NULL); +- } +- +- slapi_pblock_destroy(req->req_pblock); +- req->req_pblock = NULL; +- +- slapi_ch_free((void **)&req->req_orig_base); +- slapi_filter_free(req->req_filter, 1); +- sync_cookie_free(&req->req_cookie); +- for (qnode = req->ps_eq_head; qnode; qnode = qnodenext) { +- qnodenext = qnode->sync_next; +- sync_node_free(&qnode); +- } +- slapi_ch_free((void **)&req); ++ sync_request_free(&req); + thread_count--; + } + +-- +2.52.0 + diff --git a/SOURCES/0089-Issue-7126-WARN-keys2idl-received-NULL-idl-from-inde.patch b/SOURCES/0089-Issue-7126-WARN-keys2idl-received-NULL-idl-from-inde.patch new file mode 100644 index 0000000..68473be --- /dev/null +++ b/SOURCES/0089-Issue-7126-WARN-keys2idl-received-NULL-idl-from-inde.patch @@ -0,0 +1,197 @@ +From f02302eb0967647fc0bb08b9de9900f6c9b47396 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Thu, 2 Apr 2026 11:44:19 +0200 +Subject: [PATCH 02/13] Issue 7126 - WARN - keys2idl - received NULL idl from + index_read_ext_allids (#7127) + +Bug Description: +Under high concurrency, occasional NULL IDL warnings were logged: + +[29/Nov/2025:22:59:13.930441639 +0000] - WARN - keys2idl - received NULL idl from index_read_ext_allids, treating as empty set +[29/Nov/2025:22:59:13.936543122 +0000] - WARN - keys2idl - this is probably a bug that should be reported +[29/Nov/2025:22:59:13.947131944 +0000] - ERR - build_candidate_list - Database error -12795 + +Since #7124 all cursor operations use transaction isolation, which +increases read-write lock contention under high concurrency. Deadlocked +read transactions returned DBI_RC_RETRY, and after retry exhaustion +`index_read_ext_allids()` returned NULL to callers that did not expect it. + +Fix Description: +* Ensure `index_read_ext_allids()` never returns NULL. All early exits + and the retry-exhaustion path now return `idl_alloc(0)` (empty IDL). +* Increase IDL_FETCH_RETRY_COUNT from 5 to 10 and replace the uniform + random sleep with exponential backoff. + +Fixes: https://github.com/389ds/389-ds-base/issues/7126 + +Reviewed by: @tbordaz, @droideck, @progier389 (Thanks!) +--- + ldap/servers/slapd/back-ldbm/back-ldbm.h | 2 +- + .../slapd/back-ldbm/db-bdb/bdb_ldif2db.c | 5 +- + ldap/servers/slapd/back-ldbm/index.c | 51 +++++++++++++++---- + 3 files changed, 46 insertions(+), 12 deletions(-) + +diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h +index 462d1763c..25cbfb59b 100644 +--- a/ldap/servers/slapd/back-ldbm/back-ldbm.h ++++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h +@@ -165,7 +165,7 @@ typedef unsigned short u_int16_t; + #define SUBLEN 3 + #define LDBM_CACHE_RETRY_COUNT 1000 /* Number of times we re-try a cache operation */ + #define RETRY_CACHE_LOCK 2 /* error code to signal a retry of the cache lock */ +-#define IDL_FETCH_RETRY_COUNT 5 /* Number of times we re-try idl_fetch if it returns deadlock */ ++#define IDL_FETCH_RETRY_COUNT 10 /* Number of times we re-try idl_fetch if it returns deadlock */ + #define IMPORT_SUBCOUNT_HASHTABLE_SIZE 500 /* Number of buckets in hash used to accumulate subcount for broody parents */ + + /* minimum max ids that a single index entry can map to in ldbm */ +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_ldif2db.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_ldif2db.c +index 7972850e9..7d904df91 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_ldif2db.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_ldif2db.c +@@ -531,8 +531,8 @@ bdb_fetch_subtrees(backend *be, char **include, int *err) + *err = ldbm_ancestorid_read(be, txn, id, &idl); + } + slapi_sdn_done(&sdn); +- if (idl == NULL) { +- if (DB_NOTFOUND == *err) { ++ if (idl == NULL || IDL_NIDS(idl) == 0) { ++ if (*err == 0 || DB_NOTFOUND == *err) { + slapi_log_err(SLAPI_LOG_BACKLDBM, + "bdb_fetch_subtrees", "Entry id %u has no descendants according to %s. " + "Index file created by this reindex will be empty.\n", +@@ -543,6 +543,7 @@ bdb_fetch_subtrees(backend *be, char **include, int *err) + "bdb_fetch_subtrees", "%s not indexed on %u\n", + entryrdn_get_noancestorid() ? "entryrdn" : "ancestorid", id); + } ++ idl_free(&idl); + continue; + } + +diff --git a/ldap/servers/slapd/back-ldbm/index.c b/ldap/servers/slapd/back-ldbm/index.c +index 30fa09ebb..b22dda3c3 100644 +--- a/ldap/servers/slapd/back-ldbm/index.c ++++ b/ldap/servers/slapd/back-ldbm/index.c +@@ -932,7 +932,7 @@ index_read_ext_allids( + prefix = index_index2prefix(indextype); + if (prefix == NULL) { + slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids", "NULL prefix\n"); +- return NULL; ++ return idl_alloc(0); + } + if (slapi_is_loglevel_set(LDAP_DEBUG_TRACE)) { + slapi_log_err(SLAPI_LOG_TRACE, "index_read_ext_allids", "=> ( \"%s\" %s \"%s\" )\n", +@@ -948,7 +948,7 @@ index_read_ext_allids( + if (ai == NULL) { + index_free_prefix(prefix); + slapi_ch_free_string(&basetmp); +- return NULL; ++ return idl_alloc(0); + } + + slapi_log_err(SLAPI_LOG_ARGS, "index_read_ext_allids", "indextype: \"%s\" indexmask: 0x%x\n", +@@ -967,7 +967,7 @@ index_read_ext_allids( + slapi_ch_free_string(&basetmp); + if (NULL == val || NULL == val->bv_val) { + /* entrydn value was not given */ +- return NULL; ++ return idl_alloc(0); + } + slapi_sdn_init_dn_byval(&sdn, val->bv_val); + rc = entryrdn_index_read(be, &sdn, &id, txn); +@@ -976,11 +976,11 @@ index_read_ext_allids( + /* return an empty list */ + return idl_alloc(0); + } else if (rc) { /* failure */ +- return NULL; ++ return idl_alloc(0); + } else { /* success */ + rc = idl_append_extend(&idl, id); + if (rc) { /* failure */ +- return NULL; ++ return idl_alloc(0); + } + return idl; + } +@@ -1022,11 +1022,11 @@ index_read_ext_allids( + } + if ((*err = dblayer_get_index_file(be, ai, &db, DBOPEN_CREATE)) != 0) { + slapi_log_err(SLAPI_LOG_TRACE, "index_read_ext_allids", +- "<= NULL (index file open for attr %s)\n", ++ "<= empty IDL (index file open for attr %s)\n", + basetype); + index_free_prefix(prefix); + slapi_ch_free_string(&basetmp); +- return (NULL); ++ return idl_alloc(0); + } + + if (val != NULL) { +@@ -1067,12 +1067,32 @@ index_read_ext_allids( + idl = idl_fetch_ext(be, db, &key, db_txn, ai, err, allidslimit); + if (*err == DB_LOCK_DEADLOCK) { + ldbm_nasty("index_read_ext_allids", "index read retrying transaction", 1045, *err); ++ slapi_log_err(SLAPI_LOG_BACKLDBM, "index_read_ext_allids", ++ "DBI_RC_RETRY on retry %d/%d for %s\n", ++ retry_count + 1, IDL_FETCH_RETRY_COUNT, basetype); + #ifdef FIX_TXN_DEADLOCKS + #error can only retry here if txn == NULL - otherwise, have to abort and retry txn + #endif +- interval = PR_MillisecondsToInterval(slapi_rand() % 100); ++ /* Exponential backoff with jitter, capped at 200ms */ ++ { ++ PRUint32 backoff_ms = 10; ++ int i; ++ for (i = 0; i < retry_count && backoff_ms < 200; i++) { ++ backoff_ms *= 2; ++ } ++ if (backoff_ms > 200) { ++ backoff_ms = 200; ++ } ++ backoff_ms += slapi_rand() % (backoff_ms + 1); ++ interval = PR_MillisecondsToInterval(backoff_ms); ++ } + DS_Sleep(interval); + continue; ++ } else if (*err == DB_NOTFOUND) { ++ /* Key not found in index - this is normal, not an error */ ++ idl_free(&idl); ++ idl = idl_alloc(0); ++ break; + } else if (*err != 0 || idl == NULL) { + /* The database might not exist. We have to assume it means empty set */ + slapi_log_err(SLAPI_LOG_TRACE, "index_read_ext_allids", "Failed to access idl index for %s\n", basetype); +@@ -1086,10 +1106,13 @@ index_read_ext_allids( + } + if (retry_count == IDL_FETCH_RETRY_COUNT) { + ldbm_nasty("index_read_ext_allids", "index_read retry count exceeded", 1046, *err); ++ /* Ensure we don't return NULL after exhausting retries */ ++ if (idl == NULL) { ++ idl = idl_alloc(0); ++ } + } else if (*err != 0 && *err != DB_NOTFOUND) { + ldbm_nasty("index_read_ext_allids", errmsg, 1050, *err); + } +- slapi_ch_free_string(&basetmp); + slapi_ch_free_string(&tmpbuf); + + dblayer_release_index_file(be, ai, db); +@@ -1100,6 +1123,16 @@ index_read_ext_allids( + ber_bvfree(encrypted_val); + } + ++ /* Ensure we never return NULL - always return valid IDL */ ++ if (idl == NULL) { ++ slapi_log_err(SLAPI_LOG_WARNING, "index_read_ext_allids", ++ "Returning empty IDL for %s after error %d\n", ++ basetype, *err); ++ idl = idl_alloc(0); ++ } ++ ++ slapi_ch_free_string(&basetmp); ++ + slapi_log_err(SLAPI_LOG_TRACE, "index_read_ext_allids", "<= %lu candidates\n", + (u_long)IDL_NIDS(idl)); + return (idl); +-- +2.52.0 + diff --git a/SOURCES/0090-Issue-7124-2nd-BDB-cursor-race-condition-with-transa.patch b/SOURCES/0090-Issue-7124-2nd-BDB-cursor-race-condition-with-transa.patch new file mode 100644 index 0000000..e311476 --- /dev/null +++ b/SOURCES/0090-Issue-7124-2nd-BDB-cursor-race-condition-with-transa.patch @@ -0,0 +1,65 @@ +From 92e89927c9ed213d24c79f3cc62dc65027e09b99 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Tue, 9 Dec 2025 11:46:11 +0100 +Subject: [PATCH 03/13] Issue 7124 - (2nd) BDB cursor race condition with + transaction isolation + +Description: +1.4.x. only. +Add checks to idl_new_fetch() and idl_new_range_fetch() if the +database's environment actually supports transactions. + +Fixes: https://github.com/389ds/389-ds-base/issues/7124 +--- + ldap/servers/slapd/back-ldbm/idl_new.c | 28 ++++++++++++++++++++++++++ + 1 file changed, 28 insertions(+) + +diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c +index 212f31b7b..205d0007e 100644 +--- a/ldap/servers/slapd/back-ldbm/idl_new.c ++++ b/ldap/servers/slapd/back-ldbm/idl_new.c +@@ -163,6 +163,20 @@ idl_new_fetch( + dblayer_read_txn_begin(be, txn, &s_txn); + } + ++ /* ++ * Verify the database's environment actually supports transactions. ++ * During import, databases are opened with a private environment that ++ * lacks DB_INIT_TXN. If we try to use a transaction from the main ++ * environment with a database from the import environment, BDB will fail. ++ * Clear the txn if the database's environment is NULL or doesn't support ++ * transactions. ++ */ ++ if (s_txn.back_txn_txn != NULL) { ++ if (db->dbenv == NULL || !dblayer_db_uses_transactions(db->dbenv)) { ++ s_txn.back_txn_txn = NULL; ++ } ++ } ++ + /* Make a cursor */ + ret = db->cursor(db, txn, &cursor, 0); + if (0 != ret) { +@@ -410,6 +424,20 @@ idl_new_range_fetch( + dblayer_read_txn_begin(be, txn, &s_txn); + } + ++ /* ++ * Verify the database's environment actually supports transactions. ++ * During import, databases are opened with a private environment that ++ * lacks DB_INIT_TXN. If we try to use a transaction from the main ++ * environment with a database from the import environment, BDB will fail. ++ * Clear the txn if the database's environment is NULL or doesn't support ++ * transactions. ++ */ ++ if (s_txn.back_txn_txn != NULL) { ++ if (db->dbenv == NULL || !dblayer_db_uses_transactions(db->dbenv)) { ++ s_txn.back_txn_txn = NULL; ++ } ++ } ++ + /* Make a cursor */ + ret = db->cursor(db, txn, &cursor, 0); + if (0 != ret) { +-- +2.52.0 + diff --git a/SOURCES/0091-Issue-7124-BDB-cursor-race-condition-with-transactio.patch b/SOURCES/0091-Issue-7124-BDB-cursor-race-condition-with-transactio.patch new file mode 100644 index 0000000..1a63535 --- /dev/null +++ b/SOURCES/0091-Issue-7124-BDB-cursor-race-condition-with-transactio.patch @@ -0,0 +1,682 @@ +From d53726c4c04e8d4844ffe0a78b9713e938e1a348 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Sat, 29 Nov 2025 22:45:21 +0100 +Subject: [PATCH 04/13] Issue 7124 - BDB cursor race condition with transaction + isolation (#7125) + +Bug Description: +ASAN reported crashes in `__db_ditem_nolog()` with negative-size-param +errors. Cursor operations without transaction isolation allowed +concurrent page modifications to corrupt cursor state, leading to +invalid memory access. + +The race condition occurs when: +1. T1 opens a cursor without transaction protection +2. T2 modifies the same index page +3. T1 cursor operates on stale page metadata +4. `__db_ditem_nolog()` calculates negative size for `memmove()` +5. Crash: `AddressSanitizer: negative-size-param: (size=-8)` + +Reproducer: dirsrvtests/tests/stress/backend/bdb_cursor_race_test.py +Crash under ASAN usually happens within 10-30 minutes, but sometimes it +can run for hours without any crash. + +Fix Description: +Implement transaction isolation for cursors in `idl_new_fetch()` and +`idl_new_range_fetch()` by always calling `dblayer_read_txn_begin()`. + +In `bdb_txn_begin()` verify if the environment supports transactions +(has DB_INIT_TXN flag) before attempting to begin a transaction. +This prevents errors during offline import which uses a private +environment without transaction support. + +In `bdb_public_new_cursor()` skip transaction usage when the database's +environment doesn't support transactions. + +Fixes: https://github.com/389ds/389-ds-base/issues/7124 + +Reviewed by: @progier389, @tbordaz (Thanks!) +--- + .../stress/backend/bdb_cursor_race_test.py | 544 ++++++++++++++++++ + .../slapd/back-ldbm/db-bdb/bdb_layer.c | 13 +- + ldap/servers/slapd/back-ldbm/idl_new.c | 24 +- + 3 files changed, 571 insertions(+), 10 deletions(-) + create mode 100644 dirsrvtests/tests/stress/backend/bdb_cursor_race_test.py + +diff --git a/dirsrvtests/tests/stress/backend/bdb_cursor_race_test.py b/dirsrvtests/tests/stress/backend/bdb_cursor_race_test.py +new file mode 100644 +index 000000000..54a20e000 +--- /dev/null ++++ b/dirsrvtests/tests/stress/backend/bdb_cursor_race_test.py +@@ -0,0 +1,544 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++ ++import pytest ++import logging ++import ldap ++import ldap.modlist as modlist ++from threading import Thread, Event, Lock ++import random ++import time ++from lib389.topologies import topology_st ++from lib389._constants import DEFAULT_SUFFIX, DN_DM, PW_DM ++from lib389.idm.user import UserAccounts ++from lib389.idm.organizationalunit import OrganizationalUnits ++ ++pytestmark = pytest.mark.tier2 ++ ++log = logging.getLogger(__name__) ++ ++# Test configuration ++SEARCH_THREADS = 30 ++MODIFY_THREADS = 15 ++DELETE_THREADS = 8 ++CHURN_THREADS = 5 ++DEFAULT_TIMEOUT = 60 * 60 # 60 minutes in seconds ++ ++# Statistics and crash detection ++stats_lock = Lock() ++stats = { ++ 'searches': 0, ++ 'modifies': 0, ++ 'deletes': 0, ++ 'adds': 0, ++ 'errors': 0, ++ 'start_time': None ++} ++ ++# Global flag to track if server crashed ++server_crashed = False ++crash_lock = Lock() ++ ++HIGH_CONTENTION_FILTERS = [ ++ "(objectClass=inetOrgPerson)", ++ "(objectClass=person)", ++ "(objectClass=*)", ++ "(uid=*)", ++ "(cn=*)", ++ "(mail=*)", ++ "(description=*)", ++ "(telephoneNumber=*)", ++] ++ ++ ++def create_raw_connection(host, port): ++ """Create a raw LDAP connection independent of the instance connection pool ++ ++ :param host: LDAP server hostname ++ :param port: LDAP server port ++ :returns: Raw LDAP connection ++ """ ++ # Use ldap.initialize to create a completely independent connection ++ uri = f"ldap://{host}:{port}" ++ conn = ldap.initialize(uri) ++ conn.protocol_version = ldap.VERSION3 ++ ++ # Short timeout to fail fast ++ conn.set_option(ldap.OPT_NETWORK_TIMEOUT, 5.0) ++ conn.set_option(ldap.OPT_TIMEOUT, 5.0) ++ ++ # Bind as Directory Manager ++ conn.simple_bind_s(DN_DM, PW_DM) ++ return conn ++ ++ ++def update_stats(operation, count=1): ++ """Update operation statistics in a thread-safe manner""" ++ with stats_lock: ++ stats[operation] = stats.get(operation, 0) + count ++ ++ ++def mark_server_crashed(): ++ """Mark that the server has crashed""" ++ global server_crashed ++ with crash_lock: ++ server_crashed = True ++ ++ ++def print_stats(): ++ """Print current statistics""" ++ with stats_lock: ++ if stats['start_time']: ++ elapsed = time.time() - stats['start_time'] ++ log.info(f"Statistics after {elapsed:.1f}s: " ++ f"searches={stats['searches']}, " ++ f"modifies={stats['modifies']}, " ++ f"adds={stats['adds']}, " ++ f"deletes={stats['deletes']}, " ++ f"errors={stats['errors']}") ++ ++ ++def setup_test_data(inst, num_entries=1000): ++ """Setup test data with multi-valued attributes ++ ++ :param inst: DirSrv instance ++ :param num_entries: Number of test entries to create ++ """ ++ log.info(f"Setting up {num_entries} test entries with multi-valued attributes...") ++ ++ # Create entries with many multi-valued attributes using raw LDAP ++ created = 0 ++ for i in range(num_entries): ++ try: ++ dn = f"uid=test{i},ou=people,{DEFAULT_SUFFIX}" ++ ++ descriptions = [f"description_{i}_{j}_{'x'*50}".encode() for j in range(20)] ++ phones = [f"+1-555-{i:04d}-{j:04d}".encode() for j in range(10)] ++ mails = [f"test{i}.{j}@example.com".encode() for j in range(5)] ++ ++ attrs = { ++ 'objectClass': [b'top', b'person', b'organizationalPerson', b'inetOrgPerson'], ++ 'uid': [f'test{i}'.encode()], ++ 'cn': [f'Test User {i}'.encode()], ++ 'sn': [f'User{i}'.encode()], ++ 'description': descriptions, ++ 'telephoneNumber': phones, ++ 'mail': mails, ++ 'userPassword': [b'password'], ++ } ++ ++ inst.add_s(dn, modlist.addModlist(attrs)) ++ created += 1 ++ ++ if (i + 1) % 100 == 0: ++ log.info(f"Created {i + 1}/{num_entries} entries...") ++ ++ except ldap.ALREADY_EXISTS: ++ pass ++ except Exception as e: ++ log.warning(f"Error creating entry {i}: {e}") ++ ++ log.info(f"Test data setup complete. Created {created} new entries.") ++ ++ ++def search_worker(stop_event, host, port, thread_id): ++ """Worker thread for high-contention searches ++ ++ :param stop_event: Event to signal thread termination ++ :param host: LDAP server hostname ++ :param port: LDAP server port ++ :param thread_id: Thread identifier ++ """ ++ # Create independent raw LDAP connection for this thread ++ conn = create_raw_connection(host, port) ++ log.info(f"Search worker {thread_id} started") ++ search_count = 0 ++ ++ try: ++ while not stop_event.is_set(): ++ try: ++ filter_str = random.choice(HIGH_CONTENTION_FILTERS) ++ scope = random.choice([ldap.SCOPE_SUBTREE, ldap.SCOPE_ONELEVEL]) ++ ++ conn.search_s( ++ DEFAULT_SUFFIX, ++ scope, ++ filter_str, ++ attrlist=['dn'] ++ ) ++ ++ search_count += 1 ++ ++ if search_count % 100 == 0: ++ update_stats('searches', 100) ++ ++ except ldap.SERVER_DOWN: ++ log.error(f"Search worker {thread_id}: SERVER DOWN - ns-slapd crashed!") ++ mark_server_crashed() ++ stop_event.set() ++ break ++ except Exception as e: ++ update_stats('errors') ++ if search_count < 10: ++ log.debug(f"Search worker {thread_id} error: {e}") ++ finally: ++ try: ++ conn.unbind_s() ++ except: ++ pass ++ log.info(f"Search worker {thread_id} stopped (total searches: {search_count})") ++ ++ ++def modify_worker(stop_event, host, port, thread_id): ++ """Worker thread for modifying multi-valued attributes ++ ++ :param stop_event: Event to signal thread termination ++ :param host: LDAP server hostname ++ :param port: LDAP server port ++ :param thread_id: Thread identifier ++ """ ++ # Create independent raw LDAP connection for this thread ++ conn = create_raw_connection(host, port) ++ log.info(f"Modify worker {thread_id} started") ++ modify_count = 0 ++ ++ try: ++ while not stop_event.is_set(): ++ try: ++ entry_num = random.randint(0, 999) ++ dn = f"uid=test{entry_num},ou=people,{DEFAULT_SUFFIX}" ++ attr = 'description' ++ value = f"{attr}_{int(time.time() * 1000000)}_{'x'*100}" ++ ++ # Strategy 1: Add a new value (increases index size) ++ if random.random() < 0.3: ++ mod_attrs = [(ldap.MOD_ADD, attr, [value.encode()])] ++ ++ # Strategy 2: Replace all values (causes delete + add) ++ # This should trigger __db_ditem_nolog() in the delete phase ++ elif random.random() < 0.7: ++ new_values = [f"{attr}_new_{i}_{'y'*100}".encode() for i in range(20)] ++ mod_attrs = [(ldap.MOD_REPLACE, attr, new_values)] ++ ++ # Strategy 3: Delete all values, then add multiple new ones ++ else: ++ new_values = [f"{attr}_churn_{i}_{'z'*100}".encode() for i in range(15)] ++ mod_attrs = [ ++ (ldap.MOD_DELETE, attr, []), ++ (ldap.MOD_ADD, attr, new_values) ++ ] ++ ++ conn.modify_s(dn, mod_attrs) ++ modify_count += 1 ++ ++ if modify_count % 50 == 0: ++ update_stats('modifies', 50) ++ ++ except ldap.SERVER_DOWN: ++ log.error(f"Modify worker {thread_id}: SERVER DOWN - ns-slapd crashed!") ++ mark_server_crashed() ++ stop_event.set() ++ break ++ except ldap.NO_SUCH_OBJECT: ++ pass ++ except Exception as e: ++ update_stats('errors') ++ if modify_count < 10: ++ log.debug(f"Modify worker {thread_id} error: {e}") ++ finally: ++ try: ++ conn.unbind_s() ++ except: ++ pass ++ log.info(f"Modify worker {thread_id} stopped (total modifies: {modify_count})") ++ ++ ++def delete_worker(stop_event, host, port, thread_id): ++ """Worker thread for deleting entries ++ ++ :param stop_event: Event to signal thread termination ++ :param host: LDAP server hostname ++ :param port: LDAP server port ++ :param thread_id: Thread identifier ++ """ ++ # Create independent raw LDAP connection for this thread ++ conn = create_raw_connection(host, port) ++ log.info(f"Delete worker {thread_id} started") ++ delete_count = 0 ++ delete_range_start = 5000 + (thread_id * 1000) ++ ++ try: ++ while not stop_event.is_set(): ++ try: ++ entry_num = delete_range_start + (delete_count % 1000) ++ dn = f"uid=delete{entry_num},ou=people,{DEFAULT_SUFFIX}" ++ ++ conn.delete_s(dn) ++ delete_count += 1 ++ ++ if delete_count % 20 == 0: ++ update_stats('deletes', 20) ++ ++ time.sleep(0.01) ++ ++ except ldap.SERVER_DOWN: ++ log.error(f"Delete worker {thread_id}: SERVER DOWN - ns-slapd crashed!") ++ mark_server_crashed() ++ stop_event.set() ++ break ++ except ldap.NO_SUCH_OBJECT: ++ pass ++ except Exception as e: ++ update_stats('errors') ++ if delete_count < 10: ++ log.debug(f"Delete worker {thread_id} error: {e}") ++ finally: ++ try: ++ conn.unbind_s() ++ except: ++ pass ++ log.info(f"Delete worker {thread_id} stopped (total deletes: {delete_count})") ++ ++ ++def churn_worker(stop_event, host, port, thread_id): ++ """Worker thread for add/delete churn ++ ++ :param stop_event: Event to signal thread termination ++ :param host: LDAP server hostname ++ :param port: LDAP server port ++ :param thread_id: Thread identifier ++ """ ++ # Create independent raw LDAP connection for this thread ++ conn = create_raw_connection(host, port) ++ log.info(f"Churn worker {thread_id} started") ++ counter = 10000 + (thread_id * 10000) ++ add_count = 0 ++ ++ try: ++ while not stop_event.is_set(): ++ try: ++ dn = f"uid=churn{counter},ou=people,{DEFAULT_SUFFIX}" ++ ++ attrs = { ++ 'objectClass': [b'top', b'person', b'inetOrgPerson'], ++ 'uid': [f'churn{counter}'.encode()], ++ 'cn': [f'Churn {counter}'.encode()], ++ 'sn': [f'User{counter}'.encode()], ++ 'description': [f'desc_{i}'.encode() for i in range(15)], ++ 'telephoneNumber': [f'+1-555-churn-{i:04d}'.encode() for i in range(8)], ++ 'userPassword': [b'password'], ++ } ++ ++ conn.add_s(dn, modlist.addModlist(attrs)) ++ add_count += 1 ++ update_stats('adds') ++ ++ time.sleep(0.005) ++ ++ conn.delete_s(dn) ++ update_stats('deletes') ++ ++ counter += 1 ++ ++ time.sleep(0.01) ++ ++ except ldap.SERVER_DOWN: ++ log.error(f"Churn worker {thread_id}: SERVER DOWN - ns-slapd crashed!") ++ mark_server_crashed() ++ stop_event.set() ++ break ++ except ldap.ALREADY_EXISTS: ++ try: ++ conn.delete_s(dn) ++ except: ++ pass ++ counter += 1 ++ except Exception as e: ++ update_stats('errors') ++ if add_count < 10: ++ log.debug(f"Churn worker {thread_id} error: {e}") ++ counter += 1 ++ finally: ++ try: ++ conn.unbind_s() ++ except: ++ pass ++ log.info(f"Churn worker {thread_id} stopped (total adds: {add_count})") ++ ++ ++def monitor_worker(stop_event, duration): ++ """Worker thread to monitor progress and enforce timeout ++ ++ :param stop_event: Event to signal thread termination ++ :param duration: Test duration in seconds ++ """ ++ log.info(f"Monitor worker started (duration: {duration}s / {duration/60:.1f}m)") ++ ++ start_time = time.time() ++ last_stats_time = start_time ++ ++ while not stop_event.is_set(): ++ current_time = time.time() ++ elapsed = current_time - start_time ++ ++ # Print stats every 30 seconds ++ if current_time - last_stats_time >= 30: ++ print_stats() ++ log.info(f"Progress: {elapsed:.0f}s / {duration}s ({elapsed/duration*100:.1f}%)") ++ last_stats_time = current_time ++ ++ # Check if duration has been reached ++ if elapsed >= duration: ++ log.info(f"Duration {duration}s reached. Test completed successfully!") ++ stop_event.set() ++ break ++ ++ time.sleep(1) ++ ++ log.info("Monitor worker stopped") ++ ++ ++@pytest.mark.slow ++def test_bdb_cursor_race(topology_st): ++ """Stress test to reproduce BDB cursor race condition ++ ++ :id: d53a9580-58e5-454c-96fd-57839660fd51 ++ :setup: Standalone instance ++ :steps: ++ 1. Setup test data with 1000 entries containing multi-valued attributes ++ 2. Start search, modify, delete and churn threads ++ 3. Run for 60 minutes ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Server doesn't crash ++ """ ++ ++ inst = topology_st.standalone ++ ++ log.info("=" * 80) ++ log.info("BDB Cursor Race Condition Test") ++ log.info(f"Instance: {inst.serverid}") ++ log.info(f"Base DN: {DEFAULT_SUFFIX}") ++ log.info(f"Duration: {DEFAULT_TIMEOUT}s ({DEFAULT_TIMEOUT/60:.0f} minutes)") ++ log.info("=" * 80) ++ ++ # Enable log buffering for better performance ++ log.info("Enabling log buffering for access and error logs...") ++ inst.config.set('nsslapd-accesslog-logbuffering', 'on') ++ inst.config.set('nsslapd-errorlog-logbuffering', 'on') ++ ++ # Setup test data ++ setup_test_data(inst, num_entries=1000) ++ ++ # Extract connection parameters once to avoid sharing inst object ++ ldap_host = inst.host ++ ldap_port = inst.port ++ ++ # Reset statistics and crash flag ++ global stats, server_crashed ++ stats = { ++ 'searches': 0, ++ 'modifies': 0, ++ 'deletes': 0, ++ 'adds': 0, ++ 'errors': 0, ++ 'start_time': time.time() ++ } ++ server_crashed = False ++ ++ # Create stop event ++ stop_event = Event() ++ ++ # Start all worker threads ++ threads = [] ++ ++ # Search workers - hammer equality indexes ++ for i in range(SEARCH_THREADS): ++ t = Thread(target=search_worker, ++ args=(stop_event, ldap_host, ldap_port, i), ++ name=f"search-{i}") ++ t.daemon = True ++ t.start() ++ threads.append(t) ++ ++ # Modify workers - update multi-valued attributes ++ for i in range(MODIFY_THREADS): ++ t = Thread(target=modify_worker, ++ args=(stop_event, ldap_host, ldap_port, i), ++ name=f"modify-{i}") ++ t.daemon = True ++ t.start() ++ threads.append(t) ++ ++ # Delete workers - trigger index cleanup ++ for i in range(DELETE_THREADS): ++ t = Thread(target=delete_worker, ++ args=(stop_event, ldap_host, ldap_port, i), ++ name=f"delete-{i}") ++ t.daemon = True ++ t.start() ++ threads.append(t) ++ ++ # Churn workers - add/delete cycle ++ for i in range(CHURN_THREADS): ++ t = Thread(target=churn_worker, ++ args=(stop_event, ldap_host, ldap_port, i), ++ name=f"churn-{i}") ++ t.daemon = True ++ t.start() ++ threads.append(t) ++ ++ # Monitor worker - enforce timeout and print stats ++ monitor_thread = Thread(target=monitor_worker, ++ args=(stop_event, DEFAULT_TIMEOUT), ++ name="monitor") ++ monitor_thread.daemon = True ++ monitor_thread.start() ++ threads.append(monitor_thread) ++ ++ log.info("=" * 80) ++ log.info(f"Started {len(threads)} worker threads:") ++ log.info(f" - {SEARCH_THREADS} search threads (equality index queries)") ++ log.info(f" - {MODIFY_THREADS} modify threads (multi-valued attr updates)") ++ log.info(f" - {DELETE_THREADS} delete threads (index cleanup)") ++ log.info(f" - {CHURN_THREADS} churn threads (add/delete cycle)") ++ log.info(f" - 1 monitor thread (timeout enforcement)") ++ log.info("=" * 80) ++ log.info(f"Running for {DEFAULT_TIMEOUT}s ({DEFAULT_TIMEOUT/60:.0f} minutes)...") ++ log.info("=" * 80) ++ ++ # Wait for all threads to finish or timeout ++ for t in threads: ++ t.join() ++ ++ # Final statistics ++ log.info("=" * 80) ++ log.info("Final Statistics:") ++ print_stats() ++ log.info("=" * 80) ++ ++ # Check if server crashed during the test ++ if server_crashed: ++ log.error("=" * 80) ++ log.error("Test FAILED: Server crashed during the test!") ++ log.error("=" * 80) ++ pytest.fail("ns-slapd crashed during BDB cursor race test") ++ else: ++ log.info("=" * 80) ++ log.info("Test PASSED: Completed full duration without crash") ++ log.info("=" * 80) ++ ++ ++if __name__ == '__main__': ++ # Run isolated ++ # -s for DEBUG mode ++ import os ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main(["-s", "-v", CURRENT_FILE]) +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +index 651e595ee..a2dcc61e1 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +@@ -2634,10 +2634,19 @@ bdb_txn_begin(struct ldbminfo *li, back_txnid parent_txn, back_txn *txn, PRBool + txn->back_txn_txn = NULL; + } + +- if (conf->bdb_enable_transactions) { ++ bdb_db_env *pEnv = (bdb_db_env *)priv->dblayer_env; ++ ++ /* ++ * Check both config and actual environment capabilities before starting a transaction. ++ * During offline import, the environment is created without DB_INIT_TXN even though ++ * bdb_enable_transactions may be true in config. We use bdb_openflags (cached at ++ * environment open time) rather than get_open_flags() to safely handle cases where ++ * the environment handle exists but may not be fully opened yet. ++ */ ++ if (pEnv && pEnv->bdb_DB_ENV && conf->bdb_enable_transactions && ++ (pEnv->bdb_openflags & DB_INIT_TXN)) { + int txn_begin_flags; + +- bdb_db_env *pEnv = (bdb_db_env *)priv->dblayer_env; + if (use_lock) + slapi_rwlock_rdlock(pEnv->bdb_env_lock); + if (!parent_txn) { +diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c +index 205d0007e..fff2c805e 100644 +--- a/ldap/servers/slapd/back-ldbm/idl_new.c ++++ b/ldap/servers/slapd/back-ldbm/idl_new.c +@@ -158,10 +158,14 @@ idl_new_fetch( + return NULL; + } + ++ /* ++ * Always use transaction isolation for cursor operations to prevent ++ * race conditions. Without a transaction, concurrent modifications to ++ * index pages can corrupt cursor state, leading to crashes in BDB's ++ * internal functions (e.g., negative size passed to memmove). ++ */ + dblayer_txn_init(li, &s_txn); +- if (txn) { +- dblayer_read_txn_begin(be, txn, &s_txn); +- } ++ dblayer_read_txn_begin(be, txn, &s_txn); + + /* + * Verify the database's environment actually supports transactions. +@@ -178,7 +182,7 @@ idl_new_fetch( + } + + /* Make a cursor */ +- ret = db->cursor(db, txn, &cursor, 0); ++ ret = db->cursor(db, s_txn.back_txn_txn, &cursor, 0); + if (0 != ret) { + ldbm_nasty("idl_new_fetch", filename, 1, ret); + cursor = NULL; +@@ -419,10 +423,14 @@ idl_new_range_fetch( + return NULL; + } + ++ /* ++ * Always use transaction isolation for cursor operations to prevent ++ * race conditions. Without a transaction, concurrent modifications to ++ * index pages can corrupt cursor state, leading to crashes in BDB's ++ * internal functions (e.g., negative size passed to memmove). ++ */ + dblayer_txn_init(li, &s_txn); +- if (txn) { +- dblayer_read_txn_begin(be, txn, &s_txn); +- } ++ dblayer_read_txn_begin(be, txn, &s_txn); + + /* + * Verify the database's environment actually supports transactions. +@@ -439,7 +447,7 @@ idl_new_range_fetch( + } + + /* Make a cursor */ +- ret = db->cursor(db, txn, &cursor, 0); ++ ret = db->cursor(db, s_txn.back_txn_txn, &cursor, 0); + if (0 != ret) { + ldbm_nasty("idl_new_range_fetch", filename, 1, ret); + cursor = NULL; +-- +2.52.0 + diff --git a/SOURCES/0092-Issue-7380-Internal-op-with-negative-wtime-and-large.patch b/SOURCES/0092-Issue-7380-Internal-op-with-negative-wtime-and-large.patch new file mode 100644 index 0000000..c6aa9cd --- /dev/null +++ b/SOURCES/0092-Issue-7380-Internal-op-with-negative-wtime-and-large.patch @@ -0,0 +1,37 @@ +From 6a159091cce5a2dedef9b3b9b64dfb5b332ec9c4 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Wed, 1 Apr 2026 23:04:56 +0100 +Subject: [PATCH 05/13] Issue 7380 - Internal op with negative wtime and large + optime (#7381) + +Description: +The retro CL plugin triggers internal operations that log incorrect wtime and optime +to the server access logs.This happens because seq_internal_callback_pb() calls the +backend directly without setting the operation start time. + +Fix: +Add slapi_operation_set_time_started(op) to seq_internal_callback_pb() to record +when the operation actually starts processing. + +Fixes: https://github.com/389ds/389-ds-base/issues/7380 + +Reviewed by: @mreynolds389 (Thank you) +--- + ldap/servers/slapd/plugin_internal_op.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/ldap/servers/slapd/plugin_internal_op.c b/ldap/servers/slapd/plugin_internal_op.c +index a140e7988..38de6177e 100644 +--- a/ldap/servers/slapd/plugin_internal_op.c ++++ b/ldap/servers/slapd/plugin_internal_op.c +@@ -378,6 +378,7 @@ seq_internal_callback_pb(Slapi_PBlock *pb, void *callback_data, plugin_result_ca + set_common_params(pb); + + slapi_td_internal_op_start(); ++ slapi_operation_set_time_started(op); + if (be->be_seq != NULL) { + rc = (*be->be_seq)(pb); + } else { +-- +2.52.0 + diff --git a/SOURCES/0093-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch b/SOURCES/0093-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch new file mode 100644 index 0000000..d605333 --- /dev/null +++ b/SOURCES/0093-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch @@ -0,0 +1,82 @@ +From 159eb43433002378ded06af6c30ea7aed8c220ad Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Wed, 14 Jan 2026 17:55:29 +0100 +Subject: [PATCH 06/13] Issue 7152 - ns-slapd fails to shutdown when deferred + memberof update is in progress (#7187) + +Bug Description: +When a deferred memberof update is in progress during shutdown, the +backend operations (add, modify, delete, modrdn) wait in a polling loop +for the deferred task to complete. However, if the deferred thread exits +before clearing the SLAPI_DEFERRED_MEMBEROF flag, the loop becomes +infinite, causing the server to hang during shutdown. + +Fix Description: +Add additional check to the polling loops so they exit immediately when +the server is shutting down. + +Fixes: https://github.com/389ds/389-ds-base/issues/7152 + +Reviewed by: @tbordaz (Thanks!) +--- + ldap/servers/slapd/back-ldbm/ldbm_add.c | 2 +- + ldap/servers/slapd/back-ldbm/ldbm_delete.c | 2 +- + ldap/servers/slapd/back-ldbm/ldbm_modify.c | 2 +- + ldap/servers/slapd/back-ldbm/ldbm_modrdn.c | 2 +- + 4 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/ldap/servers/slapd/back-ldbm/ldbm_add.c b/ldap/servers/slapd/back-ldbm/ldbm_add.c +index bca660f22..226b846cb 100644 +--- a/ldap/servers/slapd/back-ldbm/ldbm_add.c ++++ b/ldap/servers/slapd/back-ldbm/ldbm_add.c +@@ -1451,7 +1451,7 @@ common_return: + if (deferred) { + delay = PR_MillisecondsToInterval(100); + } +- while (deferred) { ++ while (deferred && !g_get_shutdown()) { + DS_Sleep(delay); + slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred); + } +diff --git a/ldap/servers/slapd/back-ldbm/ldbm_delete.c b/ldap/servers/slapd/back-ldbm/ldbm_delete.c +index c0f2516de..cf79daa77 100644 +--- a/ldap/servers/slapd/back-ldbm/ldbm_delete.c ++++ b/ldap/servers/slapd/back-ldbm/ldbm_delete.c +@@ -1577,7 +1577,7 @@ diskfull_return: + if (deferred) { + delay = PR_MillisecondsToInterval(100); + } +- while (deferred) { ++ while (deferred && !g_get_shutdown()) { + DS_Sleep(delay); + slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred); + } +diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modify.c b/ldap/servers/slapd/back-ldbm/ldbm_modify.c +index 0c62c87f0..ccecce571 100644 +--- a/ldap/servers/slapd/back-ldbm/ldbm_modify.c ++++ b/ldap/servers/slapd/back-ldbm/ldbm_modify.c +@@ -1168,7 +1168,7 @@ common_return: + if (deferred) { + delay = PR_MillisecondsToInterval(100); + } +- while (deferred) { ++ while (deferred && !g_get_shutdown()) { + DS_Sleep(delay); + slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred); + } +diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c +index fe35991b5..31e2caa46 100644 +--- a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c ++++ b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c +@@ -1519,7 +1519,7 @@ common_return: + if (deferred) { + delay = PR_MillisecondsToInterval(100); + } +- while (deferred) { ++ while (deferred && !g_get_shutdown()) { + DS_Sleep(delay); + slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred); + } +-- +2.52.0 + diff --git a/SOURCES/0094-Issue-7271-plugins-that-create-threads-need-to-updat.patch b/SOURCES/0094-Issue-7271-plugins-that-create-threads-need-to-updat.patch new file mode 100644 index 0000000..8472c02 --- /dev/null +++ b/SOURCES/0094-Issue-7271-plugins-that-create-threads-need-to-updat.patch @@ -0,0 +1,86 @@ +From cb1caa0093381d7b52ea61931db7d33804a025b8 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Mon, 23 Feb 2026 12:37:26 -0500 +Subject: [PATCH 07/13] Issue 7271 - plugins that create threads need to update + active thread count + +Description: + +Plugins that create threads need to up to the global active thread count. +Otherwise when the server is being stopped the plugin's close function gets +called while these threads are still running and still using the plugin +configuration. This can lead to crashes. + +relates: https://github.com/389ds/389-ds-base/issues/7271 + +Reviewed by: progier & tbordaz (Thanks!!) +--- + ldap/servers/plugins/replication/repl5_protocol.c | 5 +++++ + ldap/servers/plugins/retrocl/retrocl_trim.c | 7 ++++++- + 2 files changed, 11 insertions(+), 1 deletion(-) + +diff --git a/ldap/servers/plugins/replication/repl5_protocol.c b/ldap/servers/plugins/replication/repl5_protocol.c +index dfdb69889..6cdbcc4f1 100644 +--- a/ldap/servers/plugins/replication/repl5_protocol.c ++++ b/ldap/servers/plugins/replication/repl5_protocol.c +@@ -23,6 +23,7 @@ + + #include "repl5.h" + #include "repl5_prot_private.h" ++#include "slap.h" + + #define PROTOCOL_5_INCREMENTAL 1 + #define PROTOCOL_5_TOTAL 2 +@@ -237,6 +238,8 @@ prot_thread_main(void *arg) + return; + } + ++ g_incr_active_threadcnt(); ++ + set_thread_private_agmtname(agmt_get_long_name(agmt)); + + done = 0; +@@ -301,6 +304,8 @@ prot_thread_main(void *arg) + done = 1; + } + } ++ ++ g_decr_active_threadcnt(); + } + + /* +diff --git a/ldap/servers/plugins/retrocl/retrocl_trim.c b/ldap/servers/plugins/retrocl/retrocl_trim.c +index 158f801cc..6514fc790 100644 +--- a/ldap/servers/plugins/retrocl/retrocl_trim.c ++++ b/ldap/servers/plugins/retrocl/retrocl_trim.c +@@ -243,6 +243,8 @@ trim_changelog(void) + + now_interval = slapi_current_rel_time_t(); /* monotonic time for interval */ + ++ g_incr_active_threadcnt(); ++ + PR_Lock(ts.ts_s_trim_mutex); + max_age = ts.ts_c_max_age; + trim_interval = ts.ts_c_trim_interval; +@@ -257,7 +259,7 @@ trim_changelog(void) + */ + done = 0; + now_maxage = slapi_current_utc_time(); /* real time for trim candidates */ +- while (!done && retrocl_trimming == 1) { ++ while (!done && retrocl_trimming == 1 && !slapi_is_shutting_down()) { + int did_delete; + + did_delete = 0; +@@ -309,6 +311,9 @@ trim_changelog(void) + "trim_changelog: removed %d change records\n", + num_deleted); + } ++ ++ g_decr_active_threadcnt(); ++ + return rc; + } + +-- +2.52.0 + diff --git a/SOURCES/0095-Issue-7271-implement-a-pre-close-plugin-function.patch b/SOURCES/0095-Issue-7271-implement-a-pre-close-plugin-function.patch new file mode 100644 index 0000000..3477c59 --- /dev/null +++ b/SOURCES/0095-Issue-7271-implement-a-pre-close-plugin-function.patch @@ -0,0 +1,257 @@ +From 0e98684fb9a4db555b92d5fd2c261f26e89dc16f Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Mon, 2 Mar 2026 14:31:29 -0500 +Subject: [PATCH 08/13] Issue 7271 - implement a pre-close plugin function + +Description: + +replication protocol could benefit from being notifioed the shutdown process +has started before calling the "close" plugin function. This would allow the +replication protocol to wake up and adjust its active thread count to prevent +a hang during shutdown. + +relates: https://github.com/389ds/389-ds-base/issues/7271 + +Reviewed by: progier, spichugi, and vashirov(Thanks!!!) +--- + ldap/servers/plugins/replication/repl5_init.c | 20 +++++++++++----- + ldap/servers/slapd/daemon.c | 6 ++++- + ldap/servers/slapd/pblock.c | 14 ++++++++++- + ldap/servers/slapd/plugin.c | 24 ++++++++++++++++++- + ldap/servers/slapd/proto-slap.h | 3 ++- + ldap/servers/slapd/slap.h | 3 ++- + ldap/servers/slapd/slapi-plugin.h | 5 ++-- + 7 files changed, 62 insertions(+), 13 deletions(-) + +diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c +index 9b6523a2e..dd4870746 100644 +--- a/ldap/servers/plugins/replication/repl5_init.c ++++ b/ldap/servers/plugins/replication/repl5_init.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -122,6 +122,8 @@ static PRUintn thread_private_agmtname; /* thread private index for logging*/ + static PRUintn thread_private_cache; + static PRUintn thread_primary_csn; + ++static int multisupplier_pre_stop(Slapi_PBlock *pb __attribute__((unused))); ++ + char * + get_thread_private_agmtname() + { +@@ -832,10 +834,6 @@ multimaster_stop(Slapi_PBlock *pb __attribute__((unused))) + int rc = 0; /* OK */ + + if (!multimaster_stopped_flag) { +- if (!is_ldif_dump) { +- /* Shut down replication agreements */ +- agmtlist_shutdown(); +- } + /* if we are cleaning a ruv, stop */ + stop_ruv_cleaning(); + /* unregister backend state change notification */ +@@ -849,6 +847,16 @@ multimaster_stop(Slapi_PBlock *pb __attribute__((unused))) + return rc; + } + ++static int ++multisupplier_pre_stop(Slapi_PBlock *pb __attribute__((unused))) ++{ ++ if (!multisupplier_stopped_flag) { ++ /* Shut down replication agreements which will stop all the ++ * replication protocol threads */ ++ agmtlist_shutdown(); ++ } ++ return 0; ++} + + PRBool + multimaster_started() +@@ -896,7 +904,7 @@ replication_multimaster_plugin_init(Slapi_PBlock *pb) + rc = slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *)&multimasterdesc); + rc = slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN, (void *)multimaster_start); + rc = slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN, (void *)multimaster_stop); +- ++ rc = slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_CLOSE_FN, (void *)multisupplier_pre_stop); + /* Register the plugin interfaces we implement */ + /* preop acquires csn generator handle */ + rc = slapi_register_plugin("preoperation", 1 /* Enabled */, +diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c +index 2534483c1..c2977aa6f 100644 +--- a/ldap/servers/slapd/daemon.c ++++ b/ldap/servers/slapd/daemon.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2021 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -1367,6 +1367,10 @@ slapd_daemon(daemon_ports_t *ports) + task_cancel_all(); + } + ++ /* Call plugin pre close functions */ ++ plugin_pre_closeall(); ++ ++ /* Now wait for active threads to terminate */ + threads = g_get_active_threadcnt(); + if (threads > 0) { + slapi_log_err(SLAPI_LOG_INFO, "slapd_daemon", +diff --git a/ldap/servers/slapd/pblock.c b/ldap/servers/slapd/pblock.c +index 7c1cdc81c..b97c1acef 100644 +--- a/ldap/servers/slapd/pblock.c ++++ b/ldap/servers/slapd/pblock.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * Copyright (C) 2009 Hewlett-Packard Development Company, L.P. + * All rights reserved. + * +@@ -2524,6 +2524,14 @@ slapi_pblock_get(Slapi_PBlock *pblock, int arg, void *value) + } + break; + ++ case SLAPI_PLUGIN_PRE_CLOSE_FN: ++ if (pblock->pb_plugin != NULL) { ++ (*(IFP *)value) = pblock->pb_plugin->plg_pre_close; ++ } else { ++ (*(IFP *)value) = NULL; ++ } ++ break; ++ + default: + slapi_log_err(SLAPI_LOG_ERR, "slapi_pblock_get", "Unknown parameter block argument %d\n", arg); + PR_ASSERT(0); +@@ -4218,6 +4226,10 @@ slapi_pblock_set(Slapi_PBlock *pblock, int arg, void *value) + pblock->pb_misc->pb_aci_target_check = *((int *)value); + break; + ++ case SLAPI_PLUGIN_PRE_CLOSE_FN: ++ pblock->pb_plugin->plg_pre_close = (IFP)value; ++ break; ++ + default: + slapi_log_err(SLAPI_LOG_ERR, "slapi_pblock_set", + "Unknown parameter block argument %d\n", arg); +diff --git a/ldap/servers/slapd/plugin.c b/ldap/servers/slapd/plugin.c +index 449be2142..8b62dba32 100644 +--- a/ldap/servers/slapd/plugin.c ++++ b/ldap/servers/slapd/plugin.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -1881,6 +1881,28 @@ plugin_dependency_closeall(void) + } + } + ++/* Call the pre close functions of all the plugins */ ++void ++plugin_pre_closeall(void) ++{ ++ Slapi_PBlock *pb = NULL; ++ int plugins_pre_closed = 0; ++ int index = 0; ++ ++ while (plugins_pre_closed < global_plugins_started) { ++ if (global_plugin_shutdown_order[index].name) { ++ if (!global_plugin_shutdown_order[index].removed) { ++ pb = slapi_pblock_new(); ++ plugin_call_one(global_plugin_shutdown_order[index].plugin, ++ SLAPI_PLUGIN_PRE_CLOSE_FN, pb); ++ slapi_pblock_destroy(pb); ++ } ++ plugins_pre_closed++; ++ } ++ index++; ++ } ++} ++ + void + plugin_freeall(void) + { +diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h +index 6e473a08e..ab42c0e0f 100644 +--- a/ldap/servers/slapd/proto-slap.h ++++ b/ldap/servers/slapd/proto-slap.h +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -959,6 +959,7 @@ int plugin_call_exop_plugins(Slapi_PBlock *pb, struct slapdplugin *p); + Slapi_Backend *plugin_extended_op_getbackend(Slapi_PBlock *pb, struct slapdplugin *p); + const char *plugin_extended_op_oid2string(const char *oid); + void plugin_closeall(int close_backends, int close_globals); ++void plugin_pre_closeall(void); + void plugin_dependency_freeall(void); + void plugin_startall(int argc, char **argv, char **plugin_list); + void plugin_get_plugin_dependencies(char *plugin_name, char ***names); +diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h +index e6b49d366..0f67dda7d 100644 +--- a/ldap/servers/slapd/slap.h ++++ b/ldap/servers/slapd/slap.h +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2009 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * Copyright (C) 2009 Hewlett-Packard Development Company, L.P. + * All rights reserved. + * +@@ -1009,6 +1009,7 @@ struct slapdplugin + char *plg_libpath; /* library path for dll/so */ + char *plg_initfunc; /* init symbol */ + IFP plg_close; /* close function */ ++ IFP plg_pre_close; /* pre close function */ + Slapi_PluginDesc plg_desc; /* vendor's info */ + char *plg_name; /* used for plugin rdn in cn=config */ + struct slapdplugin *plg_next; /* for plugin lists */ +diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h +index 6517665a9..5dd78004c 100644 +--- a/ldap/servers/slapd/slapi-plugin.h ++++ b/ldap/servers/slapd/slapi-plugin.h +@@ -1,6 +1,6 @@ + /* BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2009 Red Hat, Inc. ++ * Copyright (C) 2026 Red Hat, Inc. + * Copyright (C) 2009 Hewlett-Packard Development Company, L.P. + * All rights reserved. + * +@@ -6789,7 +6789,7 @@ time_t slapi_current_time(void) __attribute__((deprecated)); + * and should NOT be used for timer information. + */ + int32_t slapi_clock_gettime(struct timespec *tp); +-/* ++/* + * slapi_clock_gettime should have better been called + * slapi_clock_utc_gettime but sice the function pre-existed + * we are just adding an alias (to avoid risking to break +@@ -7093,6 +7093,7 @@ typedef struct slapi_plugindesc + + /* miscellaneous plugin functions */ + #define SLAPI_PLUGIN_CLOSE_FN 210 ++#define SLAPI_PLUGIN_PRE_CLOSE_FN 211 + #define SLAPI_PLUGIN_START_FN 212 + #define SLAPI_PLUGIN_CLEANUP_FN 232 + #define SLAPI_PLUGIN_POSTSTART_FN 233 +-- +2.52.0 + diff --git a/SOURCES/0096-Issue-7271-Add-new-plugin-pre-close-function-check-t.patch b/SOURCES/0096-Issue-7271-Add-new-plugin-pre-close-function-check-t.patch new file mode 100644 index 0000000..d927a6d --- /dev/null +++ b/SOURCES/0096-Issue-7271-Add-new-plugin-pre-close-function-check-t.patch @@ -0,0 +1,34 @@ +From fbe7a335332c6afc676174a47bf109e2c8cf1647 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Wed, 4 Mar 2026 14:47:01 -0500 +Subject: [PATCH 09/13] Issue 7271 - Add new plugin pre-close function check to + plugin_invoke_plugin_pb + +Description: + +In plugin_invoke_plugin_pb we were not checking for the new pre-close function +which led to an error in the logs: pb_op is NULL. In a debug build this leads +to an assertion error at shutdown. + +relates: https://github.com/389ds/389-ds-base/issues/7271 + +Reviewed by: vashirov(Thanks!) +--- + ldap/servers/slapd/plugin.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/ldap/servers/slapd/plugin.c b/ldap/servers/slapd/plugin.c +index 8b62dba32..ab7875e87 100644 +--- a/ldap/servers/slapd/plugin.c ++++ b/ldap/servers/slapd/plugin.c +@@ -3674,6 +3674,7 @@ plugin_invoke_plugin_pb(struct slapdplugin *plugin, int operation, Slapi_PBlock + if (operation == SLAPI_PLUGIN_START_FN || + operation == SLAPI_PLUGIN_POSTSTART_FN || + operation == SLAPI_PLUGIN_CLOSE_FN || ++ operation == SLAPI_PLUGIN_PRE_CLOSE_FN || + operation == SLAPI_PLUGIN_CLEANUP_FN || + operation == SLAPI_PLUGIN_BE_PRE_CLOSE_FN || + operation == SLAPI_PLUGIN_BE_POST_OPEN_FN || +-- +2.52.0 + diff --git a/SOURCES/0097-Issue-7304-retrocl-should-not-cache-DN.patch b/SOURCES/0097-Issue-7304-retrocl-should-not-cache-DN.patch new file mode 100644 index 0000000..750918b --- /dev/null +++ b/SOURCES/0097-Issue-7304-retrocl-should-not-cache-DN.patch @@ -0,0 +1,38 @@ +From b18b6ced81be921ba9901e96566d412aa8f2cec6 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Wed, 4 Mar 2026 14:28:59 -0500 +Subject: [PATCH 10/13] Issue 7304 - retrocl should not cache DN + +Description: + +When adding a record to the retro changelog we pass in the flag +SLAPI_OP_FLAG_NEVER_CACHE to prevent the entry from being cached, +but we still update the DN cache leading to unexpected memory growth. + +relates: https://github.com/389ds/389-ds-base/issues/7304 + +Reviewed by: tbordaz(Thanks!) +--- + ldap/servers/slapd/back-ldbm/ldbm_add.c | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/ldap/servers/slapd/back-ldbm/ldbm_add.c b/ldap/servers/slapd/back-ldbm/ldbm_add.c +index 226b846cb..89a3f18f6 100644 +--- a/ldap/servers/slapd/back-ldbm/ldbm_add.c ++++ b/ldap/servers/slapd/back-ldbm/ldbm_add.c +@@ -1394,8 +1394,11 @@ common_return: + struct backdn *bdn = dncache_find_id(&inst->inst_dncache, + addingentry->ep_id); + if (bdn) { /* already in the dncache */ ++ if (is_remove_from_cache) { ++ CACHE_REMOVE(&inst->inst_dncache, bdn); ++ } + CACHE_RETURN(&inst->inst_dncache, &bdn); +- } else { /* not in the dncache yet */ ++ } else if (!is_remove_from_cache) { /* not in the dncache yet */ + Slapi_DN *addingsdn = + slapi_sdn_dup(slapi_entry_get_sdn(addingentry->ep_entry)); + if (addingsdn) { +-- +2.52.0 + diff --git a/SOURCES/0098-Issue-7291-Crash-when-configuring-a-replica-with-an-.patch b/SOURCES/0098-Issue-7291-Crash-when-configuring-a-replica-with-an-.patch new file mode 100644 index 0000000..9f2a961 --- /dev/null +++ b/SOURCES/0098-Issue-7291-Crash-when-configuring-a-replica-with-an-.patch @@ -0,0 +1,136 @@ +From 317360e7704aacfba1b05d3f68ab99be0fa427b4 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Tue, 3 Mar 2026 16:04:16 +0000 +Subject: [PATCH 11/13] Issue 7291 - Crash when configuring a replica with an + incorrect nsds5ReplicaRoot (#7292) + +Description: +Configuring a replica when the replica_root does not exist, results in +a NULL mapping tree node extension, which is deferenced without checking +for NULL. + +Fix: +Add a NULL check, log an error message, return LDAP_UNWILLING_TO_PERFORM. Added a CI test. + +Fixes: https://github.com/389ds/389-ds-base/issues/7291 + +Reviewed by: @progier389, @mreynolds389, @droideck (Thank you) +--- + .../suites/replication/replica_config_test.py | 41 +++++++++++++++++++ + .../replication/repl5_replica_config.c | 20 ++++++--- + 2 files changed, 55 insertions(+), 6 deletions(-) + +diff --git a/dirsrvtests/tests/suites/replication/replica_config_test.py b/dirsrvtests/tests/suites/replication/replica_config_test.py +index 570c3877b..1f1cde565 100644 +--- a/dirsrvtests/tests/suites/replication/replica_config_test.py ++++ b/dirsrvtests/tests/suites/replication/replica_config_test.py +@@ -17,6 +17,7 @@ from lib389.topologies import topology_st as topo + from lib389.replica import Replicas + from lib389.agreement import Agreements + from lib389.utils import ds_is_older ++from lib389 import Entry + + pytestmark = pytest.mark.tier1 + +@@ -39,6 +40,7 @@ replica_dict = {'nsDS5ReplicaRoot': 'dc=example,dc=com', + 'nsDS5ReplicaBindDN': 'cn=u', + 'cn': 'replica'} + ++ + agmt_dict = {'cn': 'test_agreement', + 'nsDS5ReplicaRoot': 'dc=example,dc=com', + 'nsDS5ReplicaHost': 'localhost.localdomain', +@@ -193,6 +195,45 @@ def test_replica_num_modify(topo, attr, too_small, too_big, overflow, notnum, va + # Value is valid + replica.replace(attr, valid) + ++def test_replica_config_add_nonexistent_replicaroot(topo): ++ """Test that adding a replica config entry with a non existent ++ nsds5replicaroot is correctly rejected. ++ ++ :id: 8eec4244-8ebb-40e1-b6f7-364b85a1862b ++ :parametrized: no ++ :setup: standalone instance ++ :steps: ++ 1. Attempt to add a replica config entry with an invalid nsds5replicaroot ++ 2. Verify that no replica entry is created ++ 3. Verify the server is still running ++ :expectedresults: ++ 1. The operation is rejected with UNWILLING_TO_PERFORM ++ 2. Success ++ 3. Success ++ """ ++ inst = topo.standalone ++ dn = "cn=replica,cn=dc\\3Dexample\\2Cdc\\3Dcom,cn=mapping tree,cn=config" ++ ++ entry = Entry((dn, { ++ 'objectclass': [ ++ 'top', ++ 'nsds5replica', ++ 'extensibleObject' ++ ], ++ 'cn': 'replica', ++ 'nsds5replicaroot': 'ou=idontexist', ++ 'nsds5replicaid': '65535', ++ 'nsds5replicatype': '2', ++ 'nsds5ReplicaBindDN': 'cn=replication manager,cn=config', ++ 'nsds5flags': '0', ++ })) ++ with pytest.raises(ldap.UNWILLING_TO_PERFORM ): ++ inst.add_s(entry) ++ ++ with pytest.raises(ldap.NO_SUCH_OBJECT): ++ inst.getEntry(dn, ldap.SCOPE_BASE, '(objectClass=*)') ++ ++ assert inst.status() + + @pytest.mark.xfail(reason="Agreement validation current does not work.") + @pytest.mark.parametrize("attr, too_small, too_big, overflow, notnum, valid", agmt_attrs) +diff --git a/ldap/servers/plugins/replication/repl5_replica_config.c b/ldap/servers/plugins/replication/repl5_replica_config.c +index 8cc7423bf..c6d730892 100644 +--- a/ldap/servers/plugins/replication/repl5_replica_config.c ++++ b/ldap/servers/plugins/replication/repl5_replica_config.c +@@ -279,10 +279,17 @@ replica_config_add(Slapi_PBlock *pb __attribute__((unused)), + replica_add_by_dn(replica_root); + + mtnode_ext = _replica_config_get_mtnode_ext(e); +- PR_ASSERT(mtnode_ext); +- ++ if (mtnode_ext == NULL) { ++ if (errortext != NULL) { ++ PR_snprintf(errortext, SLAPI_DSE_RETURNTEXT_SIZE, "replica root '%s' does not exist", replica_root); ++ } ++ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_config_add - replica root '%s' does not exist", ++ replica_root); ++ *returncode = LDAP_UNWILLING_TO_PERFORM; ++ goto done; ++ } + if (mtnode_ext->replica) { +- if ( errortext != NULL ) { ++ if (errortext != NULL) { + PR_snprintf(errortext, SLAPI_DSE_RETURNTEXT_SIZE, MSG_ALREADYCONFIGURED, replica_root); + } + slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_config_add - "MSG_ALREADYCONFIGURED, replica_root); +@@ -306,15 +313,16 @@ replica_config_add(Slapi_PBlock *pb __attribute__((unused)), + + /* add replica object to the hash */ + *returncode = replica_add_by_name(replica_get_name(r), r); /* Increments object refcnt */ +- /* delete the dn from the dn hash - done with configuration */ +- replica_delete_by_dn(replica_root); + + done: + ++ /* delete the dn from the dn hash - done with configuration */ ++ replica_delete_by_dn(replica_root); ++ + PR_Unlock(s_configLock); + + if (*returncode != LDAP_SUCCESS) { +- if (mtnode_ext->replica) ++ if (mtnode_ext && mtnode_ext->replica) + object_release(mtnode_ext->replica); + return SLAPI_DSE_CALLBACK_ERROR; + } else +-- +2.52.0 + diff --git a/SOURCES/0099-Issue-7300-RFE-Add-OS-level-thread-names-to-all-serv.patch b/SOURCES/0099-Issue-7300-RFE-Add-OS-level-thread-names-to-all-serv.patch new file mode 100644 index 0000000..cb7c37d --- /dev/null +++ b/SOURCES/0099-Issue-7300-RFE-Add-OS-level-thread-names-to-all-serv.patch @@ -0,0 +1,822 @@ +From 6ea0a4542e09191fbce053b9aca9dc0db31a744c Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Thu, 5 Mar 2026 17:09:13 -0800 +Subject: [PATCH 12/13] Issue 7300 - RFE - Add OS-level thread names to all + server threads (#7301) + +Description: Add slapi_set_thread_name() API wrapping pthread_setname_np +and apply it to all server threads. +Workers appear as worker-0..N, the listener as "listener", +replication threads as "repl-prot", "repl-inc-res", etc. + +For functions that serve as both thread entry points and direct calls +(db2ldif_skip_all, dbmdb_recno_cache_build), thin wrapper functions +are used to keep thread naming separate from the core logic. + +Fixes: https://github.com/389ds/389-ds-base/issues/7300 + +Reviewed by: @vashirov, @jchapma, @mreynolds389 (Thanks!!!) +--- + dirsrvtests/tests/suites/threads/__init__.py | 0 + .../suites/threads/thread_naming_test.py | 76 +++++++++++++++++++ + ldap/servers/plugins/automember/automember.c | 4 + + ldap/servers/plugins/cos/cos_cache.c | 1 + + ldap/servers/plugins/linkedattrs/fixup_task.c | 1 + + ldap/servers/plugins/memberof/memberof.c | 2 + + .../plugins/posix-winsync/posix-group-task.c | 5 ++ + ldap/servers/plugins/referint/referint.c | 1 + + ldap/servers/plugins/replication/cl5_api.c | 1 + + ldap/servers/plugins/replication/cl5_test.c | 1 + + .../plugins/replication/repl5_inc_protocol.c | 1 + + .../plugins/replication/repl5_protocol.c | 1 + + .../plugins/replication/repl5_replica.c | 1 + + .../plugins/replication/repl5_tot_protocol.c | 1 + + ldap/servers/plugins/retrocl/retrocl_trim.c | 1 + + ldap/servers/plugins/roles/roles_cache.c | 1 + + .../plugins/schema_reload/schema_reload.c | 1 + + ldap/servers/plugins/sync/sync_persist.c | 11 +-- + ldap/servers/plugins/syntaxes/validate_task.c | 1 + + ldap/servers/plugins/usn/usn_cleanup.c | 1 + + ldap/servers/plugins/views/views.c | 1 + + .../slapd/back-ldbm/db-bdb/bdb_import.c | 1 + + .../back-ldbm/db-bdb/bdb_import_threads.c | 3 + + .../slapd/back-ldbm/db-bdb/bdb_layer.c | 7 ++ + ldap/servers/slapd/connection.c | 3 + + ldap/servers/slapd/csngen.c | 13 ++-- + ldap/servers/slapd/daemon.c | 2 + + ldap/servers/slapd/eventq-deprecated.c | 1 + + ldap/servers/slapd/eventq.c | 1 + + ldap/servers/slapd/house.c | 1 + + ldap/servers/slapd/psearch.c | 1 + + ldap/servers/slapd/slapi-plugin.h | 3 + + ldap/servers/slapd/task.c | 6 ++ + ldap/servers/slapd/test-plugins/sampletask.c | 5 ++ + ldap/servers/slapd/thread_data.c | 8 ++ + ldap/servers/slapd/vattr.c | 1 + + 36 files changed, 159 insertions(+), 10 deletions(-) + create mode 100644 dirsrvtests/tests/suites/threads/__init__.py + create mode 100644 dirsrvtests/tests/suites/threads/thread_naming_test.py + +diff --git a/dirsrvtests/tests/suites/threads/__init__.py b/dirsrvtests/tests/suites/threads/__init__.py +new file mode 100644 +index 000000000..e69de29bb +diff --git a/dirsrvtests/tests/suites/threads/thread_naming_test.py b/dirsrvtests/tests/suites/threads/thread_naming_test.py +new file mode 100644 +index 000000000..0f2380ac4 +--- /dev/null ++++ b/dirsrvtests/tests/suites/threads/thread_naming_test.py +@@ -0,0 +1,76 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2026 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++import logging ++import os ++import platform ++import pytest ++ ++from lib389 import pid_from_file ++from lib389.topologies import topology_st ++ ++pytestmark = pytest.mark.tier1 ++ ++logging.getLogger(__name__).setLevel(logging.INFO) ++log = logging.getLogger(__name__) ++ ++ ++def _get_thread_names(pid): ++ """Read thread names from /proc//task/*/comm""" ++ names = [] ++ task_dir = f"/proc/{pid}/task" ++ if not os.path.isdir(task_dir): ++ pytest.skip(f"{task_dir} not available") ++ for tid in os.listdir(task_dir): ++ comm_path = os.path.join(task_dir, tid, "comm") ++ try: ++ with open(comm_path, "r") as f: ++ names.append(f.read().strip()) ++ except (FileNotFoundError, PermissionError): ++ pass ++ return names ++ ++ ++def test_thread_names_present(topology_st): ++ """Test that key threads have been named via pthread_setname_np ++ ++ :id: a95b4eca-e806-48dc-8c91-debbc3ef4053 ++ :setup: Standalone instance ++ :steps: ++ 1. Get the server PID ++ 2. Read thread names from /proc/{pid}/task/{tid}/comm ++ 3. Verify expected thread names are present ++ :expectedresults: ++ 1. PID is obtained ++ 2. Thread names are readable ++ 3. Core thread names (listener, worker, event-q, ++ housekeep) are found ++ """ ++ inst = topology_st.standalone ++ pid = str(pid_from_file(inst.ds_paths.pid_file)) ++ log.info(f"Server PID: {pid}") ++ ++ thread_names = _get_thread_names(pid) ++ log.info(f"Found {len(thread_names)} threads: {sorted(set(thread_names))}") ++ ++ # Verify the listener thread ++ assert "listener" in thread_names, \ ++ f"'listener' thread not found in {thread_names}" ++ ++ # Verify at least one worker thread ++ worker_threads = [n for n in thread_names if n.startswith("worker-")] ++ assert len(worker_threads) > 0, \ ++ f"No 'worker-*' threads found in {thread_names}" ++ ++ # Verify event queue thread ++ assert "event-q" in thread_names, \ ++ f"'event-q' thread not found in {thread_names}" ++ ++ # Verify housekeeping thread ++ assert "housekeep" in thread_names, \ ++ f"'housekeep' thread not found in {thread_names}" +diff --git a/ldap/servers/plugins/automember/automember.c b/ldap/servers/plugins/automember/automember.c +index 1b1da39b3..65404a574 100644 +--- a/ldap/servers/plugins/automember/automember.c ++++ b/ldap/servers/plugins/automember/automember.c +@@ -2300,6 +2300,7 @@ automember_task_abort(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter __at + void + automember_task_abort_thread(void *arg) + { ++ slapi_set_thread_name("automem-abort"); + Slapi_Task *task = (Slapi_Task *)arg; + + slapi_task_inc_refcount(task); +@@ -2456,6 +2457,7 @@ out: + void + automember_rebuild_task_thread(void *arg) + { ++ slapi_set_thread_name("automem-rebld"); + Slapi_Task *task = (Slapi_Task *)arg; + struct configEntry *config = NULL; + Slapi_PBlock *search_pb = NULL; +@@ -2759,6 +2761,7 @@ out: + void + automember_export_task_thread(void *arg) + { ++ slapi_set_thread_name("automem-exprt"); + Slapi_Task *task = (Slapi_Task *)arg; + Slapi_PBlock *search_pb = NULL; + Slapi_Entry **entries = NULL; +@@ -2949,6 +2952,7 @@ out: + void + automember_map_task_thread(void *arg) + { ++ slapi_set_thread_name("automem-map"); + Slapi_Task *task = (Slapi_Task *)arg; + Slapi_Entry *e = NULL; + int result = SLAPI_DSE_CALLBACK_OK; +diff --git a/ldap/servers/plugins/cos/cos_cache.c b/ldap/servers/plugins/cos/cos_cache.c +index d404ff901..d8ebbbe3d 100644 +--- a/ldap/servers/plugins/cos/cos_cache.c ++++ b/ldap/servers/plugins/cos/cos_cache.c +@@ -373,6 +373,7 @@ out: + static void + cos_cache_wait_on_change(void *arg __attribute__((unused))) + { ++ slapi_set_thread_name("cos-cache"); + slapi_log_err(SLAPI_LOG_TRACE, COS_PLUGIN_SUBSYSTEM, "--> cos_cache_wait_on_change thread\n"); + + slapi_lock_mutex(stop_lock); +diff --git a/ldap/servers/plugins/linkedattrs/fixup_task.c b/ldap/servers/plugins/linkedattrs/fixup_task.c +index f47ccf6d1..e69c62d86 100644 +--- a/ldap/servers/plugins/linkedattrs/fixup_task.c ++++ b/ldap/servers/plugins/linkedattrs/fixup_task.c +@@ -107,6 +107,7 @@ linked_attrs_fixup_task_destructor(Slapi_Task *task) + static void + linked_attrs_fixup_task_thread(void *arg) + { ++ slapi_set_thread_name("linked-fix"); + Slapi_Task *task = (Slapi_Task *)arg; + task_data *td = NULL; + PRCList *main_config = NULL; +diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c +index b52bc0331..153b08ca0 100644 +--- a/ldap/servers/plugins/memberof/memberof.c ++++ b/ldap/servers/plugins/memberof/memberof.c +@@ -994,6 +994,7 @@ is_memberof_plugin_started(struct slapdplugin **plg_addr) + void + deferred_thread_func(void *arg) + { ++ slapi_set_thread_name("memberof-def"); + MemberofDeferredList *deferred_list = (MemberofDeferredList *) arg; + MemberofDeferredTask *task; + struct slapdplugin *plg = NULL; +@@ -3862,6 +3863,7 @@ memberof_qsort_compare(const void *a, const void *b) + void + memberof_fixup_task_thread(void *arg) + { ++ slapi_set_thread_name("memberof-fix"); + MemberOfConfig configCopy = {0}; + Slapi_Task *task = (Slapi_Task *)arg; + task_data *td = NULL; +diff --git a/ldap/servers/plugins/posix-winsync/posix-group-task.c b/ldap/servers/plugins/posix-winsync/posix-group-task.c +index 922c4f743..96f205822 100644 +--- a/ldap/servers/plugins/posix-winsync/posix-group-task.c ++++ b/ldap/servers/plugins/posix-winsync/posix-group-task.c +@@ -1,3 +1,7 @@ ++#ifdef HAVE_CONFIG_H ++#include ++#endif ++ + #include + #include "posix-wsp-ident.h" + #include "posix-group-func.h" +@@ -463,6 +467,7 @@ bail: + static void + posix_group_fixup_task_thread(void *arg) + { ++ slapi_set_thread_name("posix-grp-fix"); + slapi_log_err(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME, + "_task_thread ==>\n"); + +diff --git a/ldap/servers/plugins/referint/referint.c b/ldap/servers/plugins/referint/referint.c +index c5e259d8d..2dd3f1ed5 100644 +--- a/ldap/servers/plugins/referint/referint.c ++++ b/ldap/servers/plugins/referint/referint.c +@@ -1393,6 +1393,7 @@ referint_postop_close(Slapi_PBlock *pb __attribute__((unused))) + void + referint_thread_func(void *arg __attribute__((unused))) + { ++ slapi_set_thread_name("referint"); + PRFileDesc *prfd = NULL; + char *logfilename = NULL; + char thisline[MAX_LINE]; +diff --git a/ldap/servers/plugins/replication/cl5_api.c b/ldap/servers/plugins/replication/cl5_api.c +index 21d2f5b8b..87d517dcf 100644 +--- a/ldap/servers/plugins/replication/cl5_api.c ++++ b/ldap/servers/plugins/replication/cl5_api.c +@@ -3156,6 +3156,7 @@ do_cl_compact(time_t when, void *arg) + static int + _cl5TrimMain(void *param __attribute__((unused))) + { ++ slapi_set_thread_name("cl-trim"); + time_t timePrev = slapi_current_utc_time(); + time_t timeCompactPrev = slapi_current_utc_time(); + time_t timeNow; +diff --git a/ldap/servers/plugins/replication/cl5_test.c b/ldap/servers/plugins/replication/cl5_test.c +index efb8c543a..c9078eb2e 100644 +--- a/ldap/servers/plugins/replication/cl5_test.c ++++ b/ldap/servers/plugins/replication/cl5_test.c +@@ -551,6 +551,7 @@ clearCSNList(CSN ***csnList, int count) + static void + threadMain(void *data) + { ++ slapi_set_thread_name("cl5-test"); + int entryCount = *(int *)data; + CSN **csnList; + +diff --git a/ldap/servers/plugins/replication/repl5_inc_protocol.c b/ldap/servers/plugins/replication/repl5_inc_protocol.c +index 846951b9e..23e5b474c 100644 +--- a/ldap/servers/plugins/replication/repl5_inc_protocol.c ++++ b/ldap/servers/plugins/replication/repl5_inc_protocol.c +@@ -237,6 +237,7 @@ repl5_inc_log_operation_failure(int operation_code, int ldap_error, char *ldap_e + static void + repl5_inc_result_threadmain(void *param) + { ++ slapi_set_thread_name("repl-inc-res"); + result_data *rd = (result_data *)param; + ConnResult conres = 0; + Repl_Connection *conn = rd->prp->conn; +diff --git a/ldap/servers/plugins/replication/repl5_protocol.c b/ldap/servers/plugins/replication/repl5_protocol.c +index 6cdbcc4f1..f230fb702 100644 +--- a/ldap/servers/plugins/replication/repl5_protocol.c ++++ b/ldap/servers/plugins/replication/repl5_protocol.c +@@ -226,6 +226,7 @@ finished (any) finished + static void + prot_thread_main(void *arg) + { ++ slapi_set_thread_name("repl-prot"); + Repl_Protocol *rp = (Repl_Protocol *)arg; + int done; + Repl_Agmt *agmt = NULL; +diff --git a/ldap/servers/plugins/replication/repl5_replica.c b/ldap/servers/plugins/replication/repl5_replica.c +index cec140140..edfbfce3a 100644 +--- a/ldap/servers/plugins/replication/repl5_replica.c ++++ b/ldap/servers/plugins/replication/repl5_replica.c +@@ -3158,6 +3158,7 @@ process_reap_entry(Slapi_Entry *entry, void *cb_data) + static void + _replica_reap_tombstones(void *arg) + { ++ slapi_set_thread_name("tomb-reap"); + const char *replica_name = (const char *)arg; + Slapi_PBlock *pb = NULL; + Replica *replica = NULL; +diff --git a/ldap/servers/plugins/replication/repl5_tot_protocol.c b/ldap/servers/plugins/replication/repl5_tot_protocol.c +index 4b2064912..1ec930116 100644 +--- a/ldap/servers/plugins/replication/repl5_tot_protocol.c ++++ b/ldap/servers/plugins/replication/repl5_tot_protocol.c +@@ -102,6 +102,7 @@ repl5_tot_log_operation_failure(int ldap_error, char *ldap_error_string, const c + static void + repl5_tot_result_threadmain(void *param) + { ++ slapi_set_thread_name("repl-tot-res"); + callback_data *cb = (callback_data *)param; + ConnResult conres = 0; + Repl_Connection *conn = cb->prp->conn; +diff --git a/ldap/servers/plugins/retrocl/retrocl_trim.c b/ldap/servers/plugins/retrocl/retrocl_trim.c +index 6514fc790..a363c8a30 100644 +--- a/ldap/servers/plugins/retrocl/retrocl_trim.c ++++ b/ldap/servers/plugins/retrocl/retrocl_trim.c +@@ -334,6 +334,7 @@ static int retrocl_active_threads; + static void + changelog_trim_thread_fn(void *arg __attribute__((unused))) + { ++ slapi_set_thread_name("retrocl-trim"); + PR_AtomicIncrement(&retrocl_active_threads); + trim_changelog(); + PR_AtomicDecrement(&retrocl_active_threads); +diff --git a/ldap/servers/plugins/roles/roles_cache.c b/ldap/servers/plugins/roles/roles_cache.c +index 329b8c888..5030a2d1b 100644 +--- a/ldap/servers/plugins/roles/roles_cache.c ++++ b/ldap/servers/plugins/roles/roles_cache.c +@@ -359,6 +359,7 @@ roles_cache_create_suffix(Slapi_DN *sdn) + static void + roles_cache_wait_on_change(void *arg) + { ++ slapi_set_thread_name("roles-cache"); + roles_cache_def *roles_def = (roles_cache_def *)arg; + + slapi_log_err(SLAPI_LOG_PLUGIN, ROLES_PLUGIN_SUBSYSTEM, "--> roles_cache_wait_on_change\n"); +diff --git a/ldap/servers/plugins/schema_reload/schema_reload.c b/ldap/servers/plugins/schema_reload/schema_reload.c +index 847323af4..8b0b64bca 100644 +--- a/ldap/servers/plugins/schema_reload/schema_reload.c ++++ b/ldap/servers/plugins/schema_reload/schema_reload.c +@@ -125,6 +125,7 @@ typedef struct _task_data + static void + schemareload_thread(void *arg) + { ++ slapi_set_thread_name("schema-reload"); + Slapi_Task *task = (Slapi_Task *)arg; + int rv = 0; + int total_work = 2; +diff --git a/ldap/servers/plugins/sync/sync_persist.c b/ldap/servers/plugins/sync/sync_persist.c +index a78c273cd..fdb41fd23 100644 +--- a/ldap/servers/plugins/sync/sync_persist.c ++++ b/ldap/servers/plugins/sync/sync_persist.c +@@ -260,7 +260,7 @@ ignore_op_pl(Slapi_PBlock *pb) + * of the completed operation. + * When all operations are completed, if the primary operation is successful it + * flushes (enqueue) the operations to the sync repl queue(s), else it just free +- * the pending list (skipping enqueue). ++ * the pending list (skipping enqueue). + */ + static void + sync_update_persist_op(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eprev, ber_int_t op_tag, char *label) +@@ -286,7 +286,7 @@ sync_update_persist_op(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eprev, ber + ignore_op_pl(pb); + return; + } +- ++ + /* Retrieve the result of the operation */ + if (slapi_op_internal(pb)) { + slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &rc); +@@ -351,11 +351,11 @@ sync_update_persist_op(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eprev, ber + } + } + if (!curr_op) { +- slapi_log_err(SLAPI_LOG_ERR, SYNC_PLUGIN_SUBSYSTEM, "%s - operation (op=0x%lx, idx_pl=%d) not found on the pendling list\n", ++ slapi_log_err(SLAPI_LOG_ERR, SYNC_PLUGIN_SUBSYSTEM, "%s - operation (op=0x%lx, idx_pl=%d) not found on the pendling list\n", + label, (ulong) pb_op, ident->idx_pl); + PR_ASSERT(curr_op); + } +- ++ + /* for diagnostic of the pending list, dump its content if it is too long */ + for (count = 0, curr_op = prim_op; curr_op; count++, curr_op = curr_op->next); + if (loglevel_is_set(SLAPI_LOG_PLUGIN) && (count > 10)) { +@@ -432,7 +432,7 @@ sync_update_persist_op(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eprev, ber + if (enqueue_it) { + sync_queue_change(curr_op); + } +- ++ + /* now free this pending operation */ + next = curr_op->next; + slapi_entry_free(curr_op->entry); +@@ -975,6 +975,7 @@ sync_release_connection(Slapi_PBlock *pb, Slapi_Connection *conn, Slapi_Operatio + static void + sync_send_results(void *arg) + { ++ slapi_set_thread_name("sync-send"); + SyncRequest *req = (SyncRequest *)arg; + SyncQueueNode *qnode, *qnodenext; + int conn_acq_flag = 0; +diff --git a/ldap/servers/plugins/syntaxes/validate_task.c b/ldap/servers/plugins/syntaxes/validate_task.c +index a634fd762..49829ab08 100644 +--- a/ldap/servers/plugins/syntaxes/validate_task.c ++++ b/ldap/servers/plugins/syntaxes/validate_task.c +@@ -166,6 +166,7 @@ syntax_validate_task_destructor(Slapi_Task *task) + static void + syntax_validate_task_thread(void *arg) + { ++ slapi_set_thread_name("syntax-valid"); + int rc = 0; + Slapi_Task *task = (Slapi_Task *)arg; + task_data *td = NULL; +diff --git a/ldap/servers/plugins/usn/usn_cleanup.c b/ldap/servers/plugins/usn/usn_cleanup.c +index 7eaf0f88f..47940eabe 100644 +--- a/ldap/servers/plugins/usn/usn_cleanup.c ++++ b/ldap/servers/plugins/usn/usn_cleanup.c +@@ -45,6 +45,7 @@ usn_cleanup_close(void) + static void + usn_cleanup_thread(void *arg) + { ++ slapi_set_thread_name("usn-cleanup"); + Slapi_Task *task = (Slapi_Task *)arg; + int rv = 0; + int total_work = 2; +diff --git a/ldap/servers/plugins/views/views.c b/ldap/servers/plugins/views/views.c +index 64e305a3f..ed2ba73e4 100644 +--- a/ldap/servers/plugins/views/views.c ++++ b/ldap/servers/plugins/views/views.c +@@ -1797,5 +1797,6 @@ views_cache_backend_state_change(void *handle __attribute__((unused)), char *be_ + static void + views_cache_act_on_change_thread(void *arg __attribute__((unused))) + { ++ slapi_set_thread_name("views-cache"); + views_cache_create(); + } +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +index 7f484934f..2eede5931 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +@@ -2663,6 +2663,7 @@ error: + void + import_main(void *arg) + { ++ slapi_set_thread_name("bdb-import"); + /* For online import tasks increment/decrement the global thread count */ + g_incr_active_threadcnt(); + import_main_offline(arg); +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c +index e433f3db2..350266a02 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c +@@ -340,6 +340,7 @@ import_add_created_attrs(Slapi_Entry *e) + void + import_producer(void *param) + { ++ slapi_set_thread_name("bdb-imp-prod"); + ImportWorkerInfo *info = (ImportWorkerInfo *)param; + ImportJob *job = info->job; + ID id = job->first_ID, id_filestart = id; +@@ -2469,6 +2470,7 @@ foreman_do_entryrdn(ImportJob *job, FifoItem *fi) + void + import_foreman(void *param) + { ++ slapi_set_thread_name("bdb-imp-frmn"); + ImportWorkerInfo *info = (ImportWorkerInfo *)param; + ImportJob *job = info->job; + ldbm_instance *inst = job->inst; +@@ -2812,6 +2814,7 @@ error: + void + import_worker(void *param) + { ++ slapi_set_thread_name("bdb-imp-work"); + ImportWorkerInfo *info = (ImportWorkerInfo *)param; + ImportJob *job = info->job; + ldbm_instance *inst = job->inst; +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +index a2dcc61e1..ec7524d02 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +@@ -2909,6 +2909,7 @@ bdb_start_perf_thread(struct ldbminfo *li) + static int + perf_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-perf"); + struct ldbminfo *li = NULL; + + PR_ASSERT(NULL != param); +@@ -2959,6 +2960,7 @@ bdb_start_locks_monitoring_thread(struct ldbminfo *li) + static int + locks_monitoring_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-lock-mon"); + int ret = 0; + uint64_t current_locks = 0; + uint64_t max_locks = 0; +@@ -3169,6 +3171,7 @@ txn_test_init_cfg(txn_test_cfg *cfg) + static int + txn_test_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-txn-test"); + struct ldbminfo *li = NULL; + Object *inst_obj; + int rc = 0; +@@ -3467,6 +3470,7 @@ bdb_start_txn_test_thread(struct ldbminfo *li) + static int + deadlock_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-deadlock"); + int rval = -1; + struct ldbminfo *li = NULL; + PRIntervalTime interval; /*NSPR timeout stuffy*/ +@@ -3564,6 +3568,7 @@ bdb_start_log_flush_thread(struct ldbminfo *li) + static int + log_flush_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-logflush"); + PRIntervalTime interval_flush, interval_def; + PRIntervalTime last_flush = 0; + int i; +@@ -3761,6 +3766,7 @@ bdb_start_checkpoint_thread(struct ldbminfo *li) + static int + checkpoint_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-chkpoint"); + PRIntervalTime interval; + int rval = -1; + struct ldbminfo *li = NULL; +@@ -3964,6 +3970,7 @@ bdb_start_trickle_thread(struct ldbminfo *li) + static int + trickle_threadmain(void *param) + { ++ slapi_set_thread_name("bdb-trickle"); + PRIntervalTime interval; /*NSPR timeout stuffy*/ + int rval = -1; + dblayer_private *priv = NULL; +diff --git a/ldap/servers/slapd/connection.c b/ldap/servers/slapd/connection.c +index 6c5ef5291..e757c838a 100644 +--- a/ldap/servers/slapd/connection.c ++++ b/ldap/servers/slapd/connection.c +@@ -1634,6 +1634,9 @@ connection_threadmain(void *arg) + { + Slapi_PBlock *pb = slapi_pblock_new(); + int32_t *snmp_vars_idx = (int32_t *) arg; ++ char tname[16]; ++ snprintf(tname, sizeof(tname), "worker-%d", *snmp_vars_idx); ++ slapi_set_thread_name(tname); + /* wait forever for new pb until one is available or shutdown */ + int32_t interval = 0; /* used be 10 seconds */ + Connection *conn = NULL; +diff --git a/ldap/servers/slapd/csngen.c b/ldap/servers/slapd/csngen.c +index c7c5c2ba8..8fff8e87e 100644 +--- a/ldap/servers/slapd/csngen.c ++++ b/ldap/servers/slapd/csngen.c +@@ -315,15 +315,15 @@ csngen_adjust_time(CSNGen *gen, const CSN *csn) + /* To avoid beat phenomena between suppliers let put 1 second in local_offset + * it will be eaten at next clock tick rather than increasing remote offset + * If we do not do that we will have a time skew drift of 1 second per 2 seconds +- * if suppliers are desynchronized by 0.5 second ++ * if suppliers are desynchronized by 0.5 second + */ + if (gen->state.local_offset == 0) { + gen->state.local_offset++; + gen->state.remote_offset--; + } + } +- /* Time to compute seqnum so that +- * new csn >= remote csn and new csn >= old local csn ++ /* Time to compute seqnum so that ++ * new csn >= remote csn and new csn >= old local csn + */ + new_time = CSN_CALC_TSTAMP(gen); + PR_ASSERT(new_time >= old_time); +@@ -570,7 +570,7 @@ _csngen_adjust_local_time(CSNGen *gen) + time_t cur_time; + int rc; + +- ++ + if ((rc = gen->gettime(&now)) != 0) { + /* Failed to get system time, we must abort */ + slapi_log_err(SLAPI_LOG_ERR, "csngen_new_csn", +@@ -617,7 +617,7 @@ _csngen_adjust_local_time(CSNGen *gen) + gen->state.sampled_time = cur_time; + gen->state.local_offset = MAX_VAL(0, gen->state.local_offset - time_diff); + /* new local_offset = MAX_VAL(0, old sample_time + old local_offset - cur_time) +- * ==> new local_offset >= 0 and ++ * ==> new local_offset >= 0 and + * new local_offset + cur_time >= old sample_time + old local_offset + * ==> new local_offset + cur_time + remote_offset >= + * sample_time + old local_offset + remote_offset +@@ -731,6 +731,7 @@ _csngen_stop_test_threads(void) + static void + _csngen_gen_tester_main(void *data) + { ++ slapi_set_thread_name("csn-gen-test"); + CSNGen *gen = (CSNGen *)data; + CSN *csn = NULL; + char buff[CSN_STRSIZE]; +@@ -761,6 +762,7 @@ _csngen_gen_tester_main(void *data) + static void + _csngen_remote_tester_main(void *data) + { ++ slapi_set_thread_name("csn-rmt-test"); + CSNGen *gen = (CSNGen *)data; + CSN *csn; + time_t csn_time; +@@ -798,6 +800,7 @@ _csngen_remote_tester_main(void *data) + static void + _csngen_local_tester_main(void *data) + { ++ slapi_set_thread_name("csn-lcl-test"); + CSNGen *gen = (CSNGen *)data; + + PR_ASSERT(gen); +diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c +index c2977aa6f..36e52cace 100644 +--- a/ldap/servers/slapd/daemon.c ++++ b/ldap/servers/slapd/daemon.c +@@ -426,6 +426,7 @@ disk_mon_check_diskspace(char **dirs, uint64_t threshold, uint64_t *disk_space) + void + disk_monitoring_thread(void *nothing __attribute__((unused))) + { ++ slapi_set_thread_name("disk-mon"); + char **dirs = NULL; + char *dirstr = NULL; + uint64_t previous_mark = 0; +@@ -946,6 +947,7 @@ convert_pbe_des_to_aes(void) + void + accept_thread(void *vports) + { ++ slapi_set_thread_name("listener"); + daemon_ports_t *ports = (daemon_ports_t *)vports; + Connection_Table *ct = the_connection_table; + PRIntn num_poll = 0; +diff --git a/ldap/servers/slapd/eventq-deprecated.c b/ldap/servers/slapd/eventq-deprecated.c +index 71a7bf8f5..b9811c905 100644 +--- a/ldap/servers/slapd/eventq-deprecated.c ++++ b/ldap/servers/slapd/eventq-deprecated.c +@@ -302,6 +302,7 @@ eq_call_all(void) + static void + eq_loop(void *arg __attribute__((unused))) + { ++ slapi_set_thread_name("event-q-old"); + while (eq_running) { + time_t curtime = slapi_current_utc_time(); + PRIntervalTime timeout; +diff --git a/ldap/servers/slapd/eventq.c b/ldap/servers/slapd/eventq.c +index 4c39e08cf..d3a849f66 100644 +--- a/ldap/servers/slapd/eventq.c ++++ b/ldap/servers/slapd/eventq.c +@@ -298,6 +298,7 @@ eq_call_all_rel(void) + static void + eq_loop_rel(void *arg __attribute__((unused))) + { ++ slapi_set_thread_name("event-q"); + while (eq_rel_running) { + time_t curtime = slapi_current_rel_time_t(); + int until; +diff --git a/ldap/servers/slapd/house.c b/ldap/servers/slapd/house.c +index ac1d94f26..78164d5ab 100644 +--- a/ldap/servers/slapd/house.c ++++ b/ldap/servers/slapd/house.c +@@ -30,6 +30,7 @@ static pthread_cond_t housekeeping_cvar; + static void + housecleaning(void *cur_time __attribute__((unused))) + { ++ slapi_set_thread_name("housekeep"); + while (!g_get_shutdown()) { + struct timespec current_time = {0}; + /* +diff --git a/ldap/servers/slapd/psearch.c b/ldap/servers/slapd/psearch.c +index c60e6a8ed..df1dc0162 100644 +--- a/ldap/servers/slapd/psearch.c ++++ b/ldap/servers/slapd/psearch.c +@@ -259,6 +259,7 @@ pe_ch_free(PSEQNode **pe) + static void + ps_send_results(void *arg) + { ++ slapi_set_thread_name("ps-send"); + PSearch *ps = (PSearch *)arg; + PSEQNode *peq, *peqnext; + struct slapi_filter *filter = 0; +diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h +index 5dd78004c..e1cbe5db7 100644 +--- a/ldap/servers/slapd/slapi-plugin.h ++++ b/ldap/servers/slapd/slapi-plugin.h +@@ -6109,6 +6109,9 @@ int slapi_wait_condvar(Slapi_CondVar *cvar, struct timeval *timeout) __attribute + int slapi_notify_condvar(Slapi_CondVar *cvar, int notify_all); + int slapi_wait_condvar_pt(Slapi_CondVar *cvar, Slapi_Mutex *mutex, struct timeval *timeout); + ++/* Set OS-level thread name for visibility in ps, top, perf, etc. (max 15 chars) */ ++void slapi_set_thread_name(const char *name); ++ + /** + * Creates a new read/write lock + * If prio_writer the rwlock gives priority on writers +diff --git a/ldap/servers/slapd/task.c b/ldap/servers/slapd/task.c +index 1a8be6c85..350a62814 100644 +--- a/ldap/servers/slapd/task.c ++++ b/ldap/servers/slapd/task.c +@@ -1113,6 +1113,7 @@ out: + static void + task_export_thread(void *arg) + { ++ slapi_set_thread_name("export"); + Slapi_PBlock *pb = (Slapi_PBlock *)arg; + // I think someone is mis-using this point to store multiple names ... + char **instance_names = NULL; +@@ -1484,6 +1485,7 @@ out: + static void + task_backup_thread(void *arg) + { ++ slapi_set_thread_name("backup"); + Slapi_PBlock *pb = (Slapi_PBlock *)arg; + Slapi_Task *task = NULL; + struct slapdplugin *pb_plugin; +@@ -1635,6 +1637,7 @@ out: + static void + task_restore_thread(void *arg) + { ++ slapi_set_thread_name("restore"); + Slapi_PBlock *pb = (Slapi_PBlock *)arg; + Slapi_Task *task = NULL; + struct slapdplugin *pb_plugin; +@@ -1789,6 +1792,7 @@ out: + static void + task_index_thread(void *arg) + { ++ slapi_set_thread_name("index"); + Slapi_PBlock *pb = (Slapi_PBlock *)arg; + char *instance_name = NULL; + char **db2index_attrs = NULL; +@@ -2346,6 +2350,7 @@ struct task_tombstone_data + static void + task_fixup_tombstone_thread(void *arg) + { ++ slapi_set_thread_name("tombfix"); + struct task_tombstone_data *task_data = arg; + Slapi_Entry **entries = NULL; + Slapi_Task *task = task_data->task; +@@ -2956,6 +2961,7 @@ compact_db_task_destructor(Slapi_Task *task) + static void + task_compact_thread(void *arg) + { ++ slapi_set_thread_name("compact"); + struct task_compact_data *task_data = arg; + Slapi_Task *task = task_data->task; + Slapi_Backend *be = NULL; +diff --git a/ldap/servers/slapd/test-plugins/sampletask.c b/ldap/servers/slapd/test-plugins/sampletask.c +index 1734e2904..08c8315a4 100644 +--- a/ldap/servers/slapd/test-plugins/sampletask.c ++++ b/ldap/servers/slapd/test-plugins/sampletask.c +@@ -48,6 +48,10 @@ + * [...] - Sample task finished. + */ + ++#ifdef HAVE_CONFIG_H ++#include ++#endif ++ + #include "slapi-plugin.h" + #include "nspr.h" + +@@ -88,6 +92,7 @@ task_sampletask_start(Slapi_PBlock *pb) + static void + task_sampletask_thread(void *arg) + { ++ slapi_set_thread_name("sample-task"); + Slapi_Task *task = (Slapi_Task *)arg; + char *myarg = NULL; + int i, rv = 0; +diff --git a/ldap/servers/slapd/thread_data.c b/ldap/servers/slapd/thread_data.c +index fb9951d13..1e64a0d77 100644 +--- a/ldap/servers/slapd/thread_data.c ++++ b/ldap/servers/slapd/thread_data.c +@@ -60,6 +60,14 @@ slapi_td_init(void) + * Wrapper Functions + */ + ++/* Set OS-level thread name (visible in ps, top, perf, pstack, etc.) ++ * The name is silently truncated to 15 characters by the kernel. */ ++void ++slapi_set_thread_name(const char *name) ++{ ++ pthread_setname_np(pthread_self(), name); ++} ++ + /* plugin list locking */ + int32_t + slapi_td_set_plugin_locked() +diff --git a/ldap/servers/slapd/vattr.c b/ldap/servers/slapd/vattr.c +index 6fad473c4..b722185c2 100644 +--- a/ldap/servers/slapd/vattr.c ++++ b/ldap/servers/slapd/vattr.c +@@ -140,6 +140,7 @@ vattr_cleanup() + static void + vattr_check_thread(void *arg) + { ++ slapi_set_thread_name("vattr-chk"); + Slapi_Backend *be = NULL; + char *cookie = NULL; + Slapi_DN *base_sdn = NULL; +-- +2.52.0 + diff --git a/SOURCES/0100-Issue-5947-Do-not-release-the-target-entry-in-ldbm_b.patch b/SOURCES/0100-Issue-5947-Do-not-release-the-target-entry-in-ldbm_b.patch new file mode 100644 index 0000000..6b3a5b8 --- /dev/null +++ b/SOURCES/0100-Issue-5947-Do-not-release-the-target-entry-in-ldbm_b.patch @@ -0,0 +1,43 @@ +From dfb686b4d3542c931dcfd4b8ad641a319566d7dc Mon Sep 17 00:00:00 2001 +From: progier389 +Date: Fri, 17 Nov 2023 12:33:38 +0100 +Subject: [PATCH 13/13] Issue 5947 - Do not release the target entry in + ldbm_back_search_cleanup + +Partial backport from f5bd03748 for 1.4.3. +--- + ldap/servers/slapd/back-ldbm/ldbm_search.c | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/ldap/servers/slapd/back-ldbm/ldbm_search.c b/ldap/servers/slapd/back-ldbm/ldbm_search.c +index 27301f453..768151985 100644 +--- a/ldap/servers/slapd/back-ldbm/ldbm_search.c ++++ b/ldap/servers/slapd/back-ldbm/ldbm_search.c +@@ -157,7 +157,9 @@ ldbm_back_search_cleanup(Slapi_PBlock *pb, + ldbm_instance *inst; + back_search_result_set *sr = NULL; + int free_candidates = 1; ++ Slapi_Operation *op = NULL; + ++ slapi_pblock_get(pb, SLAPI_OPERATION, &op); + slapi_pblock_get(pb, SLAPI_BACKEND, &be); + inst = (ldbm_instance *)be->be_instance_info; + /* +@@ -165,7 +167,13 @@ ldbm_back_search_cleanup(Slapi_PBlock *pb, + * clean it up for the following sessions. + */ + slapi_be_unset_flag(be, SLAPI_BE_FLAG_DONT_BYPASS_FILTERTEST); +- CACHE_RETURN(&inst->inst_cache, &e); /* NULL e is handled correctly */ ++ if (e != operation_get_target_entry(op)) { ++ /* ++ * Target entry is released later on ++ * (in cache_return_target_entry called by op_shared_search). ++ */ ++ CACHE_RETURN(&inst->inst_cache, &e); /* NULL e is handled correctly */ ++ } + if (inst->inst_ref_count) { + slapi_counter_decrement(inst->inst_ref_count); + } +-- +2.52.0 + diff --git a/SOURCES/0101-Issue-7271-fixed-build-error-in-repl5_init.patch b/SOURCES/0101-Issue-7271-fixed-build-error-in-repl5_init.patch new file mode 100644 index 0000000..0b396a8 --- /dev/null +++ b/SOURCES/0101-Issue-7271-fixed-build-error-in-repl5_init.patch @@ -0,0 +1,39 @@ +From b0b0bb3bc096cca0715fa46a7548c4f5a8bf2717 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 18 May 2026 14:16:06 +0200 +Subject: [PATCH] Issue 7271 - Fix build failure on 1.4.3 + +Co-authored-by: James Chapman +--- + ldap/servers/plugins/dna/dna.c | 1 + + ldap/servers/plugins/replication/repl5_init.c | 2 +- + 2 files changed, 2 insertions(+), 1 deletion(-) + +diff --git a/ldap/servers/plugins/dna/dna.c b/ldap/servers/plugins/dna/dna.c +index ee4132646..e23bfbbae 100644 +--- a/ldap/servers/plugins/dna/dna.c ++++ b/ldap/servers/plugins/dna/dna.c +@@ -9,6 +9,7 @@ + #ifdef HAVE_CONFIG_H + #include + #endif ++#include + + + /** +diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c +index dd4870746..526c759a5 100644 +--- a/ldap/servers/plugins/replication/repl5_init.c ++++ b/ldap/servers/plugins/replication/repl5_init.c +@@ -850,7 +850,7 @@ multimaster_stop(Slapi_PBlock *pb __attribute__((unused))) + static int + multisupplier_pre_stop(Slapi_PBlock *pb __attribute__((unused))) + { +- if (!multisupplier_stopped_flag) { ++ if (!multimaster_stopped_flag) { + /* Shut down replication agreements which will stop all the + * replication protocol threads */ + agmtlist_shutdown(); +-- +2.52.0 + diff --git a/SOURCES/0102-Issue-6929-Compilation-failure-with-rust-1.89.patch b/SOURCES/0102-Issue-6929-Compilation-failure-with-rust-1.89.patch new file mode 100644 index 0000000..d89bdf9 --- /dev/null +++ b/SOURCES/0102-Issue-6929-Compilation-failure-with-rust-1.89.patch @@ -0,0 +1,35 @@ +From 98f67447cf068377efe0a91d8e3c5ef44883fdbc Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 11 Aug 2025 13:19:13 +0200 +Subject: [PATCH] Issue 6929 - Compilation failure with rust-1.89 on Fedora ELN + +Bug Description: +The `ValueArrayRefIter` struct has a lifetime parameter `'a`. +But in the `iter` method the return type doesn't specify the lifetime parameter. + +Fix Description: +Make the lifetime explicit. + +Fixes: https://github.com/389ds/389-ds-base/issues/6929 + +Reviewed by: @droideck (Thanks!) +--- + src/slapi_r_plugin/src/value.rs | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/slapi_r_plugin/src/value.rs b/src/slapi_r_plugin/src/value.rs +index 2fd35c808..fec74ac25 100644 +--- a/src/slapi_r_plugin/src/value.rs ++++ b/src/slapi_r_plugin/src/value.rs +@@ -61,7 +61,7 @@ impl ValueArrayRef { + ValueArrayRef { raw_slapi_val } + } + +- pub fn iter(&self) -> ValueArrayRefIter { ++ pub fn iter(&self) -> ValueArrayRefIter<'_> { + ValueArrayRefIter { + idx: 0, + va_ref: &self, +-- +2.52.0 + diff --git a/SOURCES/0103-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch b/SOURCES/0103-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch new file mode 100644 index 0000000..f367463 --- /dev/null +++ b/SOURCES/0103-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch @@ -0,0 +1,439 @@ +From b7063e97edf005e5e4553faa31c910ddab954257 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Thu, 21 May 2026 09:17:39 -0400 +Subject: [PATCH] Issue 7503 - CVE-2026-9064 - Add a limit to the number + controls per operation + +Description: + +Security fix for CVE-2026-9064 + +To prevent resource starvation limit the number of controls the server will +process per operation. Reject the operation if number of controls exceeds +the limit + +relates: https://github.com/389ds/389-ds-base/issues/7503 + +References: + - https://access.redhat.com/security/cve/cve-2026-9064 + - https://bugzilla.redhat.com/show_bug.cgi?id=2480093 + +CI test assisted by: Cursor + +Reviewed by: jchapman & tbordaz (Thanks!!) + +(cherry-picked 08a31c002b946973b09e01a5f27214273691fbea) +--- + .../suites/features/ldap_controls_test.py | 157 ++++++++++++++++++ + ldap/schema/01core389.ldif | 5 +- + ldap/servers/slapd/control.c | 14 +- + ldap/servers/slapd/libglobs.c | 52 ++++++ + ldap/servers/slapd/proto-slap.h | 2 + + ldap/servers/slapd/slap.h | 4 + + .../389-console/src/lib/server/tuning.jsx | 6 + + 7 files changed, 237 insertions(+), 3 deletions(-) + create mode 100644 dirsrvtests/tests/suites/features/ldap_controls_test.py + +diff --git a/dirsrvtests/tests/suites/features/ldap_controls_test.py b/dirsrvtests/tests/suites/features/ldap_controls_test.py +new file mode 100644 +index 000000000..59a58b21d +--- /dev/null ++++ b/dirsrvtests/tests/suites/features/ldap_controls_test.py +@@ -0,0 +1,157 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++ ++import logging ++import pytest ++import ldap ++from ldap.controls import RequestControl ++from ldap.controls.readentry import PostReadControl ++from lib389.idm.user import UserAccounts, UserAccount ++from test389.topologies import topology_st ++from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD ++ ++pytestmark = pytest.mark.tier1 ++ ++log = logging.getLogger(__name__) ++ ++MANAGE_DSAIT_OID = "2.16.840.1.113730.3.4.2" ++MAX_CONTROLS_PER_OP_ATTR = "nsslapd-maxcontrolsperop" ++ ++def test_postread_ctrl_modify(topology_st): ++ """Test PostReadControl with LDAP modify operations. ++ ++ :id: 47920dc1-7a9b-4e8d-9f3a-6c5d4e3f2a1b ++ :setup: Standalone instance ++ :steps: ++ 1. Create test user entry with initial description ++ 2. Verify initial description value ++ 3. Modify description with PostReadControl requesting 'cn' and 'description' ++ 4. Verify PostReadControl response contains both requested attributes ++ 5. Verify the entry was actually modified in the database ++ :expectedresults: ++ 1. User entry created successfully ++ 2. Initial description matches expected value ++ 3. Modify operation with control succeeds ++ 4. Control response contains both 'cn' and 'description' attributes ++ 5. Database entry reflects the modification ++ """ ++ inst = topology_st.standalone ++ INITIAL_DESC = "initial description" ++ FINAL_DESC = "final description" ++ ++ log.info("Create test user with initial description") ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ user = users.create(properties={ ++ 'uid': 'postread_user', ++ 'cn': 'postread_user', ++ 'sn': 'user', ++ 'uidNumber': '1000', ++ 'gidNumber': '2000', ++ 'homeDirectory': '/home/postread_user', ++ 'description': INITIAL_DESC ++ }) ++ ++ log.info("Verify initial description value") ++ assert user.present('description', INITIAL_DESC) ++ ++ log.info("Modify description with PostReadControl") ++ pr_ctrl = PostReadControl(criticality=True, attrList=['cn', 'description']) ++ msg_id = inst.modify_ext( ++ user.dn, ++ [(ldap.MOD_REPLACE, 'description', FINAL_DESC.encode('utf-8'))], ++ serverctrls=[pr_ctrl] ++ ) ++ ++ log.info("Get result with response controls") ++ _, _, _, resp_ctrls = inst.result3(msg_id) ++ ++ log.info("Verify PostReadControl response is properly encoded") ++ assert resp_ctrls, "Server should return PostReadControl" ++ assert resp_ctrls[0].dn == user.dn, "Control should return correct DN" ++ assert 'description' in resp_ctrls[0].entry, "Control should return description attribute" ++ assert 'cn' in resp_ctrls[0].entry, "Control should return cn attribute" ++ ++ log.info("Verify entry was modified with correct value") ++ user = UserAccount(inst, user.dn) ++ assert user.get_attr_val_utf8('description') == FINAL_DESC ++ user.delete() ++ ++ ++def _make_request_controls(count): ++ return [ ++ RequestControl(controlType=MANAGE_DSAIT_OID, criticality=False) ++ for _ in range(count) ++ ] ++ ++ ++def test_bind_excessive_controls(topology_st): ++ """Bind request control count is limited by nsslapd-maxcontrolsperop ++ ++ :id: c3888f02-2107-4682-a50a-2189d1436233 ++ :setup: Standalone instance ++ :steps: ++ 1. Read nsslapd-maxcontrolsperop from cn=config (default 10) ++ 2. Bind with one fewer control than the limit ++ 3. Bind with one more control than the limit ++ 4. Set nsslapd-maxcontrolsperop to 5 ++ 5. Bind with 4 controls (new limit minus one) ++ 6. Bind with 6 controls (over new limit) ++ 7. Restore nsslapd-maxcontrolsperop and re-bind as Directory Manager ++ :expectedresults: ++ 1. Config value is 10 ++ 2. Bind succeeds ++ 3. Bind fails with ldap.UNWILLING_TO_PERFORM ++ 4. Success ++ 5. Bind succeeds ++ 6. Bind fails with ldap.UNWILLING_TO_PERFORM ++ 7. Success ++ """ ++ inst = topology_st.standalone ++ original_max = inst.config.get_attr_val_utf8(MAX_CONTROLS_PER_OP_ATTR) ++ ++ try: ++ max_controls = int(inst.config.get_attr_val_utf8(MAX_CONTROLS_PER_OP_ATTR)) ++ assert max_controls == 10 ++ ++ log.info("Bind with %d controls (limit %d, limit minus one)", ++ max_controls - 1, max_controls) ++ inst.simple_bind_s(DN_DM, PASSWORD, ++ serverctrls=_make_request_controls(max_controls - 1)) ++ ++ log.info("Bind with %d controls (limit %d plus one)", ++ max_controls + 1, max_controls) ++ with pytest.raises(ldap.UNWILLING_TO_PERFORM): ++ inst.simple_bind_s(DN_DM, PASSWORD, ++ serverctrls=_make_request_controls(max_controls + 1)) ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ lowered_max = 5 ++ log.info("Set %s to %d", MAX_CONTROLS_PER_OP_ATTR, lowered_max) ++ inst.config.set(MAX_CONTROLS_PER_OP_ATTR, str(lowered_max)) ++ assert int(inst.config.get_attr_val_utf8(MAX_CONTROLS_PER_OP_ATTR)) == lowered_max ++ ++ log.info("Bind with %d controls (lowered limit %d, limit minus one)", ++ lowered_max - 1, lowered_max) ++ inst.simple_bind_s(DN_DM, PASSWORD, ++ serverctrls=_make_request_controls(lowered_max - 1)) ++ ++ log.info("Bind with %d controls (lowered limit %d plus one)", ++ lowered_max + 1, lowered_max) ++ with pytest.raises(ldap.UNWILLING_TO_PERFORM): ++ inst.simple_bind_s(DN_DM, PASSWORD, ++ serverctrls=_make_request_controls(lowered_max + 1)) ++ finally: ++ log.info("Restore %s to %s", MAX_CONTROLS_PER_OP_ATTR, original_max) ++ inst.config.set(MAX_CONTROLS_PER_OP_ATTR, original_max) ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ ++if __name__ == '__main__': ++ CURRENT_FILE = __file__ ++ pytest.main(["-s", "-v", CURRENT_FILE]) ++ +diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif +index 4745a6ca0..d2f9db08f 100644 +--- a/ldap/schema/01core389.ldif ++++ b/ldap/schema/01core389.ldif +@@ -5,7 +5,7 @@ + # All rights reserved. + # + # License: GPL (version 3 or any later version). +-# See LICENSE for details. ++# See LICENSE for details. + # END COPYRIGHT BLOCK + # + # +@@ -331,6 +331,9 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2390 NAME 'nsds5ReplicaKeepAliveUpdateIn + attributeTypes: ( 2.16.840.1.113730.3.1.2391 NAME 'dsEntryDN' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 NO-USER-MODIFICATION SINGLE-VALUE USAGE directoryOperation X-ORIGIN '389 Directory Server' ) + attributeTypes: ( 2.16.840.1.113730.3.1.2392 NAME 'nsslapd-return-original-entrydn' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) + attributeTypes: ( 2.16.840.1.113730.3.1.2393 NAME 'nsslapd-auditlog-display-attrs' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) ++attributeTypes: ( 2.16.840.1.113730.3.1.2398 NAME 'nsslapd-haproxy-trusted-ip' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN '389 Directory Server' ) ++attributeTypes: ( 2.16.840.1.113730.3.1.2400 NAME 'nsslapd-pwdPBKDF2NumIterations' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Directory Server' ) ++attributeTypes: ( 2.16.840.1.113730.3.1.2402 NAME 'nsslapd-maxcontrolsperop' DESC '389 Directory Server defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN '389 Directory Server' ) + # + # objectclasses + # +diff --git a/ldap/servers/slapd/control.c b/ldap/servers/slapd/control.c +index 4fd8473be..d9b1db2fc 100644 +--- a/ldap/servers/slapd/control.c ++++ b/ldap/servers/slapd/control.c +@@ -175,7 +175,7 @@ get_ldapmessage_controls_ext( + ber_tag_t tag; + /* ber_len_t is uint, cannot be -1 */ + ber_len_t len = LBER_ERROR; +- int rc, maxcontrols, curcontrols; ++ int rc, maxcontrols, curcontrols, maxcontrols_per_op; + char *last; + int managedsait, pwpolicy_ctrl; + Connection *pb_conn = NULL; +@@ -251,11 +251,21 @@ get_ldapmessage_controls_ext( + return (LDAP_PROTOCOL_ERROR); + } + ++ maxcontrols_per_op = config_get_maxcontrolsperop(); + maxcontrols = curcontrols = 0; + for (tag = ber_first_element(ber, &len, &last); + tag != LBER_ERROR && tag != LBER_END_OF_SEQORSET; +- tag = ber_next_element(ber, &len, last)) { ++ tag = ber_next_element(ber, &len, last)) ++ { + len = -1; /* reset */ ++ if (curcontrols >= maxcontrols_per_op) { ++ slapi_log_err(SLAPI_LOG_ERR, "get_ldapmessage_controls_ext", ++ "Too many controls in LDAP request (max %d)\n", ++ maxcontrols_per_op); ++ rc = LDAP_UNWILLING_TO_PERFORM; ++ goto free_and_return; ++ } ++ + if (curcontrols >= maxcontrols - 1) { + #define CONTROL_GRABSIZE 6 + maxcontrols += CONTROL_GRABSIZE; +diff --git a/ldap/servers/slapd/libglobs.c b/ldap/servers/slapd/libglobs.c +index 99b2c5d8e..d956f6940 100644 +--- a/ldap/servers/slapd/libglobs.c ++++ b/ldap/servers/slapd/libglobs.c +@@ -1323,6 +1323,11 @@ static struct config_get_and_set + (void **)&global_slapdFrontendConfig.ldapssotoken_ttl, + CONFIG_INT, NULL, SLAPD_DEFAULT_LDAPSSOTOKEN_TTL_STR, NULL}, + #endif ++ {CONFIG_MAXCONTROLS_PER_OP_ATTRIBUTE, config_set_maxcontrolsperop, ++ NULL, 0, ++ (void **)&global_slapdFrontendConfig.maxcontrols_per_op, ++ CONFIG_INT, (ConfigGetFunc)config_get_maxcontrolsperop, ++ SLAPD_DEFAULT_MAXCONTROLS_PER_OP_STR, NULL}, + /* End config */ + }; + +@@ -1836,6 +1841,7 @@ FrontendConfig_init(void) + init_cn_uses_dn_syntax_in_dns = cfg->cn_uses_dn_syntax_in_dns = LDAP_OFF; + init_global_backend_local = LDAP_OFF; + cfg->maxsimplepaged_per_conn = SLAPD_DEFAULT_MAXSIMPLEPAGED_PER_CONN; ++ cfg->maxcontrols_per_op = SLAPD_DEFAULT_MAXCONTROLS_PER_OP; + cfg->maxbersize = SLAPD_DEFAULT_MAXBERSIZE; + cfg->logging_backend = slapi_ch_strdup(SLAPD_INIT_LOGGING_BACKEND_INTERNAL); + cfg->rootdn = slapi_ch_strdup(SLAPD_DEFAULT_DIRECTORY_MANAGER); +@@ -9046,6 +9052,52 @@ config_get_maxsimplepaged_per_conn() + return retVal; + } + ++int ++config_set_maxcontrolsperop(const char *attrname, char *value, char *errorbuf, int apply) ++{ ++ int retVal = LDAP_SUCCESS; ++ slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig(); ++ long size; ++ char *endp; ++ ++ if (config_value_is_null(attrname, value, errorbuf, 0)) { ++ return LDAP_OPERATIONS_ERROR; ++ } ++ ++ errno = 0; ++ size = strtol(value, &endp, 10); ++ if (*endp != '\0' || errno == ERANGE || size < 1 || size > 1000) { ++ slapi_create_errormsg(errorbuf, SLAPI_DSE_RETURNTEXT_SIZE, ++ "(%s) value (%s) is invalid, must be at least 1 and less than 1000\n", ++ attrname, value); ++ return LDAP_OPERATIONS_ERROR; ++ } ++ ++ if (!apply) { ++ return retVal; ++ } ++ ++ CFG_LOCK_WRITE(slapdFrontendConfig); ++ ++ slapdFrontendConfig->maxcontrols_per_op = size; ++ ++ CFG_UNLOCK_WRITE(slapdFrontendConfig); ++ return retVal; ++} ++ ++int ++config_get_maxcontrolsperop() ++{ ++ slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig(); ++ int retVal; ++ ++ retVal = slapdFrontendConfig->maxcontrols_per_op; ++ if (retVal == 0) { ++ retVal = SLAPD_DEFAULT_MAXCONTROLS_PER_OP; ++ } ++ return retVal; ++} ++ + int32_t + config_set_extract_pem(const char *attrname, char *value, char *errorbuf, int apply) + { +diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h +index ab42c0e0f..2ec7c06b7 100644 +--- a/ldap/servers/slapd/proto-slap.h ++++ b/ldap/servers/slapd/proto-slap.h +@@ -410,6 +410,7 @@ int32_t config_set_maxdescriptors(const char *attrname, char *value, char *error + int config_set_localuser(const char *attrname, char *value, char *errorbuf, int apply); + + int config_set_maxsimplepaged_per_conn(const char *attrname, char *value, char *errorbuf, int apply); ++int config_set_maxcontrolsperop(const char *attrname, char *value, char *errorbuf, int apply); + + int log_external_libs_debug_set_log_fn(void); + int log_set_backend(const char *attrname, char *value, int logtype, char *errorbuf, int apply); +@@ -609,6 +610,7 @@ int config_get_malloc_mmap_threshold(void); + #endif + + int config_get_maxsimplepaged_per_conn(void); ++int config_get_maxcontrolsperop(void); + int config_get_extract_pem(void); + + int32_t config_get_enable_upgrade_hash(void); +diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h +index 0f67dda7d..6296de5af 100644 +--- a/ldap/servers/slapd/slap.h ++++ b/ldap/servers/slapd/slap.h +@@ -290,6 +290,8 @@ upgrade_status upgrade_server(void); + #define SLAPD_DEFAULT_MAXSIMPLEPAGED_PER_CONN_STR "-1" + /* We'd like this number to be prime for the hash into the Connection table */ + #define SLAPD_DEFAULT_CONNTABLESIZE 64000 /* connection table size */ ++#define SLAPD_DEFAULT_MAXCONTROLS_PER_OP 10 ++#define SLAPD_DEFAULT_MAXCONTROLS_PER_OP_STR "10" + #define SLAPD_DEFAULT_LDAPSSOTOKEN_TTL 3600 + #define SLAPD_DEFAULT_LDAPSSOTOKEN_TTL_STR "3600" + +@@ -2318,6 +2320,7 @@ typedef struct _slapdEntryPoints + #define CONFIG_CN_USES_DN_SYNTAX_IN_DNS "nsslapd-cn-uses-dn-syntax-in-dns" + + #define CONFIG_MAXSIMPLEPAGED_PER_CONN_ATTRIBUTE "nsslapd-maxsimplepaged-per-conn" ++#define CONFIG_MAXCONTROLS_PER_OP_ATTRIBUTE "nsslapd-maxcontrolsperop" + #define CONFIG_LOGGING_BACKEND "nsslapd-logging-backend" + + #define CONFIG_EXTRACT_PEM "nsslapd-extract-pemfiles" +@@ -2613,6 +2616,7 @@ typedef struct _slapdFrontendConfig + slapi_onoff_t cn_uses_dn_syntax_in_dns; /* indicates the cn value in dns has dn syntax */ + slapi_onoff_t global_backend_lock; + slapi_int_t maxsimplepaged_per_conn; /* max simple paged results reqs handled per connection */ ++ slapi_int_t maxcontrols_per_op; /* max LDAP controls allowed per operation */ + slapi_onoff_t enable_nunc_stans; /* Despite the removal of NS, we have to leave the value in + * case someone was setting it. + */ +diff --git a/src/cockpit/389-console/src/lib/server/tuning.jsx b/src/cockpit/389-console/src/lib/server/tuning.jsx +index a8abe1fcc..d5a8817bc 100644 +--- a/src/cockpit/389-console/src/lib/server/tuning.jsx ++++ b/src/cockpit/389-console/src/lib/server/tuning.jsx +@@ -27,6 +27,7 @@ const tuning_attrs = [ + 'nsslapd-connection-nocanon', + 'nsslapd-enable-turbo-mode', + 'nsslapd-threadnumber', ++ 'nsslapd-maxthreadsperconn', + 'nsslapd-maxdescriptors', + 'nsslapd-timelimit', + 'nsslapd-sizelimit', +@@ -38,6 +39,7 @@ const tuning_attrs = [ + 'nsslapd-maxsasliosize', + 'nsslapd-listen-backlog-size', + 'nsslapd-max-filter-nest-level', ++ 'nsslapd-maxcontrolsperop', + 'nsslapd-ndn-cache-max-size', + ]; + +@@ -163,6 +165,7 @@ export class ServerTuning extends React.Component { + 'nsslapd-connection-nocanon': connNoCannon, + 'nsslapd-enable-turbo-mode': turboMode, + 'nsslapd-threadnumber': attrs['nsslapd-threadnumber'][0], ++ 'nsslapd-maxthreadsperconn': attrs['nsslapd-maxthreadsperconn'][0], + 'nsslapd-maxdescriptors': attrs['nsslapd-maxdescriptors'][0], + 'nsslapd-timelimit': attrs['nsslapd-timelimit'][0], + 'nsslapd-sizelimit': attrs['nsslapd-sizelimit'][0], +@@ -174,6 +177,7 @@ export class ServerTuning extends React.Component { + 'nsslapd-maxsasliosize': attrs['nsslapd-maxsasliosize'][0], + 'nsslapd-listen-backlog-size': attrs['nsslapd-listen-backlog-size'][0], + 'nsslapd-max-filter-nest-level': attrs['nsslapd-max-filter-nest-level'][0], ++ 'nsslapd-maxcontrolsperop': attrs['nsslapd-maxcontrolsperop'][0], + 'nsslapd-ndn-cache-max-size': attrs['nsslapd-ndn-cache-max-size'][0], + // Record original values + '_nsslapd-ndn-cache-enabled': ndnEnabled, +@@ -181,6 +185,7 @@ export class ServerTuning extends React.Component { + '_nsslapd-connection-nocanon': connNoCannon, + '_nsslapd-enable-turbo-mode': turboMode, + '_nsslapd-threadnumber': attrs['nsslapd-threadnumber'][0], ++ '_nsslapd-maxthreadsperconn': attrs['nsslapd-maxthreadsperconn'][0], + '_nsslapd-maxdescriptors': attrs['nsslapd-maxdescriptors'][0], + '_nsslapd-timelimit': attrs['nsslapd-timelimit'][0], + '_nsslapd-sizelimit': attrs['nsslapd-sizelimit'][0], +@@ -192,6 +197,7 @@ export class ServerTuning extends React.Component { + '_nsslapd-maxsasliosize': attrs['nsslapd-maxsasliosize'][0], + '_nsslapd-listen-backlog-size': attrs['nsslapd-listen-backlog-size'][0], + '_nsslapd-max-filter-nest-level': attrs['nsslapd-max-filter-nest-level'][0], ++ '_nsslapd-maxcontrolsperop': attrs['nsslapd-maxcontrolsperop'][0], + '_nsslapd-ndn-cache-max-size': attrs['nsslapd-ndn-cache-max-size'][0], + }, this.props.enableTree()); + }) +-- +2.52.0 + diff --git a/SPECS/389-ds-base.spec b/SPECS/389-ds-base.spec index 2fcb0aa..91c372c 100644 --- a/SPECS/389-ds-base.spec +++ b/SPECS/389-ds-base.spec @@ -52,7 +52,7 @@ ExcludeArch: i686 Summary: 389 Directory Server (base) Name: 389-ds-base Version: 1.4.3.39 -Release: %{?relprefix}23%{?prerel}%{?dist} +Release: %{?relprefix}24%{?prerel}%{?dist} License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (0BSD OR Apache-2.0 OR MIT) AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR BSD-2-Clause OR MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT OR Zlib) AND (Apache-2.0 OR MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND Apache-2.0 AND BSD-3-Clause AND MIT AND MPL-2.0 URL: https://www.port389.org Group: System Environment/Daemons @@ -385,7 +385,22 @@ Patch84: 0084-Issue-7223-Use-lexicographical-order-for-ancestorid.patch Patch85: 0085-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch Patch86: 0086-Security-fix-for-CVE-2025-14905.patch Patch87: 0087-Issue-7096-During-replication-online-total-init-the-.patch - +Patch88: 0088-Issue-7366-Memory-leaks-in-syncrepl-plugin-during-pe.patch +Patch89: 0089-Issue-7126-WARN-keys2idl-received-NULL-idl-from-inde.patch +Patch90: 0090-Issue-7124-2nd-BDB-cursor-race-condition-with-transa.patch +Patch91: 0091-Issue-7124-BDB-cursor-race-condition-with-transactio.patch +Patch92: 0092-Issue-7380-Internal-op-with-negative-wtime-and-large.patch +Patch93: 0093-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch +Patch94: 0094-Issue-7271-plugins-that-create-threads-need-to-updat.patch +Patch95: 0095-Issue-7271-implement-a-pre-close-plugin-function.patch +Patch96: 0096-Issue-7271-Add-new-plugin-pre-close-function-check-t.patch +Patch97: 0097-Issue-7304-retrocl-should-not-cache-DN.patch +Patch98: 0098-Issue-7291-Crash-when-configuring-a-replica-with-an-.patch +Patch99: 0099-Issue-7300-RFE-Add-OS-level-thread-names-to-all-serv.patch +Patch100: 0100-Issue-5947-Do-not-release-the-target-entry-in-ldbm_b.patch +Patch101: 0101-Issue-7271-fixed-build-error-in-repl5_init.patch +Patch102: 0102-Issue-6929-Compilation-failure-with-rust-1.89.patch +Patch103: 0103-Issue-7503-CVE-2026-9064-Add-a-limit-to-the-number-c.patch #Patch100: cargo.patch @@ -1045,6 +1060,20 @@ exit 0 %doc README.md %changelog +* Wed May 13 2026 Arun Bansal - 1.4.3.39-24 +- Bump version to 1.4.3.39-24 +- Resolves: RHEL-170278 - Memory leaks in syncrepl plugin during persistent search operations [rhel-8.10.z] +- Resolves: RHEL-163375 - WARN - keys2idl - received NULL idl from index_read_ext_allids +- Resolves: RHEL-159306 - ns-slapd crash in libdb possible memory corruption [rhel-8.10.z] +- Resolves: RHEL-170284 - access log - suspicious wtime optime negative and large values in internal op [rhel-8.10.z] +- Resolves: RHEL-170507 - ns-slapd fails to shutdown when deferred memberof update is in progress [rhel-8.10.z] +- Resolves: RHEL-170509 - Crash in trim_changelog() during the Retro Changelog trimming [rhel-8.10.z] +- Resolves: RHEL-170514 - Possible memory leak when using the Retro Changelog plugin [rhel-8.10.z] +- Resolves: RHEL-170512 - Crash in replica_config_add when manually configuring a replica with an incorrect nsds5ReplicaRoot [rhel-8.10.z] +- Resolves: RHEL-174523 - [RFE] Add OS-level thread names to all server threads [rhel-8.10.z] +- Resolves: RHEL-170483 - test_vlv_recreation_reindex fails on LMDB [rhel-8.10.z] +- Resolves: RHEL-178076 - CVE-2026-9064 389-ds:1.4/389-ds-base: unbounded LDAP controls count in get_ldapmessage_controls_ext() causes CPU and heap amplification (remote DoS) [rhel-8.10.z] + * Wed Mar 04 2026 Arun Bansal - 1.4.3.39-23 - Resolves: RHEL-137074 - CVE-2025-14905 389-ds:1.4/389-ds-base: 389-ds-base: Remote Code Execution and Denial of Service via heap buffer overflow [rhel-8.10.z] - Resolves: RHEL-152098 - Scalability issue of replication online initialization with large database [rhel-8.10.z]