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:
parent
c824da947d
commit
a7590cdbcb
@ -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
|
||||
|
||||
598
0010-Issue-7558-During-online-import-the-IDL-should-be-cr.patch
Normal file
598
0010-Issue-7558-During-online-import-the-IDL-should-be-cr.patch
Normal 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
|
||||
|
||||
517
0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
Normal file
517
0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
Normal 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
|
||||
|
||||
164
0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Normal file
164
0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
244
0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch
Normal file
244
0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch
Normal 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, ¶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
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user