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]
This commit is contained in:
Simon Pichugin 2026-07-21 22:55:53 -07:00
parent c824da947d
commit a7590cdbcb
7 changed files with 1660 additions and 1 deletions

View File

@ -0,0 +1,89 @@
From 6ae6fdbf9c42f30a5653215ff271d700e6a1a0ce Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
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; i<ctx->workerq.max_slots; i++) {
+ for (size_t i = 0; i < max_slots; i++) {
entry_processed += slots[i].count;
}
--
2.54.0

View File

@ -0,0 +1,598 @@
From 6a5aa0434fb32467fccb746915aa1a9ac740e4a6 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
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

View File

@ -0,0 +1,517 @@
From fb6bd04c04ab92fe51c3206f9c2ef0433ea27d87 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
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

View File

@ -0,0 +1,164 @@
From f5199821675e3234335c5c28d31b0e9705685fd9 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
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 <sasl/sasl.h>
#include <arpa/inet.h>
+#include <stdint.h>
/*
* 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

View File

@ -0,0 +1,32 @@
From 0b8df3515772fbb06a422fd32239a56e244cba83 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
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

View File

@ -0,0 +1,244 @@
From c4e1a72eb5500b94a7d20bbfc7c0d3fd2a599cb9 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
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, &param->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

View File

@ -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 <spichugi@redhat.com> - 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 <vashirov@redhat.com> - 2.9.0-1
- Bump version to 2.9.0
- Resolves: RHEL-165978 - Replication halt caused by an incorrect setting of "nsslapd-changelogmaxage"