diff --git a/0009-Issue-7500-Prevent-unsigned-integer-underflow-during.patch b/0009-Issue-7500-Prevent-unsigned-integer-underflow-during.patch new file mode 100644 index 0000000..e180061 --- /dev/null +++ b/0009-Issue-7500-Prevent-unsigned-integer-underflow-during.patch @@ -0,0 +1,89 @@ +From 6ae6fdbf9c42f30a5653215ff271d700e6a1a0ce Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Mon, 18 May 2026 14:27:04 -0400 +Subject: [PATCH 09/14] Issue 7500 - Prevent unsigned integer underflow during + stalled import + +Description: + +During a stalled import the foreman's first ID could be greater than the +history progress size which leads to underflowing the rate. Check if the +foreman's first ID is greater than the progress size and just set it to +zero. + +Relates: https://github.com/389ds/389-ds-base/issues/7500 + +Reviewed by: progier(Thanks!) +--- + .../slapd/back-ldbm/db-bdb/bdb_import.c | 22 ++++++++++++++----- + .../slapd/back-ldbm/db-mdb/mdb_import.c | 4 ++-- + 2 files changed, 19 insertions(+), 7 deletions(-) + +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 bdc4ee4f6..0815c48e9 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +@@ -1765,16 +1765,28 @@ bdb_import_monitor_threads(ImportJob *job, int *status) + /* Now calculate our rate of progress overall for this chunk */ + if (time_now != job->start_time) { + /* log a cute chart of the worker progress */ ++ uint32_t history_size = 0; ++ double rate = 0.0; ++ + bdb_import_log_status_start(job); + bdb_import_log_status_add_line(job, +- "Index status for import of %s:", job->inst->inst_name); ++ "Index status for import of %s:", ++ job->inst->inst_name); + bdb_import_log_status_add_line(job, +- "-------Index Task-------State---Entry----Rate-"); ++ "-------Index Task-------State---Entry----Rate-"); + + bdb_import_push_progress_history(job, foreman->last_ID_processed, +- time_now); +- job->average_progress_rate = +- (double)(HISTORY(IMPORT_JOB_PROG_HISTORY_SIZE - 1) + 1 - foreman->first_ID) / ++ time_now); ++ ++ history_size = HISTORY(IMPORT_JOB_PROG_HISTORY_SIZE - 1) + 1; ++ if (foreman->first_ID > history_size) { ++ /* Import is stalled and subtracting first_ID will ++ * underflow the rate - so set it to 0.0 */ ++ rate = 0.0; ++ } else { ++ rate = (double)(history_size - foreman->first_ID); ++ } ++ job->average_progress_rate = rate / + (double)(TIMES(IMPORT_JOB_PROG_HISTORY_SIZE - 1) - job->start_time); + job->recent_progress_rate = + PROGRESS(0, IMPORT_JOB_PROG_HISTORY_SIZE - 1); +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c +index 51f1272f4..10ac903d2 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c +@@ -586,7 +586,6 @@ dbmdb_import_monitor_threads(ImportJob *job, int *status) + int count = 1; /* 1 to prevent premature status report */ + const int display_interval = 200; + time_t time_now = 0; +- int i = 0; + + for (current_worker = job->worker_list; current_worker != NULL; + current_worker = current_worker->next) +@@ -615,12 +614,13 @@ dbmdb_import_monitor_threads(ImportJob *job, int *status) + dbmdb_import_clear_progress_history(job); + + while (!finished) { ++ size_t max_slots = ctx->workerq.max_slots; + DS_Sleep(tenthsecond); + finished = 1; + + /* Compute the number of entries processed by the workers */ + entry_processed = 0; +- for (i=0; iworkerq.max_slots; i++) { ++ for (size_t i = 0; i < max_slots; i++) { + entry_processed += slots[i].count; + } + +-- +2.54.0 + diff --git a/0010-Issue-7558-During-online-import-the-IDL-should-be-cr.patch b/0010-Issue-7558-During-online-import-the-IDL-should-be-cr.patch new file mode 100644 index 0000000..b55bf35 --- /dev/null +++ b/0010-Issue-7558-During-online-import-the-IDL-should-be-cr.patch @@ -0,0 +1,598 @@ +From 6a5aa0434fb32467fccb746915aa1a9ac740e4a6 Mon Sep 17 00:00:00 2001 +From: tbordaz +Date: Wed, 10 Jun 2026 15:14:08 +0200 +Subject: [PATCH 10/14] Issue 7558 - During online import, the IDL should be + created with in-depth first approach (#7559) + +Bug description: + The online initialization requires that the supplier builds a sorted IDL. + It is sorted in the way that the parent entry appears before the children in the IDL. + The current implementation goes through the parentid index from the first entry + until the end (next). To make sure parent entry is already in IDL before adding a child + it uses a list of ID ranges. + This list works well if the ID are mostly consecutive and with limited number of holes, + else the list grows (lot of singleton) and checking the list becomes costly as well. + +Fix description: + Instead of walking the parentid index from the first entry to the end it walks + the parentid in depth first. So there is no need to check that the parent ID is already + present in IDL + +fixes: #7558 + +Reviewed by: Pierre Rogier (Thanks !!) +--- + ldap/servers/slapd/back-ldbm/idl_new.c | 459 +++++++++++++++---------- + 1 file changed, 285 insertions(+), 174 deletions(-) + +diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c +index 613d53815..29457500e 100644 +--- a/ldap/servers/slapd/back-ldbm/idl_new.c ++++ b/ldap/servers/slapd/back-ldbm/idl_new.c +@@ -45,13 +45,6 @@ struct idl_private + int dummy; + }; + +-/* Used to store leftover parentid and entry ids */ +-typedef struct _range_id_pair +-{ +- ID key; +- ID id; +-} idl_range_id_pair; +- + /* lmdb iterator callback context */ + typedef struct { + backend *be; +@@ -62,18 +55,31 @@ typedef struct { + struct timespec *expire_time; + int lookthrough_limit; + int operator; +- idl_range_id_pair *leftover; +- size_t leftoverlen; +- size_t leftovercnt; + IDList *idl; +- IdRange_t *idrange_list; + int flag_err; + ID lastid; +- ID suffix; + uint64_t count; + char *index_id; + } idl_range_ctx_t; + ++/* Context for depth-first parentid index walk (bulk import) */ ++typedef struct { ++ backend *be; ++ dbi_db_t *db; ++ dbi_txn_t *txn; ++ struct attrinfo *ai; ++ IDList *idl; ++ int *flag_err; ++ int allidslimit; ++ int sizelimit; ++ struct timespec *expire_time; ++ int lookthrough_limit; ++ uint64_t count; ++ char *index_id; ++ char keybuf[32]; ++ dbi_val_t key; ++} idl_parentid_walk_ctx_t; ++ + + static int idl_tune = DEFAULT_IDL_TUNE; /* tuning parameters for IDL code */ + /* Currently none for new IDL code */ +@@ -383,22 +389,263 @@ keycmp(dbi_val_t *L, dbi_val_t *R, value_compare_fn_type cmp_fn) + return cmp_fn(&Lv, &Rv); + } + ++static void ++idl_parentid_sorted_suffix_init(backend *be, ID *suffix, const char *logfn) ++{ ++ struct _back_info_index_key bck_info; ++ int rc; ++ ++ bck_info.index = SLAPI_ATTR_PARENTID; ++ bck_info.key = "0"; ++ *suffix = 0; ++ ++ rc = slapi_back_get_info(be, BACK_INFO_INDEX_KEY, (void **)&bck_info); ++ if (rc) { ++ slapi_log_err(SLAPI_LOG_WARNING, logfn, ++ "Total update: fail to retrieve suffix entryID, continue assuming it is the first entry\n"); ++ } ++ if (bck_info.key_found) { ++ *suffix = bck_info.id; ++ } ++} ++ ++static void ++idl_parentid_set_index_key(idl_parentid_walk_ctx_t *ctx, ID parent_id) ++{ ++ ctx->key.ulen = sizeof(ctx->keybuf); ++ ctx->key.size = PR_snprintf(ctx->keybuf, ctx->key.ulen, "%c%lu", ++ EQ_PREFIX, (u_long)parent_id); ++ ctx->key.size++; /* include the null terminator */ ++ dblayer_value_set_buffer(ctx->be, &ctx->key, ctx->keybuf, ctx->key.size); ++} ++ ++static int ++idl_parentid_walk_check_limits(idl_parentid_walk_ctx_t *ctx) ++{ ++ if (ctx->idl) { ++ if ((ctx->lookthrough_limit != -1) && ++ (ctx->idl->b_nids > (ID)ctx->lookthrough_limit)) { ++ idl_free(&ctx->idl); ++ ctx->idl = idl_allids(ctx->be); ++ slapi_log_err(SLAPI_LOG_TRACE, "idl_new_parentid_sorted_range_fetch", ++ "lookthrough_limit exceeded\n"); ++ *(ctx->flag_err) = LDAP_ADMINLIMIT_EXCEEDED; ++ return -1; ++ } ++ if ((ctx->sizelimit > 0) && (ctx->idl->b_nids > (ID)ctx->sizelimit)) { ++ slapi_log_err(SLAPI_LOG_TRACE, "idl_new_parentid_sorted_range_fetch", ++ "sizelimit exceeded\n"); ++ *(ctx->flag_err) = LDAP_SIZELIMIT_EXCEEDED; ++ return -1; ++ } ++ } ++ if (slapi_timespec_expire_check(ctx->expire_time) == TIMER_EXPIRED) { ++ slapi_log_err(SLAPI_LOG_TRACE, "idl_new_parentid_sorted_range_fetch", ++ "timelimit exceeded\n"); ++ *(ctx->flag_err) = LDAP_TIMELIMIT_EXCEEDED; ++ return -1; ++ } ++ return 0; ++} ++ ++static int ++idl_parentid_walk_push(ID **stack, size_t *stack_size, size_t *stack_top, ID id) ++{ ++ if (*stack_top >= *stack_size) { ++ size_t new_size = (*stack_size == 0) ? 256 : (*stack_size * 2); ++ ID *new_stack = (ID *)slapi_ch_realloc((char *)*stack, new_size * sizeof(ID)); ++ ++ if (new_stack == NULL) { ++ return -1; ++ } ++ *stack = new_stack; ++ *stack_size = new_size; ++ } ++ (*stack)[(*stack_top)++] = id; ++ return 0; ++} ++ + /* +- * Perform the range search in the idl layer instead of the index layer +- * to improve the performance. ++ * Walk the parentid index depth-first starting at root_id. ++ * For each parent, look up index key "=parent_id" (direct get, not cursor next), ++ * append the parent to the IDList, then visit each child the same way. + */ ++static int ++idl_parentid_walk_tree(idl_parentid_walk_ctx_t *ctx, ID root_id, ID **stack, ++ size_t *stack_size, size_t *stack_top) ++{ ++ IDList *children = NULL; ++ int fetch_err = NEW_IDL_NO_ALLID; ++ size_t i; ++ ++ if (idl_parentid_walk_push(stack, stack_size, stack_top, root_id) != 0) { ++ return -1; ++ } ++ ++ while (*stack_top > 0) { ++ ID parent_id = (*stack)[--(*stack_top)]; ++ ++ if (idl_parentid_walk_check_limits(ctx) != 0) { ++ return -1; ++ } ++ ++ if (idl_append_extend(&ctx->idl, parent_id) != 0) { ++ slapi_log_err(SLAPI_LOG_ERR, "idl_new_parentid_sorted_range_fetch", ++ "Unable to extend id list for attribute (%s)\n", ctx->index_id); ++ idl_free(&ctx->idl); ++ return -1; ++ } ++ ctx->count++; ++ ++#if defined(DB_ALLIDS_ON_READ) ++ if ((NEW_IDL_NO_ALLID != *(ctx->flag_err)) && ctx->ai && (ctx->idl != NULL) && ++ idl_new_exceeds_allidslimit(ctx->count, ctx->ai, ctx->allidslimit)) { ++ ctx->idl->b_nids = 1; ++ ctx->idl->b_ids[0] = ALLID; ++ return 0; ++ } ++#endif ++ idl_parentid_set_index_key(ctx, parent_id); ++ children = idl_fetch_ext(ctx->be, ctx->db, &ctx->key, ctx->txn, ctx->ai, ++ &fetch_err, ctx->allidslimit); ++ if (fetch_err != 0 && fetch_err != DBI_RC_NOTFOUND) { ++ slapi_log_err(SLAPI_LOG_ERR, "idl_new_parentid_sorted_range_fetch", ++ "Failed to read parentid index key %s (err=%d)\n", ++ (char *)ctx->key.data, fetch_err); ++ *(ctx->flag_err) = fetch_err; ++ return -1; ++ } ++ if (children == NULL) { ++ continue; ++ } ++ if (ALLIDS(children)) { ++ idl_free(&ctx->idl); ++ ctx->idl = idl_allids(ctx->be); ++ idl_free(&children); ++ return 0; ++ } ++ for (i = children->b_nids; i > 0; i--) { ++ if (idl_parentid_walk_push(stack, stack_size, stack_top, ++ children->b_ids[i - 1]) != 0) { ++ idl_free(&children); ++ return -1; ++ } ++ } ++ idl_free(&children); ++ } ++ return 0; ++} ++ + /* +- * NOTE: +- * In the total update (bulk import), an entry requires its ancestors already added. +- * To guarantee it, the range search with parentid is used with setting the flag +- * SLAPI_OP_RANGE_NO_IDL_SORT in operator. +- * In bulk import the range search is parentid>=1 to retrieve all the entries +- * But we need to order the IDL with the parents first => retrieve the suffix entry ID +- * to store the children ++ * Build an IDList from the parentid index with ancestors before descendants. ++ * Uses direct key lookups (idl_fetch) and depth-first traversal from the ++ * suffix entry, rather than a full index scan with membership checks. + * +- * If the flag is set, +- * 1. the IDList is not sorted by the ID. +- * 2. holding to add an ID to the IDList unless the key is found in the IDList. ++ * Used during bulk import (SLAPI_OP_RANGE_NO_IDL_SORT). ++ */ ++static IDList * ++idl_new_parentid_sorted_range_fetch( ++ backend *be, ++ dbi_db_t *db, ++ dbi_val_t *lowerkey, ++ dbi_val_t *upperkey, ++ dbi_txn_t *txn, ++ struct attrinfo *ai, ++ int *flag_err, ++ int allidslimit, ++ int sizelimit, ++ struct timespec *expire_time, ++ int lookthrough_limit, ++ int operator) ++{ ++ int ret = 0; ++ ID suffix = 0; ++ ID *stack = NULL; ++ size_t stack_size = 0; ++ size_t stack_top = 0; ++ char *index_id = get_index_name(be, db, ai); ++ idl_parentid_walk_ctx_t walk = {0}; ++ IDList *suffix_idl = NULL; ++ int suffix_err = 0; ++ ++ walk.be = be; ++ walk.db = db; ++ walk.txn = txn; ++ walk.ai = ai; ++ walk.flag_err = flag_err; ++ walk.allidslimit = allidslimit; ++ walk.sizelimit = sizelimit; ++ walk.expire_time = expire_time; ++ walk.lookthrough_limit = lookthrough_limit; ++ walk.index_id = index_id; ++ walk.idl = idl_alloc(IDLIST_MIN_BLOCK_SIZE); ++ if (walk.idl == NULL) { ++ *flag_err = ENOMEM; ++ return NULL; ++ } ++ ++ idl_parentid_sorted_suffix_init(be, &suffix, "idl_new_parentid_sorted_range_fetch"); ++ if (suffix == 0) { ++ idl_parentid_set_index_key(&walk, 0); ++ suffix_idl = idl_fetch_ext(be, db, &walk.key, txn, ai, &suffix_err, allidslimit); ++ if (suffix_idl && suffix_idl->b_nids > 0 && !ALLIDS(suffix_idl)) { ++ suffix = suffix_idl->b_ids[0]; ++ } ++ idl_free(&suffix_idl); ++ } ++ if (suffix == 0) { ++ slapi_log_err(SLAPI_LOG_ERR, "idl_new_parentid_sorted_range_fetch", ++ "Unable to determine suffix entry ID for parentid tree walk\n"); ++ idl_free(&walk.idl); ++ *flag_err = LDAP_UNWILLING_TO_PERFORM; ++ return NULL; ++ } ++ ++ if (slapi_is_loglevel_set(SLAPI_LOG_FILTER)) { ++ char *included = ((operator & SLAPI_OP_RANGE) == SLAPI_OP_LESS) ? "not " : ""; ++ slapi_log_err(SLAPI_LOG_FILTER, ++ "idl_new_parentid_sorted_range_fetch", ++ "Walking parentid index from suffix ID %u, keys %s to %s\n", ++ suffix, (char *)lowerkey->data, ++ upperkey && upperkey->data ? (char *)upperkey->data : "(none)"); ++ slapi_log_err(SLAPI_LOG_FILTER, "idl_new_parentid_sorted_range_fetch", ++ "Candidate list is not sorted. lower key is %sincluded.\n", included); ++ } ++ ++ if (idl_parentid_walk_tree(&walk, suffix, &stack, &stack_size, &stack_top) != 0) { ++ if (*flag_err == 0) { ++ *flag_err = LDAP_UNWILLING_TO_PERFORM; ++ } ++ ret = *flag_err; ++ } ++ ++ slapi_ch_free((void **)&stack); ++ ++ if (walk.idl && (walk.idl->b_nids == 1) && (walk.idl->b_ids[0] == ALLID)) { ++ idl_free(&walk.idl); ++ walk.idl = idl_allids(be); ++ slapi_log_err(SLAPI_LOG_TRACE, "idl_new_parentid_sorted_range_fetch", ++ "%s returns allids\n", index_id); ++ } else { ++ slapi_log_err(SLAPI_LOG_TRACE, "idl_new_parentid_sorted_range_fetch", ++ "%s returns nids=%lu\n", index_id, (u_long)IDL_NIDS(walk.idl)); ++ } ++ ++ if (ret) { ++ slapi_log_err(SLAPI_LOG_ERR, "idl_new_parentid_sorted_range_fetch", ++ "Failed to build parentid candidate list on %s index. Error is %d\n", ++ index_id, ret); ++ } ++ *flag_err = ret; ++ slapi_log_err(SLAPI_LOG_FILTER, "idl_new_parentid_sorted_range_fetch", ++ "Found %d candidates; error code is: %d\n", ++ walk.idl ? walk.idl->b_nids : 0, *flag_err); ++ return walk.idl; ++} ++ ++/* ++ * Perform the range search in the idl layer instead of the index layer ++ * to improve the performance. + */ + IDList * + idl_new_range_fetch( +@@ -430,39 +677,19 @@ idl_new_range_fetch( + back_txn s_txn; + struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private; + int coreop = operator&SLAPI_OP_RANGE; +- ID key = 0xff; /* random- to suppress compiler warning */ +- ID suffix = 0; /* random- to suppress compiler warning */ +- idl_range_id_pair *leftover = NULL; +- size_t leftoverlen = 32; +- size_t leftovercnt = 0; + char *index_id = get_index_name(be, db, ai); +- IdRange_t *idrange_list = NULL; +- + + if (NULL == flag_err) { + return NULL; + } +- if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) { +- struct _back_info_index_key bck_info; +- int rc; +- /* We are doing a bulk import +- * try to retrieve the suffix entry id from the index +- */ +- +- bck_info.index = SLAPI_ATTR_PARENTID; +- bck_info.key = "0"; +- +- if ((rc = slapi_back_get_info(be, BACK_INFO_INDEX_KEY, (void **)&bck_info))) { +- slapi_log_err(SLAPI_LOG_WARNING, "idl_new_range_fetch", "Total update: fail to retrieve suffix entryID, continue assuming it is the first entry\n"); +- } +- if (bck_info.key_found) { +- suffix = bck_info.id; +- } +- } +- + if (NEW_IDL_NOOP == *flag_err) { + return NULL; + } ++ if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) { ++ return idl_new_parentid_sorted_range_fetch(be, db, lowerkey, upperkey, txn, ai, ++ flag_err, allidslimit, sizelimit, ++ expire_time, lookthrough_limit, operator); ++ } + if (slapi_is_loglevel_set(SLAPI_LOG_FILTER)) { + char *included = ((operator & SLAPI_OP_RANGE) == SLAPI_OP_LESS) ? "not " : ""; + const char *sorted = (operator & SLAPI_OP_RANGE_NO_IDL_SORT) ? "not " : ""; +@@ -552,9 +779,6 @@ idl_new_range_fetch( + *flag_err = LDAP_TIMELIMIT_EXCEEDED; + goto error; + } +- if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) { +- key = (ID)strtol((char *)cur_key.data + 1, (char **)NULL, 10); +- } + while (DBI_RC_SUCCESS == dblayer_bulk_nextdata(&bulkdata, &dataret)) { + if (dataret.size != sizeof(ID)) { + slapi_log_err(SLAPI_LOG_ERR, "idl_new_range_fetch", "Database index is corrupt; " +@@ -570,38 +794,7 @@ idl_new_range_fetch( + } + /* note the last id read to check for dups */ + lastid = id; +- /* we got another ID, add it to our IDL */ +- if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) { +- if ((count == 0) && (suffix == 0)) { +- /* First time. Keep the suffix ID. +- * note that 'suffix==0' mean we did not retrieve the suffix entry id +- * from the parentid index (key '=0'), so let assume the first +- * found entry is the one from the suffix +- */ +- suffix = key; +- idl_append_extend(&idl, id); +- idrange_add_id(&idrange_list, id); +- } else if ((key == suffix) || idl_id_is_in_idlist_ranges(idl, idrange_list, key)) { +- /* the parent is the suffix or already in idl. */ +- idl_append_extend(&idl, id); +- idrange_add_id(&idrange_list, id); +- } else { +- /* Otherwise, keep the {key,id} in leftover array */ +- if (!leftover) { +- leftover = (idl_range_id_pair *)slapi_ch_calloc(leftoverlen, sizeof(idl_range_id_pair)); +- } else if (leftovercnt == leftoverlen) { +- leftover = (idl_range_id_pair *)slapi_ch_realloc((char *)leftover, 2 * leftoverlen * sizeof(idl_range_id_pair)); +- memset(leftover + leftovercnt, 0, leftoverlen); +- leftoverlen *= 2; +- } +- leftover[leftovercnt].key = key; +- leftover[leftovercnt].id = id; +- leftovercnt++; +- } +- } else { +- idl_append_extend(&idl, id); +- } +- ++ idl_append_extend(&idl, id); + count++; + } + +@@ -684,26 +877,9 @@ error: + *flag_err = ret; + + /* sort idl */ +- if (idl && !ALLIDS(idl) && !(operator&SLAPI_OP_RANGE_NO_IDL_SORT)) { ++ if (idl && !ALLIDS(idl)) { + qsort((void *)&idl->b_ids[0], idl->b_nids, (size_t)sizeof(ID), idl_sort_cmp); + } +- if (operator&SLAPI_OP_RANGE_NO_IDL_SORT) { +- size_t remaining = leftovercnt; +- +- while(remaining > 0) { +- for (size_t i = 0; i < leftovercnt; i++) { +- if (leftover[i].key > 0 && idl_id_is_in_idlist_ranges(idl, idrange_list, leftover[i].key) != 0) { +- /* if the leftover key has its parent in the idl */ +- idl_append_extend(&idl, leftover[i].id); +- idrange_add_id(&idrange_list, leftover[i].id); +- leftover[i].key = 0; +- remaining--; +- } +- } +- } +- } +- slapi_ch_free((void **)&leftover); +- idrange_free(&idrange_list); + slapi_log_err(SLAPI_LOG_FILTER, "idl_new_range_fetch", + "Found %d candidates; error code is: %d\n", + idl ? idl->b_nids : 0, *flag_err); +@@ -769,38 +945,7 @@ idl_range_add_id_cb(dbi_val_t *key, dbi_val_t *data, void *ctx) + "Detected duplicate id %d due to DB_MULTIPLE error - skipping\n", id); + return DBI_RC_SUCCESS; + } +- /* we got another ID, add it to our IDL */ +- if (rctx->operator & SLAPI_OP_RANGE_NO_IDL_SORT) { +- ID keyval = (ID)strtol((char *)key->data + 1, (char **)NULL, 10); +- if ((rctx->count == 0) && (rctx->suffix == 0)) { +- /* First time. Keep the suffix ID. +- * note that 'suffix==0' mean we did not retrieve the suffix entry id +- * from the parentid index (key '=0'), so let assume the first +- * found entry is the one from the suffix +- */ +- rctx->suffix = keyval; +- idl_append_extend(&rctx->idl, id); +- idrange_add_id(&rctx->idrange_list, id); +- } else if ((keyval == rctx->suffix) || idl_id_is_in_idlist_ranges(rctx->idl, rctx->idrange_list, keyval)) { +- /* the parent is the suffix or already in idl. */ +- idl_append_extend(&rctx->idl, id); +- idrange_add_id(&rctx->idrange_list, id); +- } else { +- /* Otherwise, keep the {keyval,id} in leftover array */ +- if (!rctx->leftover) { +- rctx->leftover = (idl_range_id_pair *)slapi_ch_calloc(rctx->leftoverlen, sizeof(idl_range_id_pair)); +- } else if (rctx->leftovercnt == rctx->leftoverlen) { +- rctx->leftover = (idl_range_id_pair *)slapi_ch_realloc((char *)rctx->leftover, 2 * rctx->leftoverlen * sizeof(idl_range_id_pair)); +- memset(rctx->leftover + rctx->leftovercnt, 0, rctx->leftoverlen * sizeof(idl_range_id_pair)); +- rctx->leftoverlen *= 2; +- } +- rctx->leftover[rctx->leftovercnt].key = keyval; +- rctx->leftover[rctx->leftovercnt].id = id; +- rctx->leftovercnt++; +- } +- } else { +- idl_append_extend(&rctx->idl, id); +- } ++ idl_append_extend(&rctx->idl, id); + #if defined(DB_ALLIDS_ON_READ) + /* enforce the allids read limit */ + if ((NEW_IDL_NO_ALLID != rctx->flag_err) && rctx->ai && (rctx->idl != NULL) && +@@ -845,6 +990,11 @@ idl_lmdb_range_fetch( + if ((NULL == flag_err) || (NEW_IDL_NOOP == *flag_err)) { + return NULL; + } ++ if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) { ++ return idl_new_parentid_sorted_range_fetch(be, db, lowerkey, upperkey, txn, ai, ++ flag_err, allidslimit, sizelimit, ++ expire_time, lookthrough_limit, operator); ++ } + if (slapi_is_loglevel_set(SLAPI_LOG_FILTER)) { + char *included = ((operator & SLAPI_OP_RANGE) == SLAPI_OP_LESS) ? "not " : ""; + const char *sorted = (operator & SLAPI_OP_RANGE_NO_IDL_SORT) ? "not " : ""; +@@ -877,32 +1027,11 @@ idl_lmdb_range_fetch( + idl_range_ctx.expire_time = expire_time; + idl_range_ctx.lookthrough_limit = lookthrough_limit; + idl_range_ctx.operator = operator; +- idl_range_ctx.leftover = NULL; +- idl_range_ctx.leftoverlen = 32; +- idl_range_ctx.leftovercnt = 0; + idl_range_ctx.idl = idl_alloc(IDLIST_MIN_BLOCK_SIZE); + idl_range_ctx.flag_err = 0; + idl_range_ctx.lastid = 0; + idl_range_ctx.count = 0; + idl_range_ctx.index_id = index_id; +- idl_range_ctx.idrange_list = NULL; +- if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) { +- struct _back_info_index_key bck_info; +- /* We are doing a bulk import +- * try to retrieve the suffix entry id from the index +- */ +- +- bck_info.index = SLAPI_ATTR_PARENTID; +- bck_info.key = "0"; +- +- if ((ret = slapi_back_get_info(be, BACK_INFO_INDEX_KEY, (void **)&bck_info))) { +- slapi_log_err(SLAPI_LOG_WARNING, "idl_lmdb_range_fetch", +- "Total update: fail to retrieve suffix entryID, continue assuming it is the first entry\n"); +- } +- if (bck_info.key_found) { +- idl_range_ctx.suffix = bck_info.id; +- } +- } + + /* + * Iterate +@@ -952,27 +1081,9 @@ error: + } + + /* sort idl */ +- if (!ALLIDS(idl_range_ctx.idl) && !(operator&SLAPI_OP_RANGE_NO_IDL_SORT)) { ++ if (!ALLIDS(idl_range_ctx.idl)) { + qsort((void *)&idl_range_ctx.idl->b_ids[0], idl_range_ctx.idl->b_nids, sizeof(ID), idl_sort_cmp); + } +- if (operator&SLAPI_OP_RANGE_NO_IDL_SORT) { +- size_t remaining = idl_range_ctx.leftovercnt; +- +- while(remaining > 0) { +- for (size_t i = 0; i < idl_range_ctx.leftovercnt; i++) { +- if (idl_range_ctx.leftover[i].key > 0 && +- idl_id_is_in_idlist_ranges(idl_range_ctx.idl, idl_range_ctx.idrange_list, idl_range_ctx.leftover[i].key) != 0) { +- /* if the leftover key has its parent in the idl */ +- idl_append_extend(&idl_range_ctx.idl, idl_range_ctx.leftover[i].id); +- idrange_add_id(&idl_range_ctx.idrange_list, idl_range_ctx.leftover[i].id); +- idl_range_ctx.leftover[i].key = 0; +- remaining--; +- } +- } +- } +- } +- slapi_ch_free((void **)&idl_range_ctx.leftover); +- idrange_free(&idl_range_ctx.idrange_list); + *flag_err = idl_range_ctx.flag_err; + slapi_log_err(SLAPI_LOG_FILTER, "idl_lmdb_range_fetch", + "Found %d candidates; error code is: %d\n", +-- +2.54.0 + diff --git a/0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch b/0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch new file mode 100644 index 0000000..b38fba2 --- /dev/null +++ b/0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch @@ -0,0 +1,517 @@ +From fb6bd04c04ab92fe51c3206f9c2ef0433ea27d87 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 23 Jan 2026 17:35:45 -0800 +Subject: [PATCH 11/14] Issue 7198 - Web console doesn't show sub-suffix when + parent-suffix points to an entry (#7202) + +Description: The web console doesn't show sub-suffixes when the +nsslapd-parent-suffix attribute points to an entry rather than a backend +suffix. +For example, creating a sub-suffix ou=foo,ou=people,dc=example,dc=com +with parent-suffix ou=people,dc=example,dc=com (where ou=people is just an +entry, not a suffix) would not appear in the web console tree. + +Fix: In backend_build_tree() and get_sub_suffixes(), the code only matched +when nsslapd-parent-suffix exactly equaled an existing backend suffix. +Now it also checks if the parent-suffix is an entry under the current +suffix (ends with ,suffix) and is not itself a backend suffix. This +correctly attaches sub-suffixes to their containing suffix when the +parent-suffix points to an intermediate entry. + +Fixes: https://github.com/389ds/389-ds-base/issues/7198 + +Reviewed by: @progier389 (Thanks!) +--- + .../suites/lib389/subsuffix_tree_test.py | 313 ++++++++++++++++++ + src/lib389/lib389/backend.py | 47 ++- + src/lib389/lib389/cli_conf/backend.py | 34 +- + 3 files changed, 370 insertions(+), 24 deletions(-) + create mode 100644 dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py + +diff --git a/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py b/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py +new file mode 100644 +index 000000000..fa10ba530 +--- /dev/null ++++ b/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py +@@ -0,0 +1,313 @@ ++# --- 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 pytest ++from lib389.topologies import topology_st as topo ++from lib389.backend import Backends ++from lib389.idm.organizationalunit import OrganizationalUnits ++from lib389._constants import DEFAULT_SUFFIX ++ ++pytestmark = pytest.mark.tier1 ++ ++logging.getLogger(__name__).setLevel(logging.INFO) ++log = logging.getLogger(__name__) ++ ++ ++@pytest.fixture(scope="function") ++def setup_subsuffix_with_entry_parent(topo, request): ++ """Setup a sub-suffix whose parent-suffix points to an entry, not a suffix.""" ++ inst = topo.standalone ++ ++ # Create ou=people entry under the root suffix ++ log.info("Creating ou=people,dc=example,dc=com entry") ++ ous = OrganizationalUnits(inst, DEFAULT_SUFFIX) ++ ou_people = ous.get('people') ++ ++ # Create sub-suffix with parent-suffix pointing to the entry ++ log.info("Creating sub-suffix ou=foo,ou=people,dc=example,dc=com") ++ backends = Backends(inst) ++ subsuffix_dn = 'ou=foo,ou=people,dc=example,dc=com' ++ parent_suffix_dn = 'ou=people,dc=example,dc=com' ++ ++ foo_backend = backends.create(properties={ ++ 'cn': 'foo', ++ 'nsslapd-suffix': subsuffix_dn, ++ 'parent': parent_suffix_dn, ++ }) ++ ++ # Create the suffix entry ++ foo_ous = OrganizationalUnits(inst, parent_suffix_dn) ++ foo_ou = foo_ous.create(properties={'ou': 'foo'}) ++ ++ def cleanup(): ++ log.info("Cleaning up test backends and entries") ++ try: ++ foo_ou.delete() ++ except Exception as e: ++ log.warning(f"Failed to delete foo_ou: {e}") ++ try: ++ foo_backend.delete() ++ except Exception as e: ++ log.warning(f"Failed to delete foo_backend: {e}") ++ ++ request.addfinalizer(cleanup) ++ ++ return { ++ 'instance': inst, ++ 'backends': backends, ++ 'foo_backend': foo_backend, ++ 'ou_people': ou_people, ++ 'subsuffix_dn': subsuffix_dn, ++ 'parent_suffix_dn': parent_suffix_dn, ++ } ++ ++ ++def test_subsuffix_with_entry_parent_in_tree(topo, setup_subsuffix_with_entry_parent): ++ """Test that a sub-suffix with parent pointing to an entry is visible in the tree. ++ ++ :id: 256f36f5-76ad-4043-ad8d-1f9e2afc4e1d ++ :setup: Standalone instance with sub-suffix whose parent is an entry ++ :steps: ++ 1. Verify the sub-suffix backend exists ++ 2. Get sub-suffixes of the root backend ++ 3. Verify the sub-suffix appears in the list ++ :expectedresults: ++ 1. Backend should exist ++ 2. Sub-suffixes should be retrievable ++ 3. Sub-suffix should be visible (this is where the bug manifested) ++ """ ++ backends = setup_subsuffix_with_entry_parent['backends'] ++ foo_backend = setup_subsuffix_with_entry_parent['foo_backend'] ++ subsuffix_dn = setup_subsuffix_with_entry_parent['subsuffix_dn'] ++ ++ # Step 1: Verify the sub-suffix backend exists ++ assert foo_backend.exists(), "The foo backend should exist" ++ ++ # Step 2: Get sub-suffixes of the root backend ++ root_backend = backends.get(DEFAULT_SUFFIX) ++ sub_suffixes = root_backend.get_sub_suffixes() ++ log.info(f"Sub-suffixes found: {[s.get_attr_val_utf8('nsslapd-suffix') for s in sub_suffixes]}") ++ ++ # Step 3: Verify sub-suffix is in the list ++ sub_suffix_found = any( ++ s.get_attr_val_utf8_l('nsslapd-suffix') == subsuffix_dn.lower() ++ for s in sub_suffixes ++ ) ++ ++ assert sub_suffix_found, ( ++ f"Sub-suffix {subsuffix_dn} should be visible in get_sub_suffixes(). " ++ "The parent-suffix points to an entry, not a backend suffix." ++ ) ++ ++ ++def test_subsuffix_in_backend_list(topo, setup_subsuffix_with_entry_parent): ++ """Test that the sub-suffix appears in the backend list. ++ ++ :id: 0ccc49af-91bb-4e8f-b0e1-1bd0b75c041b ++ :setup: Standalone instance with sub-suffix configuration ++ :steps: ++ 1. Get all backends ++ 2. Verify both root suffix and sub-suffix are present ++ :expectedresults: ++ 1. Should retrieve all backends ++ 2. Both suffixes should be listed ++ """ ++ backends = setup_subsuffix_with_entry_parent['backends'] ++ subsuffix_dn = setup_subsuffix_with_entry_parent['subsuffix_dn'] ++ ++ be_list = backends.list() ++ suffixes = [be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_list] ++ ++ assert DEFAULT_SUFFIX.lower() in suffixes, \ ++ f"Root suffix {DEFAULT_SUFFIX} should be in the list" ++ assert subsuffix_dn.lower() in suffixes, \ ++ f"Sub-suffix {subsuffix_dn} should be in the list" ++ ++ ++def test_subsuffix_dn_boundary_matching(): ++ """Test that suffix matching respects DN component boundaries. ++ ++ :id: 0b856e26-c394-4c36-b9ba-d7894aa2ed11 ++ :setup: None (unit test) ++ :steps: ++ 1. Test exact suffix match ++ 2. Test proper DN ancestor match (ends with ,suffix) ++ 3. Test that partial string matches are rejected ++ :expectedresults: ++ 1. Exact match should return True ++ 2. Proper ancestor should return True ++ 3. Partial string match should return False ++ """ ++ from lib389.backend import is_subsuffix_of ++ ++ all_suffixes = {'dc=com', 'dc=example,dc=com', 'ou=dept,dc=example,dc=com'} ++ ++ # Test 1: Exact match ++ assert is_subsuffix_of('dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \ ++ "Exact match should return True" ++ ++ # Test 2: Parent is an entry under the suffix (not itself a suffix) ++ assert is_subsuffix_of('ou=people,dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \ ++ "Parent entry under suffix should return True" ++ ++ # Test 3: Parent IS a suffix - should return False (handled separately) ++ assert not is_subsuffix_of('ou=dept,dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \ ++ "Parent that is itself a suffix should return False" ++ ++ # Test 4: Edge case - wrong DN boundary (string ends with suffix but wrong boundary) ++ edge_suffixes = {'dc=com', 'st,dc=com'} ++ assert is_subsuffix_of('dc=test,dc=com', 'dc=com', edge_suffixes), \ ++ "dc=test,dc=com should match dc=com" ++ assert not is_subsuffix_of('dc=test,dc=com', 'st,dc=com', edge_suffixes), \ ++ "dc=test,dc=com should NOT match st,dc=com (wrong DN boundary)" ++ ++ # Test 5: None input ++ assert not is_subsuffix_of(None, 'dc=com', all_suffixes), \ ++ "None parent should return False" ++ ++ # Test 6: Closest ancestor - should only match the nearest suffix ++ # Hierarchy: dc=com -> dc=example,dc=com -> ou=branch,dc=example,dc=com (suffix) ++ # -> ou=dept,ou=branch,dc=example,dc=com (entry) -> subsuffix ++ # The subsuffix should only appear under ou=branch, not under dc=example,dc=com ++ nested_suffixes = {'dc=com', 'dc=example,dc=com', 'ou=branch,dc=example,dc=com'} ++ entry_parent = 'ou=dept,ou=branch,dc=example,dc=com' ++ # Should match ou=branch (closest) ++ assert is_subsuffix_of(entry_parent, 'ou=branch,dc=example,dc=com', nested_suffixes), \ ++ "Should match closest ancestor suffix (ou=branch)" ++ # Should NOT match dc=example,dc=com (not closest) ++ assert not is_subsuffix_of(entry_parent, 'dc=example,dc=com', nested_suffixes), \ ++ "Should NOT match distant ancestor (dc=example) - ou=branch is closer" ++ # Should NOT match dc=com (not closest) ++ assert not is_subsuffix_of(entry_parent, 'dc=com', nested_suffixes), \ ++ "Should NOT match distant ancestor (dc=com) - ou=branch is closer" ++ ++ log.info("All DN boundary edge cases passed") ++ ++ ++def test_deep_suffix_hierarchy(topo, request): ++ """Test complex hierarchy: suffix -> suffix -> entry -> suffix -> suffix. ++ ++ :id: fd06491a-defa-4780-8472-78c077febdfb ++ :setup: Standalone instance ++ :steps: ++ 1. Create sub-suffix ou=branch (parent=dc=example,dc=com - a suffix) ++ 2. Create entry ou=dept,ou=branch (not a suffix) ++ 3. Create sub-suffix ou=team,ou=dept,ou=branch (parent=ou=dept - an entry) ++ 4. Create sub-suffix ou=sub,ou=team,ou=dept,ou=branch (parent=ou=team - a suffix) ++ 5. Verify all sub-suffixes are correctly placed in the tree ++ :expectedresults: ++ 1. Sub-suffix created successfully ++ 2. Entry created successfully ++ 3. Sub-suffix with entry parent created successfully ++ 4. Sub-suffix with suffix parent created successfully ++ 5. Tree hierarchy is correct ++ """ ++ inst = topo.standalone ++ backends = Backends(inst) ++ ++ # Define the hierarchy ++ branch_suffix = f'ou=branch,{DEFAULT_SUFFIX}' ++ dept_entry = f'ou=dept,{branch_suffix}' # This is an ENTRY, not a suffix ++ team_suffix = f'ou=team,{dept_entry}' ++ sub_suffix = f'ou=sub,{team_suffix}' ++ ++ created_backends = [] ++ created_entries = [] ++ ++ def cleanup(): ++ log.info("Cleaning up deep hierarchy test") ++ for entry in reversed(created_entries): ++ try: ++ entry.delete() ++ except Exception as e: ++ log.warning(f"Failed to delete entry: {e}") ++ for be in reversed(created_backends): ++ try: ++ be.delete() ++ except Exception as e: ++ log.warning(f"Failed to delete backend: {e}") ++ ++ request.addfinalizer(cleanup) ++ ++ # Step 1: Create ou=branch sub-suffix (parent is root suffix) ++ log.info(f"Creating sub-suffix {branch_suffix}") ++ branch_be = backends.create(properties={ ++ 'cn': 'branch', ++ 'nsslapd-suffix': branch_suffix, ++ 'parent': DEFAULT_SUFFIX, ++ }) ++ created_backends.append(branch_be) ++ branch_ous = OrganizationalUnits(inst, DEFAULT_SUFFIX) ++ branch_ou = branch_ous.create(properties={'ou': 'branch'}) ++ created_entries.append(branch_ou) ++ ++ # Step 2: Create ou=dept entry under branch (NOT a suffix) ++ log.info(f"Creating entry {dept_entry}") ++ dept_ous = OrganizationalUnits(inst, branch_suffix) ++ dept_ou = dept_ous.create(properties={'ou': 'dept'}) ++ created_entries.append(dept_ou) ++ ++ # Step 3: Create ou=team sub-suffix (parent is dept ENTRY, not a suffix) ++ log.info(f"Creating sub-suffix {team_suffix} with entry parent {dept_entry}") ++ team_be = backends.create(properties={ ++ 'cn': 'team', ++ 'nsslapd-suffix': team_suffix, ++ 'parent': dept_entry, # Parent is an ENTRY! ++ }) ++ created_backends.append(team_be) ++ team_ous = OrganizationalUnits(inst, dept_entry) ++ team_ou = team_ous.create(properties={'ou': 'team'}) ++ created_entries.append(team_ou) ++ ++ # Step 4: Create ou=sub sub-suffix (parent is team suffix) ++ log.info(f"Creating sub-suffix {sub_suffix} with suffix parent {team_suffix}") ++ sub_be = backends.create(properties={ ++ 'cn': 'sub', ++ 'nsslapd-suffix': sub_suffix, ++ 'parent': team_suffix, # Parent is a SUFFIX ++ }) ++ created_backends.append(sub_be) ++ sub_ous = OrganizationalUnits(inst, team_suffix) ++ sub_ou = sub_ous.create(properties={'ou': 'sub'}) ++ created_entries.append(sub_ou) ++ ++ # Step 5: Verify the tree hierarchy ++ log.info("Verifying tree hierarchy...") ++ ++ # Root should have branch as sub-suffix ++ root_be = backends.get(DEFAULT_SUFFIX) ++ root_subs = root_be.get_sub_suffixes() ++ root_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in root_subs] ++ log.info(f"Root sub-suffixes: {root_sub_suffixes}") ++ assert branch_suffix.lower() in root_sub_suffixes, \ ++ f"branch should be under root suffix" ++ ++ # Branch should have team as sub-suffix (even though team's parent is an entry) ++ branch_be_obj = backends.get(branch_suffix) ++ branch_subs = branch_be_obj.get_sub_suffixes() ++ branch_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in branch_subs] ++ log.info(f"Branch sub-suffixes: {branch_sub_suffixes}") ++ assert team_suffix.lower() in branch_sub_suffixes, \ ++ f"team should be under branch suffix (parent is entry under branch)" ++ ++ # Team should have sub as sub-suffix ++ team_be_obj = backends.get(team_suffix) ++ team_subs = team_be_obj.get_sub_suffixes() ++ team_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in team_subs] ++ log.info(f"Team sub-suffixes: {team_sub_suffixes}") ++ assert sub_suffix.lower() in team_sub_suffixes, \ ++ f"sub should be under team suffix" ++ ++ log.info("Deep hierarchy test passed!") ++ ++ ++if __name__ == '__main__': ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main(["-s", CURRENT_FILE]) +diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py +index 42173eb06..7588e64e9 100644 +--- a/src/lib389/lib389/backend.py ++++ b/src/lib389/lib389/backend.py +@@ -38,6 +38,36 @@ from lib389.lint import DSBLE0001, DSBLE0002, DSBLE0003, DSBLE0004, DSBLE0005, D + from lib389.plugins import USNPlugin + + ++def is_subsuffix_of(sub_parent, be_suffix, all_suffixes): ++ """Check if sub_parent indicates this is a sub-suffix of be_suffix. ++ ++ Returns True only if be_suffix is the CLOSEST ancestor suffix of sub_parent. ++ This prevents a sub-suffix from appearing under multiple ancestors. ++ ++ :param sub_parent: The nsslapd-parent-suffix value (lowercase) ++ :param be_suffix: The suffix to check against (lowercase) ++ :param all_suffixes: Set of all backend suffixes (lowercase) ++ :returns: True if be_suffix is the closest ancestor suffix ++ """ ++ if not sub_parent: ++ return False ++ if sub_parent == be_suffix: ++ return True ++ if sub_parent in all_suffixes: ++ # sub_parent is itself a suffix, will be handled separately ++ return False ++ if not sub_parent.endswith(',' + be_suffix): ++ return False ++ # Find the closest (longest) matching suffix for this parent ++ best_match = None ++ for sfx in all_suffixes: ++ if sub_parent == sfx or sub_parent.endswith(',' + sfx): ++ if best_match is None or len(sfx) > len(best_match): ++ best_match = sfx ++ # Only return True if be_suffix is the closest match ++ return best_match == be_suffix ++ ++ + class BackendLegacy(object): + proxied_methods = 'search_s getEntry'.split() + +@@ -1111,22 +1141,27 @@ class Backend(DSLdapObject): + vlv.create(rdn="cn=" + vlvname, properties=props, basedn=basedn) + + def get_sub_suffixes(self): +- """Return a list of Backend's +- returns: a List of subsuffix entries ++ """Return a list of Backend's that are sub-suffixes of this backend. ++ :returns: A list of Backend instances that are sub-suffixes + """ + subsuffixes = [] + top_be_suffix = self.get_attr_val_utf8_l('nsslapd-suffix') ++ if not top_be_suffix: ++ return subsuffixes ++ + mts = self._mts.list() ++ be_insts = Backends(self._instance).list() ++ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts} ++ + for mt in mts: + parent_suffix = mt.get_attr_val_utf8_l('nsslapd-parent-suffix') + if parent_suffix is None: + continue +- if parent_suffix == top_be_suffix: ++ ++ if is_subsuffix_of(parent_suffix, top_be_suffix, all_suffixes): + child_suffix = mt.get_attr_val_utf8_l('cn') +- be_insts = Backends(self._instance).list() + for be in be_insts: +- be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix') +- if child_suffix == be_suffix: ++ if child_suffix == be.get_attr_val_utf8_l('nsslapd-suffix'): + subsuffixes.append(be) + break + return subsuffixes +diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py +index 80008d22d..0a9ce8f5a 100644 +--- a/src/lib389/lib389/cli_conf/backend.py ++++ b/src/lib389/lib389/cli_conf/backend.py +@@ -7,7 +7,7 @@ + # See LICENSE for details. + # --- END COPYRIGHT BLOCK --- + +-from lib389.backend import Backend, Backends, DatabaseConfig, BackendSuffixView ++from lib389.backend import Backend, Backends, DatabaseConfig, BackendSuffixView, is_subsuffix_of + from lib389.configurations.sample import ( + create_base_domain, + create_base_org, +@@ -337,6 +337,7 @@ def is_db_replicated(inst, suffix): + def backend_get_subsuffixes(inst, basedn, log, args): + subsuffixes = [] + be_insts = MANY(inst).list() ++ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts} + for be in be_insts: + be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix') + if be_suffix == args.be_name.lower(): +@@ -346,7 +347,7 @@ def backend_get_subsuffixes(inst, basedn, log, args): + db_type = "suffix" + sub = mt.get_attr_val_utf8_l('nsslapd-parent-suffix') + sub_be = mt.get_attr_val_utf8_l('nsslapd-backend') +- if sub == be_suffix: ++ if is_subsuffix_of(sub, be_suffix, all_suffixes): + # We have a subsuffix (maybe a db link?) + if is_db_link(inst, sub_be): + db_type = "link" +@@ -398,38 +399,34 @@ def build_node(suffix, be_name, subsuf=False, link=False, replicated=False): + } + + +-def backend_build_tree(inst, be_insts, nodes): +- """Recursively build the tree +- """ +- if len(nodes) == 0: +- # Done ++def backend_build_tree(inst, be_insts, nodes, all_suffixes): ++ """Recursively build the tree.""" ++ if not nodes: + return + + for node in nodes: +- node_suffix = node['id'] ++ node_suffix = node['id'].lower() + # Get sub suffixes and chaining of node + for be in be_insts: + be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix') +- if be_suffix == node_suffix.lower(): ++ if be_suffix == node_suffix: + # We have our parent, now find the children + mts = be._mts.list() +- + for mt in mts: + sub_parent = mt.get_attr_val_utf8_l('nsslapd-parent-suffix') + sub_be = mt.get_attr_val_utf8_l('nsslapd-backend') + sub_suffix = mt.get_attr_val_utf8_l('cn') +- if sub_parent == be_suffix: ++ if is_subsuffix_of(sub_parent, be_suffix, all_suffixes): + # We have a subsuffix (maybe a db link?) + link = is_db_link(inst, sub_be) + replicated = is_db_replicated(inst, sub_suffix) + node['children'].append(build_node(sub_suffix, +- sub_be, +- subsuf=True, +- link=link, +- replicated=replicated)) +- ++ sub_be, ++ subsuf=True, ++ link=link, ++ replicated=replicated)) + # Recurse over the new subsuffixes +- backend_build_tree(inst, be_insts, node['children']) ++ backend_build_tree(inst, be_insts, node['children'], all_suffixes) + break + + +@@ -470,7 +467,8 @@ def backend_get_tree(inst, basedn, log, args): + else: + # Build the tree + be_insts = Backends(inst).list() +- backend_build_tree(inst, be_insts, nodes) ++ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts} ++ backend_build_tree(inst, be_insts, nodes, all_suffixes) + + # Done + if args.json: +-- +2.54.0 + diff --git a/0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch b/0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch new file mode 100644 index 0000000..3c4bebc --- /dev/null +++ b/0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch @@ -0,0 +1,164 @@ +From f5199821675e3234335c5c28d31b0e9705685fd9 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Tue, 23 Jun 2026 10:07:00 +0100 +Subject: [PATCH 12/14] Issue 7593 - Reject invalid SASL packet length values + in sasl_io_start_packet (#7594) + +Description: +While processing SASL encrypted traffic, sasl_io_start_packet() reads a +4-byte length from the connection and adds sizeof(uint32_t) before resizing +the read buffer. Certain large length values can wrap in uint32_t, causing +incorrect buffer sizing when malformed SASL data is received on an +established connection. + +Fixes: https://github.com/389ds/389-ds-base/issues/7593 + +Reviewed by: @tbordaz, @progier389 (Thank you) +--- + .../suites/sasl/sasl_io_overflow_test.py | 106 ++++++++++++++++++ + ldap/servers/slapd/sasl_io.c | 9 ++ + 2 files changed, 115 insertions(+) + create mode 100644 dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py + +diff --git a/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py b/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py +new file mode 100644 +index 000000000..ac1f3760f +--- /dev/null ++++ b/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py +@@ -0,0 +1,106 @@ ++# --- 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 socket ++import struct ++import time ++import ldap ++import pytest ++ ++from lib389._constants import DEFAULT_SUFFIX ++from lib389.idm.user import UserAccounts ++from lib389.saslmap import SaslMappings ++from lib389.utils import * ++from lib389.topologies import topology_st ++ ++pytestmark = pytest.mark.tier1 ++ ++log = logging.getLogger(__name__) ++ ++SASL_OVERFLOW_FAKE_LENGTH = 0xFFFFFFFC ++SASL_OVERFLOW_PAYLOAD_SIZE = 65536 ++ ++def test_sasl_io_packet_length_overflow(topology_st): ++ """Malformed SASL length prefix must not crash the server ++ :id: 318f871d-2f17-461b-98ed-04cdff6ab41a ++ :setup: Standalone instance ++ :steps: ++ 1. Set passwordStorageScheme to CLEAR and restart the instance ++ 2. Add SASL uid mapping and user sasltest for DIGEST-MD5 bind ++ 3. SASL DIGEST-MD5 bind as sasltest ++ 4. Send malformed SASL packeton the encrypted connection ++ 5. Verify server is still running ++ :expectedresults: ++ 1. CLEAR scheme and SASL map/user are configured successfully ++ 2. Test user added ++ 3. DIGEST-MD5 bind succeeds ++ 4. Malformed packet is accepted on the wire without crashing the server ++ 5. Server remains up ++ """ ++ inst = topology_st.standalone ++ inst.config.replace('passwordStorageScheme', 'CLEAR') ++ saslmappings = SaslMappings(inst) ++ ++ # Create SASL mapping ++ try: ++ saslmappings.create(properties={ ++ 'cn': 'uid map', ++ 'nsSaslMapRegexString': r'\(.*\)', ++ 'nsSaslMapBaseDNTemplate': DEFAULT_SUFFIX, ++ 'nsSaslMapFilterTemplate': '(uid=\\1)', ++ 'nsSaslMapPriority': '10', ++ }) ++ except ldap.ALREADY_EXISTS: ++ pass ++ ++ # Create test user ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ try: ++ users.create(properties={ ++ 'uid': 'sasltest', ++ 'cn': 'SASL Test User', ++ 'sn': 'Test', ++ 'uidNumber': '10001', ++ 'gidNumber': '10001', ++ 'homeDirectory': '/home/sasltest', ++ 'userPassword': 'sasltest123', ++ }) ++ except ldap.ALREADY_EXISTS: ++ pass ++ inst.restart() ++ ++ try: ++ # Open connection to server and send bad payload ++ conn = ldap.initialize(inst.get_ldap_uri()) ++ conn.protocol_version = ldap.VERSION3 ++ conn.set_option(ldap.OPT_X_SASL_SSF_MIN, 1) ++ conn.set_option(ldap.OPT_X_SASL_SSF_MAX, 256) ++ conn.sasl_interactive_bind_s( ++ '', ++ ldap.sasl.digest_md5('sasltest', 'sasltest123'), ++ ) ++ fd = conn.fileno() ++ sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) ++ payload = ( ++ struct.pack('!I', SASL_OVERFLOW_FAKE_LENGTH) ++ + b'A' * 3 ++ + b'B' * SASL_OVERFLOW_PAYLOAD_SIZE ++ ) ++ sock.send(payload) ++ sock.detach() ++ time.sleep(3) ++ ++ # Check if the server is still up ++ try: ++ inst.rootdse.get_attr_val_utf8('vendorVersion') ++ except ldap.SERVER_DOWN: ++ pytest.fail("Server is not responding after malformed SASL packet") ++ finally: ++ if not inst.status(): ++ inst.start() +diff --git a/ldap/servers/slapd/sasl_io.c b/ldap/servers/slapd/sasl_io.c +index d10ab8d2e..405c9186f 100644 +--- a/ldap/servers/slapd/sasl_io.c ++++ b/ldap/servers/slapd/sasl_io.c +@@ -16,6 +16,7 @@ + #include "fe.h" + #include + #include ++#include + + /* + * I/O Shim Layer for SASL Encryption +@@ -371,6 +372,14 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt + /* Decode the length */ + packet_length = ntohl(*(uint32_t *)sp->encrypted_buffer); + /* add length itself (for Cyrus SASL library) */ ++ if (packet_length > (UINT32_MAX - sizeof(uint32_t))) { ++ slapi_log_err(SLAPI_LOG_ERR, "sasl_io_start_packet", ++ "SASL packet length would overflow (%" PRIu32 ")\n", ++ packet_length); ++ PR_SetError(PR_BUFFER_OVERFLOW_ERROR, 0); ++ *err = PR_BUFFER_OVERFLOW_ERROR; ++ return -1; ++ } + packet_length += sizeof(uint32_t); + + slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet", +-- +2.54.0 + diff --git a/0013-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch b/0013-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch new file mode 100644 index 0000000..0ac3e4e --- /dev/null +++ b/0013-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch @@ -0,0 +1,32 @@ +From 0b8df3515772fbb06a422fd32239a56e244cba83 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Thu, 25 Jun 2026 10:45:21 +0100 +Subject: [PATCH 13/14] Issue 7593 - Fix testimony docstring for SASL overflow + test (#7606) + +Description: +The test added in #7594 failed testimony validation because the docstring +summary was not separated from the metadata fields with a blank line. + +Relates: https://github.com/389ds/389-ds-base/issues/7593 + +Reviewed by: @progier389 (Thank you) +--- + dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py b/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py +index ac1f3760f..43fc344df 100644 +--- a/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py ++++ b/dirsrvtests/tests/suites/sasl/sasl_io_overflow_test.py +@@ -28,6 +28,7 @@ SASL_OVERFLOW_PAYLOAD_SIZE = 65536 + + def test_sasl_io_packet_length_overflow(topology_st): + """Malformed SASL length prefix must not crash the server ++ + :id: 318f871d-2f17-461b-98ed-04cdff6ab41a + :setup: Standalone instance + :steps: +-- +2.54.0 + diff --git a/0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch b/0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch new file mode 100644 index 0000000..5c66cac --- /dev/null +++ b/0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch @@ -0,0 +1,244 @@ +From c4e1a72eb5500b94a7d20bbfc7c0d3fd2a599cb9 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 10 Jul 2026 18:17:40 -0700 +Subject: [PATCH 14/14] Issue 7558 - Total init sends the suffix entry twice + (#7640) + +Description: Exclude the suffix entry from the depth-first parentid +walk because total init sends it separately. +Preserve NEW_IDL_NO_ALLID across all parentid fetches and honor it in +the LMDB fetch path to maintain parent-first ordering. +Update LMDB reindex handling so entries with an explicit parentid are +treated as regular entries when their RDN matches the suffix. + +Add a regression test with a wide moved subtree that verifies entry order +and ensures the suffix is sent only once. + +Fixes: https://github.com/389ds/389-ds-base/issues/7558 +Fixes: https://github.com/389ds/389-ds-base/issues/7604 + +Reviewed by: progier389 (Thanks!) +--- + .../suites/replication/regression_m2_test.py | 105 +++++++++++++++++- + .../back-ldbm/db-mdb/mdb_import_threads.c | 10 ++ + .../slapd/back-ldbm/db-mdb/mdb_layer.c | 2 +- + ldap/servers/slapd/back-ldbm/idl_new.c | 25 +++-- + 4 files changed, 131 insertions(+), 11 deletions(-) + +diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py +index db5140b0b..11f3a1fc0 100644 +--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py ++++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py +@@ -19,6 +19,7 @@ import time + import random + import string + from shutil import rmtree ++from lib389.backend import DatabaseConfig + from lib389.dbgen import dbgen_users + from lib389.idm.user import TEST_USER_PROPERTIES, UserAccount, UserAccounts + from lib389.pwpolicy import PwPolicyManager +@@ -1321,8 +1322,108 @@ def test_get_with_normalized_rid_dict(): + assert nrd.get('099') is None + + +-@pytest.mark.ds49915 +-@pytest.mark.bz1626375 ++def test_online_init_no_duplicate_suffix(topo_m2, request): ++ """Total init must preserve tree order without sending the suffix twice ++ ++ :id: f0ccfb02-6c9d-48dd-9482-8a2c0763d485 ++ :setup: Two suppliers replication setup ++ :steps: ++ 1. Add a parent with more direct children than the ID list scan limit ++ 2. Move the subtree below a newer top-level ancestor ++ 3. Lower the ID list scan limit on supplier1 ++ 4. Perform online initialization from supplier1 to supplier2 ++ 5. Search supplier2 for entries with the suffix DN ++ 6. Compare the number of entries on both suppliers ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Exactly one entry has the suffix DN ++ 6. Both suppliers have the same number of entries ++ """ ++ m1 = topo_m2.ms["supplier1"] ++ m2 = topo_m2.ms["supplier2"] ++ ++ scan_limit = 100 ++ parent_rdn = 'ou=total-init-scanlimit' ++ ancestor_rdn = 'ou=total-init-ancestor' ++ original_parent_dn = f'{parent_rdn},{DEFAULT_SUFFIX}' ++ ancestor_dn = f'{ancestor_rdn},{DEFAULT_SUFFIX}' ++ moved_parent_dn = f'{parent_rdn},{ancestor_dn}' ++ db_config = DatabaseConfig(m1) ++ original_scan_limit = db_config.get_attr_vals_utf8('nsslapd-idlistscanlimit') ++ repl = ReplicationManager(DEFAULT_SUFFIX) ++ ++ def delete_test_subtrees(supplier): ++ deleted = False ++ for dn in (moved_parent_dn, original_parent_dn, ancestor_dn): ++ try: ++ supplier.delete_branch_s(dn, ldap.SCOPE_SUBTREE) ++ deleted = True ++ except ldap.NO_SUCH_OBJECT: ++ pass ++ return deleted ++ ++ def fin(): ++ for supplier in (m1, m2): ++ if not supplier.status(): ++ supplier.start() ++ db_config.set([('nsslapd-idlistscanlimit', original_scan_limit)]) ++ try: ++ if delete_test_subtrees(m1): ++ repl.wait_for_replication(m1, m2) ++ else: ++ delete_test_subtrees(m2) ++ except Exception as err: ++ log.warning("Replication cleanup failed, deleting supplier2 entries directly: %s", err) ++ delete_test_subtrees(m2) ++ ++ request.addfinalizer(fin) ++ ++ test_parent = OrganizationalUnits(m1, DEFAULT_SUFFIX).create( ++ properties={'ou': 'total-init-scanlimit'} ++ ) ++ # Force the parentid index lookup for this parent past the ALLIDS threshold. ++ test_children = OrganizationalUnits(m1, test_parent.dn) ++ for idx in range(scan_limit + 1): ++ last_child = test_children.create(properties={'ou': f'child{idx}'}) ++ ++ test_ancestor = OrganizationalUnits(m1, DEFAULT_SUFFIX).create( ++ properties={'ou': 'total-init-ancestor'} ++ ) ++ assert int(test_ancestor.get_attr_val_utf8('entryid')) > int( ++ last_child.get_attr_val_utf8('entryid') ++ ) ++ test_parent.rename(parent_rdn, newsuperior=test_ancestor.dn) ++ assert test_parent.dn.lower() == moved_parent_dn.lower() ++ assert repl.wait_for_replication(m1, m2) ++ ++ db_config.set([('nsslapd-idlistscanlimit', str(scan_limit))]) ++ assert db_config.get_attr_val_utf8('nsslapd-idlistscanlimit') == str(scan_limit) ++ ++ agmt = Agreements(m1).list()[0] ++ agmt.begin_reinit() ++ (done, error) = agmt.wait_reinit() ++ assert done is True ++ assert error is False ++ ++ # The consumer used to store the suffix entry twice: the supplier sent ++ # it explicitly and again as part of the bulk import candidate list ++ filter_all = '(|(objectclass=ldapsubentry)(objectclass=nstombstone)(nsuniqueid=*))' ++ m2entries = m2.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, filter_all, ++ escapehatch='i am sure') ++ suffix_entries = [e for e in m2entries if e.dn.lower() == DEFAULT_SUFFIX.lower()] ++ log.info("%d entries with the suffix DN found on supplier2", len(suffix_entries)) ++ assert len(suffix_entries) == 1 ++ ++ m1entries = m1.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, filter_all, ++ escapehatch='i am sure') ++ log.info("supplier1 has %d entries, supplier2 has %d entries", ++ len(m1entries), len(m2entries)) ++ assert len(m1entries) == len(m2entries) ++ ++ + def test_online_reinit_may_hang(topo_with_sigkill): + """Online reinitialization may hang when the first + entry of the DB is RUV entry instead of the suffix +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c +index c49412a70..a8027a28d 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c +@@ -856,6 +856,16 @@ dbmdb_import_entry_info_by_param(EntryInfoParam_t *param, WorkerQueueData_t *wqe + } + + dnrc = get_entry_type(wqelmt, ¶m->sdn); ++ if (dnrc == DNRC_BAD_SUFFIX_ID && (param->flags & EIP_RDN)) { ++ /* In reindex mode the sdn contains only the RDN. A non-root record ++ * with an explicit parentid is therefore a regular entry even when ++ * its RDN equals a one-RDN suffix. */ ++ char *pidstr = NULL; ++ if (get_value_from_string(wqelmt->data, "parentid", &pidstr) == 0) { ++ slapi_ch_free_string(&pidstr); ++ dnrc = DNRC_OK; ++ } ++ } + if (dnrc == DNRC_SUFFIX) { + if ( param->eid != 1) { + dnrc = DNRC_BAD_SUFFIX_ID; +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c +index 0b4a09026..6ddb1c43e 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c +@@ -2929,7 +2929,7 @@ dbmdb_idl_new_fetch(backend *be, dbi_db_t *db, dbi_val_t *inkey, dbi_txn_t *txn, + } + } + +- if (allidslimit && count >= allidslimit) { ++ if ((NEW_IDL_NO_ALLID != *flag_err) && allidslimit && count >= allidslimit) { + idl = idl_allids(be); + slapi_log_err(SLAPI_LOG_TRACE, "dbmdb_idl_new_fetch", "%s returns allids (attribute: %s)\n", + (char *)key.mv_data, index_id); +diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c +index 29457500e..0fa48ea5f 100644 +--- a/ldap/servers/slapd/back-ldbm/idl_new.c ++++ b/ldap/servers/slapd/back-ldbm/idl_new.c +@@ -468,14 +468,14 @@ idl_parentid_walk_push(ID **stack, size_t *stack_size, size_t *stack_top, ID id) + /* + * Walk the parentid index depth-first starting at root_id. + * For each parent, look up index key "=parent_id" (direct get, not cursor next), +- * append the parent to the IDList, then visit each child the same way. ++ * append each descendant (but not root_id) to the IDList, then visit its children. + */ + static int + idl_parentid_walk_tree(idl_parentid_walk_ctx_t *ctx, ID root_id, ID **stack, + size_t *stack_size, size_t *stack_top) + { + IDList *children = NULL; +- int fetch_err = NEW_IDL_NO_ALLID; ++ int fetch_err = 0; + size_t i; + + if (idl_parentid_walk_push(stack, stack_size, stack_top, root_id) != 0) { +@@ -489,13 +489,18 @@ idl_parentid_walk_tree(idl_parentid_walk_ctx_t *ctx, ID root_id, ID **stack, + return -1; + } + +- if (idl_append_extend(&ctx->idl, parent_id) != 0) { +- slapi_log_err(SLAPI_LOG_ERR, "idl_new_parentid_sorted_range_fetch", +- "Unable to extend id list for attribute (%s)\n", ctx->index_id); +- idl_free(&ctx->idl); +- return -1; ++ if (parent_id != root_id) { ++ /* The suffix entry itself is not a candidate: bulk import ++ * (total init) sends it separately before walking the tree, ++ * and the pre-walk implementation never returned it either. */ ++ if (idl_append_extend(&ctx->idl, parent_id) != 0) { ++ slapi_log_err(SLAPI_LOG_ERR, "idl_new_parentid_sorted_range_fetch", ++ "Unable to extend id list for attribute (%s)\n", ctx->index_id); ++ idl_free(&ctx->idl); ++ return -1; ++ } ++ ctx->count++; + } +- ctx->count++; + + #if defined(DB_ALLIDS_ON_READ) + if ((NEW_IDL_NO_ALLID != *(ctx->flag_err)) && ctx->ai && (ctx->idl != NULL) && +@@ -506,6 +511,10 @@ idl_parentid_walk_tree(idl_parentid_walk_ctx_t *ctx, ID root_id, ID **stack, + } + #endif + idl_parentid_set_index_key(ctx, parent_id); ++ /* idl_fetch_ext resets *err to 0 on success, so re-arm the ++ * no-allids hint for every fetch: collapsing one parent's ++ * children to ALLIDS would collapse the whole candidate list. */ ++ fetch_err = NEW_IDL_NO_ALLID; + children = idl_fetch_ext(ctx->be, ctx->db, &ctx->key, ctx->txn, ctx->ai, + &fetch_err, ctx->allidslimit); + if (fetch_err != 0 && fetch_err != DBI_RC_NOTFOUND) { +-- +2.54.0 + diff --git a/389-ds-base.spec b/389-ds-base.spec index 972ee8d..3bbe9da 100644 --- a/389-ds-base.spec +++ b/389-ds-base.spec @@ -47,7 +47,7 @@ ExcludeArch: i686 Summary: 389 Directory Server (base) Name: 389-ds-base Version: 2.9.0 -Release: 1%{?dist} +Release: 2%{?dist} License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) 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 MIT AND MPL-2.0 AND Zlib URL: https://www.port389.org Conflicts: selinux-policy-base < 3.9.8 @@ -294,6 +294,12 @@ Patch: 0005-Issue-7372-Reindex-adds-tombstones-to-ancestorid-cau.patc Patch: 0006-Issue-7327-dsctl-healthcheck-DSMOLE0001-inaccurate-r.patch Patch: 0007-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch Patch: 0008-Fix-test389-imports-on-older-branches.patch +Patch: 0009-Issue-7500-Prevent-unsigned-integer-underflow-during.patch +Patch: 0010-Issue-7558-During-online-import-the-IDL-should-be-cr.patch +Patch: 0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch +Patch: 0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch +Patch: 0013-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch +Patch: 0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch %description 389 Directory Server is an LDAPv3 compliant server. The base package includes @@ -743,6 +749,15 @@ exit 0 %endif %changelog +* Fri Jul 17 2026 Simon Pichugin - 2.9.0-2 +- Bump version to 2.9.0-2 +- Resolves: RHEL-88942 - LDAP healthcheck and ignoring entrydn index and associated config +- Resolves: RHEL-168964 - Web console doesn't show the sub suffix of ou=foo,ou=people,dc=example,dc=com. +- Resolves: RHEL-212097 - Online reinitialization is failing with the supplier being busy calling idrange_add_id() +- Resolves: RHEL-212816 - import_monitor_threads and average rate 214748364.8/sec rate calculation issue [rhel-9] +- Resolves: RHEL-190777 - CVE-2026-11788 389-ds-base: 389-ds-base: NULL pointer dereference in deref control plugin BER parser [rhel-9.9] +- Resolves: RHEL-183108 - CVE-2026-11774 389-ds-base: 389-ds-base: integer overflow in SASL packet length bypasses size limit leading to heap buffer overflow [rhel-9.9] + * Fri Jun 05 2026 Viktor Ashirov - 2.9.0-1 - Bump version to 2.9.0 - Resolves: RHEL-165978 - Replication halt caused by an incorrect setting of "nsslapd-changelogmaxage"