Bump version to 3.3.0-2

- Resolves: RHEL-146147 - Fix dsconf to allow specifying multiple certificates for use in 389-ds server
- Resolves: RHEL-182463 - Error: NssSsl.add_cert() got an unexpected keyword argument 'input_file'
- Resolves: RHEL-166737 - import_monitor_threads and average rate 214748364.8/sec rate calculation issue
- Resolves: RHEL-153372 - dirsrv@.service missing NoNewPrivileges and MemoryDenyWriteExecute despite broad capability set
- Resolves: RHEL-164610 - Online reinitialization is failing with the supplier being busy calling idrange_add_id()
- Resolves: RHEL-153094 - [RFE] Thread pool state mmap file with dsctl and cn=monitor exposure
- Resolves: RHEL-129204 - Local password policies can be created with unallowed values
- Resolves: RHEL-185139 - Compressed logs are prematurely deleted
- Resolves: RHEL-183383 - Post-import cache autotuning does not recompute entry cache size
- Resolves: RHEL-190780 - CVE-2026-11788 389-ds-base: 389-ds-base: NULL pointer dereference in defer control plugin BER parser [rhel-10.3]
- Resolves: RHEL-183109 - CVE-2026-11774 389-ds-base: 389-ds-base: integer overflow in SASL packet length bypasses size limit leading to heap buffer overflow [rhel-10.3]
This commit is contained in:
Simon Pichugin 2026-07-21 18:49:30 -07:00
parent 5a8e6532df
commit b1018138a1
12 changed files with 4567 additions and 0 deletions

View File

@ -0,0 +1,51 @@
From e92fa4b13066cc0107134346ea0af15981021c42 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 5 Jun 2026 18:59:25 +0200
Subject: [PATCH] Issue 7562 - Error: NssSsl.add_cert() got an unexpected
keyword argument 'input_file' (#7563)
Bug Description:
In #7281 `input_file` parameter was renamed to `cert_file`, but not all
callers were updated. This causes a TypeError at runtime.
Fix Description:
Update dscontainer and dsctl to use the new `cert_file` parameter name.
Relates: https://github.com/389ds/389-ds-base/issues/7281
Fixes: https://github.com/389ds/389-ds-base/issues/7562
Reviewed by: @ mreynolds389 (Thanks!)
---
src/lib389/cli/dscontainer | 2 +-
src/lib389/lib389/cli_ctl/tls.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/lib389/cli/dscontainer b/src/lib389/cli/dscontainer
index e206effaa..970cf1d14 100755
--- a/src/lib389/cli/dscontainer
+++ b/src/lib389/cli/dscontainer
@@ -149,7 +149,7 @@ def _begin_setup_pem_tls():
# Import the ca's
for ca_path in [os.path.join(CONTAINER_TLS_SERVER_CADIR, ca) for ca in cas]:
log.info("Enrolling -> %s" % ca_path)
- tls.add_cert(nickname=ca_path, input_file=ca_path, ca=True)
+ tls.add_cert(nickname=ca_path, cert_file=ca_path, ca=True)
tls.edit_cert_trust(ca_path, "C,,")
# Import the new server-cert
tls.add_server_key_and_cert(CONTAINER_TLS_SERVER_KEY, CONTAINER_TLS_SERVER_CERT)
diff --git a/src/lib389/lib389/cli_ctl/tls.py b/src/lib389/lib389/cli_ctl/tls.py
index f1caa3d86..0d917c62c 100644
--- a/src/lib389/lib389/cli_ctl/tls.py
+++ b/src/lib389/lib389/cli_ctl/tls.py
@@ -44,7 +44,7 @@ def import_client_ca(inst, log, args):
if nickname.lower() == CERT_NAME.lower() or nickname.lower() == CA_NAME.lower():
log.error("You may not import a CA with the nickname %s or %s" % (CERT_NAME, CA_NAME))
return
- tls.add_cert(nickname=nickname, input_file=cert_path)
+ tls.add_cert(nickname=nickname, cert_file=cert_path)
tls.edit_cert_trust(nickname, "T,,")
--
2.54.0

View File

@ -0,0 +1,89 @@
From 55377637f238346626e87c2633b79d1fd366f556 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 18 May 2026 14:27:04 -0400
Subject: [PATCH] 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 b72d9d10e..a965529ea 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 d79715fea..9a92e8ffc 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 f5fa6f7dc0adcdb48f0f17eb6d27b8fd2f575f20 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Wed, 10 Jun 2026 15:14:08 +0200
Subject: [PATCH] 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,164 @@
From c7eac4e19457fe80860ab3882e7aa63b454b241e Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Tue, 23 Jun 2026 10:07:00 +0100
Subject: [PATCH] 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 5c2093a5c..6c2cac084 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 61432231a97ff885f8987e61dd24eacdbff81da5 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Thu, 25 Jun 2026 10:45:21 +0100
Subject: [PATCH] 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,91 @@
From a4fc4059be9f93332ec15b1a1e0774a6d688d9f1 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <mirielka@users.noreply.github.com>
Date: Wed, 8 Jul 2026 14:39:38 +0200
Subject: [PATCH] Issue 7284 - Automated test for creating local password
policy with incorrect passwordInHistory value (#7608)
Description:
Adding automated test for creating local password policy with incorrect passwordInHistory value
Relates: #7284
Author: Lenka Doudova
Assisted by: Cursor
Reviewer: @progier389
---
.../suites/password/password_policy_test.py | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/dirsrvtests/tests/suites/password/password_policy_test.py b/dirsrvtests/tests/suites/password/password_policy_test.py
index a618803d0..36f25eba1 100644
--- a/dirsrvtests/tests/suites/password/password_policy_test.py
+++ b/dirsrvtests/tests/suites/password/password_policy_test.py
@@ -1563,6 +1563,66 @@ def test_additional_corner_cases(topo, policy_setup, _fixture_for_additional_cas
])
+@pytest.mark.parametrize('value,result',
+ [('0', ldap.SUCCESS),
+ ('24', ldap.SUCCESS),
+ pytest.param('-1', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284')),
+ pytest.param('30', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284')),
+ pytest.param('a', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284'))])
+def test_create_local_pwp_with_passwordInHistory(topo, value, result):
+ """Verify local password policy passwordInHistory accepts only values 0-24
+
+ :id: e7c4a1b2-3d5f-4a8e-9c1b-2f6e8d4a7b03
+ :parametrized: yes
+ :setup: Standalone instance
+ :steps:
+ 1. Enable nsslapd-pwpolicy-local
+ 2. Create a dedicated OU under the default suffix
+ 3. Create a subtree local password policy with passwordInHistory set to
+ value using PwPolicyManager.create_subtree_policy
+ 4. For successful creation, verify passwordInHistory on the policy entry
+ 5. Delete the local password policy and OU
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success for value 0 or 24; CONSTRAINT_VIOLATION for -1, 30, or a
+ (invalid cases marked xfail)
+ 4. passwordInHistory matches value when creation succeeds
+ 5. Success
+ """
+ inst = topo.standalone
+ inst.config.replace('nsslapd-pwpolicy-local', 'on')
+
+ ous = OrganizationalUnits(inst, DEFAULT_SUFFIX)
+ ou = ous.create(properties={'ou': f'pwpinhist{value}'})
+
+ pwp = PwPolicyManager(inst)
+
+ try:
+ if result == ldap.SUCCESS:
+ policy_entry = pwp.create_subtree_policy(ou.dn, {'passwordInHistory': value})
+ assert policy_entry.get_attr_val_utf8('passwordInHistory') == value
+ else:
+ with pytest.raises(result):
+ pwp.create_subtree_policy(ou.dn, {'passwordInHistory': value})
+ except ldap.LDAPError:
+ raise
+ finally:
+ try:
+ pwp.delete_local_policy(ou.dn)
+ except ValueError:
+ container = nsContainer(inst, f'cn=nsPwPolicyContainer,{ou.dn}')
+ try:
+ if container.exists():
+ container.delete()
+ except ldap.LDAPError:
+ pass
+ try:
+ ou.delete()
+ except ldap.LDAPError:
+ pass
+
+
def test_get_pwpolicy_cn_with_quotes(topology_m1, policy_qoutes_setup):
"""Test that that we can get pwpolicy when
cn attr includes quotes
--
2.54.0

View File

@ -0,0 +1,241 @@
From e0645da85c62c8b05a3350c3b976f1a2df8e1a52 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 10 Jul 2026 18:17:40 -0700
Subject: [PATCH] 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 | 103 ++++++++++++++++++
.../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(+), 9 deletions(-)
diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py
index 2f9a70686..665270754 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
@@ -1318,6 +1319,108 @@ def test_get_with_normalized_rid_dict():
assert nrd.get('099') is None
+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 831f50b3d..16e6c8f35 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
@@ -857,6 +857,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 1a273a2df..b364ec07a 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
@@ -3005,7 +3005,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

@ -0,0 +1,202 @@
From edfd0352e31e5fd093aad8169ff251a9d6b5faff Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 7 Jul 2026 17:26:30 -0700
Subject: [PATCH] Issue 7406 - Fix ldap-agent SNMP stats file loading (#7630)
Description: Fix ldap-agent stats file path construction so it opens the
instance .stats file instead of the truncated .stat path.
Move SNMP counter slot allocation after the configured worker thread count
is available so per-thread SNMP counter slots are created.
Add SNMP test that checks bindSecurityErrors updates in cn=snmp,cn=monitor
and verifies ldap-agent loads the instance stats file used for SNMP counters.
Fixes: https://github.com/389ds/389-ds-base/issues/7406
Reviewed by: @progier389 (Thanks!)
(cherry picked from commit 5d575d37c6babcf2d051ecd70764aa98eb168bfc)
---
.../tests/suites/snmp/regression_test.py | 137 ++++++++++++++++++
ldap/servers/slapd/connection.c | 2 +-
ldap/servers/snmp/main.c | 2 +-
3 files changed, 139 insertions(+), 2 deletions(-)
create mode 100644 dirsrvtests/tests/suites/snmp/regression_test.py
diff --git a/dirsrvtests/tests/suites/snmp/regression_test.py b/dirsrvtests/tests/suites/snmp/regression_test.py
new file mode 100644
index 000000000..5cc77b9d1
--- /dev/null
+++ b/dirsrvtests/tests/suites/snmp/regression_test.py
@@ -0,0 +1,137 @@
+# --- 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
+from subprocess import check_output, PIPE, run
+import time
+
+import ldap
+import pytest
+from lib389._constants import DN_DM, PW_DM
+from lib389.monitor import MonitorSNMP
+from test389.topologies import topology_st
+
+
+pytestmark = pytest.mark.tier1
+
+log = logging.getLogger(__name__)
+
+
+def get_bind_security_errors(inst):
+ return MonitorSNMP(inst).get_attr_val_int('bindsecurityerrors')
+
+
+def wait_for_bind_security_errors(inst, previous_value):
+ for _ in range(30):
+ current_value = get_bind_security_errors(inst)
+ if current_value > previous_value:
+ return current_value
+ time.sleep(1)
+ return get_bind_security_errors(inst)
+
+
+@pytest.fixture(scope="function")
+def ldapagent_config(topology_st, request):
+ """Creates an ldap-agent config for the standalone instance."""
+
+ var_dir = topology_st.standalone.get_local_state_dir()
+ config_file = os.path.join(topology_st.standalone.get_sysconf_dir(), 'dirsrv/config/agent.conf')
+ config = f"""agentx-supplier {var_dir}/agentx/supplier
+agent-logdir {var_dir}/log/dirsrv
+server slapd-{topology_st.standalone.serverid}
+"""
+
+ with open(config_file, 'w') as agent_config_file:
+ agent_config_file.write(config)
+
+ def fin():
+ os.remove(config_file)
+
+ request.addfinalizer(fin)
+
+ return config_file
+
+
+def test_ldapagent_uses_instance_stats_file(topology_st, ldapagent_config):
+ """Tests that ldap-agent loads the instance stats file used for SNMP counters
+
+ :id: 9bf83f16-922f-4505-8dc4-d22bd8e426ac
+ :setup: Standalone instance
+ :steps:
+ 1. Check the current bindSecurityErrors SNMP monitor counter.
+ 2. Perform an invalid Directory Manager bind.
+ 3. Check that bindSecurityErrors increased.
+ 4. Start ldap-agent with debug logging.
+ 5. Check that ldap-agent opens the instance .stats file.
+ 6. Check that ldap-agent does not open the truncated .stat file.
+ 7. Cleanup - Kill ldap-agent process.
+ :expectedresults:
+ 1. The initial bindSecurityErrors value should be readable.
+ 2. The invalid bind should fail.
+ 3. The bindSecurityErrors value should increase.
+ 4. ldap-agent should start.
+ 5. ldap-agent should open the full .stats file path used by ns-slapd.
+ 6. ldap-agent should not open or fail on the truncated .stat path.
+ 7. ldap-agent process should be successfully killed.
+ """
+
+ log.info('Running test_ldapagent_uses_instance_stats_file...')
+
+ if not os.path.exists(os.path.join(topology_st.standalone.get_sbin_dir(), 'ldap-agent')):
+ pytest.skip("ldap-agent is not present")
+
+ previous_bind_security_errors = get_bind_security_errors(topology_st.standalone)
+ with pytest.raises(ldap.INVALID_CREDENTIALS):
+ topology_st.standalone.simple_bind_s(DN_DM, 'badpassword')
+ topology_st.standalone.simple_bind_s(DN_DM, PW_DM)
+ assert wait_for_bind_security_errors(topology_st.standalone, previous_bind_security_errors) > previous_bind_security_errors
+
+ run_dir = topology_st.standalone.get_run_dir()
+ pidpath = os.path.join(run_dir, 'ldap-agent.pid')
+ agent_log = os.path.join(topology_st.standalone.get_local_state_dir(), 'log', 'dirsrv', 'ldap-agent.log')
+ expected_stats_file = os.path.join(run_dir, f'slapd-{topology_st.standalone.serverid}.stats')
+ truncated_stats_file = expected_stats_file[:-1]
+ expected_log_line = f'Opening stats file ({expected_stats_file})'
+ pid = None
+ agent_log_content = ''
+
+ if os.path.exists(agent_log):
+ os.remove(agent_log)
+
+ try:
+ check_output([os.path.join(topology_st.standalone.get_sbin_dir(), 'ldap-agent'), '-D', ldapagent_config])
+
+ with open(pidpath, 'r') as pf:
+ pid = pf.readlines()[0].strip()
+
+ for _ in range(30):
+ if os.path.exists(agent_log):
+ with open(agent_log, 'r') as lf:
+ agent_log_content = lf.read()
+ if expected_log_line in agent_log_content:
+ break
+ time.sleep(1)
+
+ assert expected_log_line in agent_log_content
+ assert f'Opening stats file ({truncated_stats_file})' not in agent_log_content
+ assert f'Unable to open stats file ({truncated_stats_file})' not in agent_log_content
+ finally:
+ if pid:
+ log.debug('test_ldapagent_uses_instance_stats_file: Terminating agent %s', pid)
+ run(['kill', pid], stdout=PIPE, stderr=PIPE)
+
+ log.info('test_ldapagent_uses_instance_stats_file: PASSED')
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/ldap/servers/slapd/connection.c b/ldap/servers/slapd/connection.c
index 6ddd21fcb..ff6d72372 100644
--- a/ldap/servers/slapd/connection.c
+++ b/ldap/servers/slapd/connection.c
@@ -464,12 +464,12 @@ init_op_threads()
}
pthread_condattr_destroy(&condAttr); /* no longer needed */
+ max_threads = config_get_threadnumber();
work_q_stack = PR_CreateStack("connection_work_q");
op_stack = PR_CreateStack("connection_operation");
alloc_per_thread_snmp_vars(max_threads);
init_thread_private_snmp_vars();
- max_threads = config_get_threadnumber();
threads_indexes = (int32_t *) slapi_ch_calloc(max_threads, sizeof(int32_t));
for (size_t i = 0; i < max_threads; i++) {
threads_indexes[i] = i + 1; /* idx 0 is reserved for global snmp_vars */
diff --git a/ldap/servers/snmp/main.c b/ldap/servers/snmp/main.c
index 8cc803fd3..c8fd46868 100644
--- a/ldap/servers/snmp/main.c
+++ b/ldap/servers/snmp/main.c
@@ -454,7 +454,7 @@ load_config(char *conf_path)
/* 8 = "/" + ".stats" + \0 */
serv_p->stats_file = calloc(1, vlen + (instancename ? strlen(instancename) : 0) + 8);
if (serv_p->stats_file && instancename) {
- snprintf(serv_p->stats_file, vlen + strlen(instancename) + 7,
+ snprintf(serv_p->stats_file, vlen + strlen(instancename) + 8,
"%s/%s.stats", val, instancename);
} else {
printf("ldap-agent: malloc error processing config file\n");
--
2.54.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,303 @@
From deb9fa07939c76459afbd1070f62dd2a55b204df Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 24 Jun 2026 16:12:43 +0200
Subject: [PATCH] Issue 7583 - Compressed logs are prematurely deleted (#7584)
Bug Description:
When log compression is enabled and the full path to a rotated
compressed log file exceeds 75 characters, the server fails to read the
actual compressed file size and falls back to the uncompressed
maxlogsize value 100 MB. This causes the maxdiskspace deletion check to
use incorrect sizes, triggering log deletion before the configured disk
space limit is reached.
Fix Description:
Use `sizeof(logfile)` instead of `sizeof(tbuf)` to construct the
compressed filename.
Fixes: https://github.com/389ds/389-ds-base/issues/7583
Reviewed by: @progier389, @droideck (Thanks!)
---
.../logging_long_path_compression_test.py | 219 ++++++++++++++++++
ldap/servers/slapd/log.c | 10 +-
2 files changed, 224 insertions(+), 5 deletions(-)
create mode 100644 dirsrvtests/tests/suites/logging/logging_long_path_compression_test.py
diff --git a/dirsrvtests/tests/suites/logging/logging_long_path_compression_test.py b/dirsrvtests/tests/suites/logging/logging_long_path_compression_test.py
new file mode 100644
index 000000000..61c53a658
--- /dev/null
+++ b/dirsrvtests/tests/suites/logging/logging_long_path_compression_test.py
@@ -0,0 +1,219 @@
+# --- 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 glob
+import logging
+import os
+import re
+import shutil
+import subprocess
+import time
+
+import pytest
+
+from lib389._constants import DEFAULT_SUFFIX
+from test389.topologies import topology_st as topo
+
+log = logging.getLogger(__name__)
+
+pytestmark = pytest.mark.tier1
+
+TBUFSIZE = 75
+LONG_DIR_NAME = 'someverylongpaththatislongerthan75chars'
+
+
+def generate_load(inst, threads=10, samples=6):
+ """Generate search load using ldclt to fill the access log fast.
+ Each sample is 10 seconds. Default 6 samples = 60 seconds."""
+ port = inst.port
+ subprocess.run([
+ 'ldclt', '-h', 'localhost', '-p', str(port),
+ '-b', DEFAULT_SUFFIX,
+ '-e', 'esearch',
+ '-f', 'uid=demo_user',
+ '-n', str(threads),
+ '-N', str(samples),
+ ], check=True, timeout=samples * 10 + 30)
+
+
+def parse_rotationinfo(filepath):
+ """Parse a .rotationinfo file and return list of dicts with path, ctime, size
+ for all 'Previous Log File' entries."""
+ entries = []
+ with open(filepath, 'r') as f:
+ for line in f:
+ m = re.match(r'LOGINFO:Previous Log File:(\S+)\s+\((\d+)\)\s+\((\d+)\)', line)
+ if m:
+ entries.append({
+ 'path': m.group(1),
+ 'ctime': int(m.group(2)),
+ 'size': int(m.group(3)),
+ })
+ return entries
+
+
+def get_rotated_log_files(log_dir, log_type='access'):
+ """Return sorted list of rotated log file paths."""
+ return sorted(glob.glob(f'{log_dir}/{log_type}.2*'))
+
+
+def cleanup_rotated_logs(log_dir, log_type='access'):
+ """Remove all rotated log files."""
+ for f in glob.glob(f'{log_dir}/{log_type}.2*'):
+ os.remove(f)
+
+
+@pytest.fixture()
+def long_path_setup(topo, request):
+ """Creates a long-name subdirectory for the access log,
+ reconfigures DS to use it with compression, returns the paths."""
+
+ inst = topo.standalone
+ log_dir = inst.get_log_dir()
+ original_accesslog = inst.config.get_attr_val_utf8('nsslapd-accesslog')
+
+ long_subdir = os.path.join(log_dir, LONG_DIR_NAME)
+ long_access_log = os.path.join(long_subdir, 'access')
+
+ # Verify the path will exceed TBUFSIZE with rotation suffix
+ sample_rotated = long_access_log + '.20260615-120000.gz'
+ assert len(sample_rotated) > TBUFSIZE, (
+ f"Test setup error: rotated path ({len(sample_rotated)} chars) must exceed "
+ f"TBUFSIZE ({TBUFSIZE}) to trigger the bug"
+ )
+
+ os.makedirs(long_subdir, exist_ok=True)
+ os.chown(long_subdir, inst.get_user_uid(), inst.get_group_gid())
+
+ inst.config.set('nsslapd-accesslog', long_access_log)
+ inst.config.set('nsslapd-accesslog-compress', 'on')
+ inst.config.set('nsslapd-accesslog-maxlogsize', '1')
+ inst.config.set('nsslapd-accesslog-logmaxdiskspace', '10')
+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '100')
+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
+ inst.config.set('nsslapd-accesslog-logbuffering', 'on')
+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
+ inst.config.set('nsslapd-accesslog-logminfreediskspace', '5')
+ inst.config.set('nsslapd-accesslog-logrotationtime', '1')
+ inst.config.set('nsslapd-accesslog-logrotationtimeunit', 'minute')
+ inst.config.set('nsslapd-statlog-level', '1')
+
+ def fin():
+ inst.config.set('nsslapd-accesslog', original_accesslog)
+ inst.config.set('nsslapd-accesslog-compress', 'off')
+ inst.config.set('nsslapd-accesslog-logmaxdiskspace', '500')
+ inst.config.set('nsslapd-accesslog-maxlogsize', '100')
+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '10')
+ inst.config.set('nsslapd-accesslog-logbuffering', 'on')
+ inst.config.set('nsslapd-accesslog-logexpirationtime', '1')
+ inst.config.set('nsslapd-accesslog-logexpirationtimeunit', 'month')
+ inst.config.set('nsslapd-accesslog-logrotationtime', '1')
+ inst.config.set('nsslapd-accesslog-logrotationtimeunit', 'day')
+ inst.config.set('nsslapd-accesslog-logminfreediskspace', '5')
+ inst.config.set('nsslapd-statlog-level', '0')
+ if os.path.exists(long_subdir):
+ shutil.rmtree(long_subdir)
+
+ request.addfinalizer(fin)
+
+ return {
+ 'inst': inst,
+ 'log_dir': log_dir,
+ 'long_subdir': long_subdir,
+ 'long_access_log': long_access_log,
+ }
+
+
+def test_compressed_log_long_path(topo, long_path_setup):
+ """Test that compressed log sizes in rotationinfo match actual file
+ sizes and that logs are not prematurely deleted when the access log
+ path exceeds 75 characters.
+
+ :id: 7c3b4a2e-1f8d-4e5a-b9c7-6d2e8f0a3b1c
+ :setup: Standalone Instance
+ :steps:
+ 1. Create a long-name subdirectory so the full rotated log filename
+ exceeds 75 characters (TBUFSIZE).
+ 2. Set access log to the long path with compression enabled,
+ maxlogsize 1 MB, and maxdiskspace 10 MB.
+ 3. Generate LDAP load to trigger many log rotations.
+ 4. Parse access.rotationinfo and compare recorded sizes against
+ actual compressed file sizes on disk.
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. At least 3 rotated compressed logs are created.
+ 4. Recorded sizes in rotationinfo must match actual file sizes,
+ not the maxlogsize fallback value.
+ """
+
+ inst = long_path_setup['inst']
+ long_subdir = long_path_setup['long_subdir']
+ long_access_log = long_path_setup['long_access_log']
+
+ # Generate load to trigger many rotations (6 samples × 10 sec = 60 sec)
+ generate_load(inst)
+
+ # Check rotationinfo sizes
+ rotinfo_path = long_access_log + '.rotationinfo'
+ assert os.path.exists(rotinfo_path), \
+ f"Rotationinfo file not found: {rotinfo_path}"
+
+ entries = parse_rotationinfo(rotinfo_path)
+ log.info(f"Rotationinfo has {len(entries)} entries")
+ assert len(entries) >= 3, \
+ f"Expected at least 3 rotated logs, got {len(entries)}"
+
+ maxlogsize_mb = int(inst.config.get_attr_val_utf8('nsslapd-accesslog-maxlogsize'))
+ maxlogsize_bytes = maxlogsize_mb * 1024 * 1024
+
+ mismatches = []
+ for entry in entries:
+ log_path = entry['path']
+ recorded_size = entry['size']
+
+ actual_path = log_path
+ if not os.path.exists(actual_path) and os.path.exists(log_path + '.gz'):
+ actual_path = log_path + '.gz'
+
+ if not os.path.exists(actual_path):
+ log.warning(f"File not found: {actual_path} (may have been deleted)")
+ continue
+
+ actual_size = os.path.getsize(actual_path)
+ log.info(f" {os.path.basename(actual_path)}: "
+ f"recorded={recorded_size}, actual={actual_size}")
+
+ if recorded_size != actual_size:
+ mismatches.append({
+ 'file': actual_path,
+ 'recorded': recorded_size,
+ 'actual': actual_size,
+ })
+
+ assert len(mismatches) == 0, (
+ f"Compressed log sizes in rotationinfo do not match actual file sizes! "
+ f"{len(mismatches)} of {len(entries)} entries differ. "
+ f"Mismatched files: "
+ f"{[m['file'] + ': recorded=' + str(m['recorded']) + ' actual=' + str(m['actual']) for m in mismatches]}"
+ )
+
+ # Log retained file count for debugging
+ rotated_logs = get_rotated_log_files(long_subdir, 'access')
+ log.info(f"Rotated logs retained: {len(rotated_logs)}")
+ for f in rotated_logs:
+ log.info(f" {os.path.basename(f)}: {os.path.getsize(f)} bytes")
+
+ total_actual = sum(os.path.getsize(f) for f in rotated_logs)
+ log.info(f"Total actual disk usage of rotated logs: {total_actual} bytes "
+ f"({total_actual / (1024*1024):.2f} MB)")
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
index fe075b16b..676a8fff2 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -3532,7 +3532,7 @@ log__open_accesslogfile(int logfile_state, int locked)
PR_snprintf(tbuf, sizeof(tbuf), "%s.gz", tbuf);
/* get and set the size of the new gziped file */
- PR_snprintf(logfile, sizeof(tbuf), "%s.%s", loginfo.log_access_file, tbuf);
+ PR_snprintf(logfile, sizeof(logfile), "%s.%s", loginfo.log_access_file, tbuf);
if ((logp->l_size = log__getfilesize_with_filename(logfile)) == -1) {
/* Then assume that we have the max size */
logp->l_size = loginfo.log_access_maxlogsize;
@@ -3703,7 +3703,7 @@ log__open_securitylogfile(int logfile_state, int locked)
PR_snprintf(tbuf, sizeof(tbuf), "%s.gz", tbuf);
/* get and set the size of the new gziped file */
- PR_snprintf(logfile, sizeof(tbuf), "%s.%s", loginfo.log_security_file, tbuf);
+ PR_snprintf(logfile, sizeof(logfile), "%s.%s", loginfo.log_security_file, tbuf);
if ((logp->l_size = log__getfilesize_with_filename(logfile)) == -1) {
/* Then assume that we have the max size */
logp->l_size = loginfo.log_security_maxlogsize;
@@ -6469,7 +6469,7 @@ log__open_errorlogfile(int logfile_state, int locked)
PR_snprintf(tbuf, sizeof(tbuf), "%s.gz", tbuf);
/* get and set the size of the new gziped file */
- PR_snprintf(logfile, sizeof(tbuf), "%s.%s", loginfo.log_error_file, tbuf);
+ PR_snprintf(logfile, sizeof(logfile), "%s.%s", loginfo.log_error_file, tbuf);
if ((logp->l_size = log__getfilesize_with_filename(logfile)) == -1) {
/* Then assume that we have the max size */
logp->l_size = loginfo.log_error_maxlogsize;
@@ -6634,7 +6634,7 @@ log__open_auditlogfile(int logfile_state, int locked)
PR_snprintf(tbuf, sizeof(tbuf), "%s.gz", tbuf);
/* get and set the size of the new gziped file */
- PR_snprintf(logfile, sizeof(tbuf), "%s.%s", loginfo.log_audit_file, tbuf);
+ PR_snprintf(logfile, sizeof(logfile), "%s.%s", loginfo.log_audit_file, tbuf);
if ((logp->l_size = log__getfilesize_with_filename(logfile)) == -1) {
/* Then assume that we have the max size */
logp->l_size = loginfo.log_audit_maxlogsize;
@@ -6799,7 +6799,7 @@ log__open_auditfaillogfile(int logfile_state, int locked)
PR_snprintf(tbuf, sizeof(tbuf), "%s.gz", tbuf);
/* get and set the size of the new gziped file */
- PR_snprintf(logfile, sizeof(tbuf), "%s.%s", loginfo.log_auditfail_file, tbuf);
+ PR_snprintf(logfile, sizeof(logfile), "%s.%s", loginfo.log_auditfail_file, tbuf);
if ((logp->l_size = log__getfilesize_with_filename(logfile)) == -1) {
/* Then assume that we have the max size */
logp->l_size = loginfo.log_auditfail_maxlogsize;
--
2.54.0

View File

@ -0,0 +1,187 @@
From 40fc75ff66eccd533cdc7f92464cdf19aab4fe5b Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 15 Jun 2026 12:39:34 +0200
Subject: [PATCH] Issue 7573 - Post-import cache autotuning does not recompute
entry cache size (#7574)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bug Description:
When a new empty backend is created, and an online import is completed,
`dbmdb_start_autotune()` runs but doesnt apply recomputed cache values.
A server restart is required for the new cache sizes to take effect.
Fix Description:
Always apply autotuning when autosize > 0.
Fixes: https://github.com/389ds/389-ds-base/issues/7573
Relates: https://github.com/389ds/389-ds-base/issues/6805
Reviewed by: @progier389 (Thanks!)
---
.../tests/suites/config/autotuning_test.py | 106 ++++++++++++++++++
.../servers/slapd/back-ldbm/db-mdb/mdb_misc.c | 15 ++-
2 files changed, 116 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/config/autotuning_test.py b/dirsrvtests/tests/suites/config/autotuning_test.py
index b1d0eb010..cda5f9a39 100644
--- a/dirsrvtests/tests/suites/config/autotuning_test.py
+++ b/dirsrvtests/tests/suites/config/autotuning_test.py
@@ -17,6 +17,8 @@ from test389.topologies import topology_st as topo
from lib389.backend import Backends
from lib389.idm.user import UserAccounts
from lib389.config import BDB_LDBMConfig, LMDB_LDBMConfig
+from lib389.tasks import ImportTask
+from lib389.dbgen import dbgen_users
from lib389._constants import (
@@ -630,6 +632,110 @@ def test_cache_autosize_multi_backends(topo):
userroot_cachesize = userroot_ldbm.get_attr_val('nsslapd-cachememsize')
assert int(userroot_cachesize) != 77777777
+@pytest.mark.skipif(get_default_db_lib() == "bdb",
+ reason="MDB-specific test")
+def test_mdb_cache_autotune_after_import(topo):
+ """Check that cache autotuning re-applies after an online import
+
+ When a fresh instance starts with an empty database, autotuning
+ computes a small cache size (64MB). After an online ldif2db import
+ populates the database, the post-import autotune call should recompute
+ and apply larger cache sizes based on the actual database page count
+ without requiring a restart.
+
+ :id: a3e7b2c1-8f4d-4e6a-9c5b-1d2e3f4a5b6c
+ :setup: Standalone instance
+ :steps:
+ 1. Record the initial autotuned cache sizes (from empty DB)
+ 2. Generate an LDIF with 5000 entries
+ 3. Perform an online import (ldif2db)
+ 4. Check cache sizes immediately after import
+ 5. Restart the server
+ 6. Check cache sizes after restart
+ :expectedresults:
+ 1. Cache sizes should be at 64MB
+ 2. LDIF is generated successfully
+ 3. Import completes successfully
+ 4. Cache sizes should increase after import
+ 5. Server restarts successfully
+ 6. Cache sizes should be properly autotuned based on data
+ """
+
+ inst = topo.standalone
+ mdb_config_ldbm = LMDB_LDBMConfig(inst)
+ MEGABYTE_64 = 64 * 1024 * 1024
+
+ log.info("Recreating backend to get an empty database")
+ mdb_config_ldbm.set('nsslapd-cache-autosize', '25')
+
+ backends = Backends(inst)
+ userroot = backends.get('userRoot')
+ userroot.delete()
+
+ backends.create(properties={
+ 'nsslapd-suffix': DEFAULT_SUFFIX,
+ 'name': 'userRoot',
+ })
+ inst.restart()
+
+ userroot_ldbm = DSLdapObject(inst, DN_USERROOT_LDBM)
+ cachememsize_before = int(userroot_ldbm.get_attr_val_utf8('nsslapd-cachememsize'))
+ dncachememsize_before = int(userroot_ldbm.get_attr_val_utf8('nsslapd-dncachememsize'))
+ log.info("Cache on empty DB: cachememsize=%d, dncachememsize=%d",
+ cachememsize_before, dncachememsize_before)
+
+ assert cachememsize_before == MEGABYTE_64, (
+ f"Expected 64MB entry cache on empty DB, got {cachememsize_before}")
+
+ log.info("Generating LDIF with 5000 entries")
+ ldif_dir = inst.get_ldif_dir()
+ ldif_file = os.path.join(ldif_dir, 'autotune_test.ldif')
+ dbgen_users(inst, 5000, ldif_file, DEFAULT_SUFFIX, generic=True,
+ parent=f"ou=People,{DEFAULT_SUFFIX}")
+
+ log.info("Performing online import")
+ import_task = ImportTask(inst)
+ import_task.import_suffix_from_ldif(ldiffile=ldif_file, suffix=DEFAULT_SUFFIX)
+ import_task.wait(timeout=300)
+ exit_code = import_task.get_exit_code()
+ assert exit_code == 0, f"Import task failed with exit code {exit_code}"
+ os.remove(ldif_file)
+
+ people = DSLdapObject(inst, f"ou=People,{DEFAULT_SUFFIX}")
+ num_subordinates = int(people.get_attr_val_utf8('numSubordinates'))
+ log.info("Imported %d entries", num_subordinates)
+ assert num_subordinates >= 5000, \
+ f"Expected at least 5000 entries, got {num_subordinates}"
+
+ cachememsize_after_import = int(userroot_ldbm.get_attr_val_utf8('nsslapd-cachememsize'))
+ dncachememsize_after_import = int(userroot_ldbm.get_attr_val_utf8('nsslapd-dncachememsize'))
+ log.info("Cache after import (no restart): cachememsize=%d, dncachememsize=%d",
+ cachememsize_after_import, dncachememsize_after_import)
+
+ assert cachememsize_after_import > MEGABYTE_64, (
+ f"Post-import autotune should have increased entry cache above 64MB, "
+ f"got {cachememsize_after_import}")
+ assert dncachememsize_after_import > MEGABYTE_64, (
+ f"Post-import autotune should have increased DN cache above 64MB, "
+ f"got {dncachememsize_after_import}")
+
+ log.info("Restarting server")
+ inst.restart()
+
+ userroot_ldbm = DSLdapObject(inst, DN_USERROOT_LDBM)
+ cachememsize_after_restart = int(userroot_ldbm.get_attr_val_utf8('nsslapd-cachememsize'))
+ dncachememsize_after_restart = int(userroot_ldbm.get_attr_val_utf8('nsslapd-dncachememsize'))
+ log.info("Cache after restart: cachememsize=%d, dncachememsize=%d",
+ cachememsize_after_restart, dncachememsize_after_restart)
+
+ assert cachememsize_after_import == cachememsize_after_restart, (
+ f"Post-import entry cache ({cachememsize_after_import}) "
+ f"!= post-restart entry cache ({cachememsize_after_restart})")
+ assert dncachememsize_after_import == dncachememsize_after_restart, (
+ f"Post-import DN cache ({dncachememsize_after_import}) "
+ f"!= post-restart DN cache ({dncachememsize_after_restart})")
+
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c
index 7a1860894..aa0cca96b 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c
@@ -319,11 +319,16 @@ dbmdb_start_autotune(struct ldbminfo *li)
dn_size = clamp_div * (64 * MEGABYTE);
}
- /* This is the point where we decide to apply or not. If the cache
- * size is equal or less than MINCACHESIZE then we assume it does not
- * have a custom value and we can autotune
+ /* This is the point where we decide to apply or not.
+ *
+ * If autosize > 0, we always apply because the admin explicitly
+ * requested autotuning on every startup (and after online import).
+ *
+ * If autosize <= 0 (default), we only apply on first run when
+ * the cache still has the initial default value, so that a
+ * manually configured value is preserved.
*/
- if (cache_size <= MINCACHESIZE) {
+ if (li->li_cache_autosize > 0 || cache_size <= MINCACHESIZE) {
slapi_log_err(SLAPI_LOG_NOTICE, "mdb_start_autotune",
"cache autosizing: %s entry cache (%" PRIu64 " total): %s\n",
inst->inst_name, backend_count,
@@ -331,7 +336,7 @@ dbmdb_start_autotune(struct ldbminfo *li)
cache_set_max_entries(&(inst->inst_cache), -1, true /* autotuned */);
cache_set_max_size(&(inst->inst_cache), ec_size, CACHE_TYPE_ENTRY, true);
}
- if (dncache_size <= DEFAULT_DNCACHE_SIZE) {
+ if (li->li_cache_autosize > 0 || dncache_size <= DEFAULT_DNCACHE_SIZE) {
slapi_log_err(SLAPI_LOG_NOTICE, "mdb_start_autotune",
"cache autosizing: %s dn cache (%" PRIu64 " total): %s\n",
inst->inst_name, backend_count,
--
2.54.0

View File

@ -64,6 +64,9 @@ Version: 3.3.0
Release: %{autorelease -n %{?with_asan:-e asan}}%{?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
Conflicts: selinux-policy-base < 3.9.8
# The dirsrv@.service NoNewPrivileges/MemoryDenyWriteExecute hardening
# requires SELinux policy rules only available from this version onward.
Requires: selinux-policy >= 42.1.22-1
Conflicts: freeipa-server < 4.0.3
Obsoletes: %{name} <= 1.4.4
URL: https://www.port389.org/
@ -312,6 +315,17 @@ Patch: 0003-Issue-7554-deref-plugin-null-pointer-dereference-if-.patc
Patch: 0004-Issue-3555-UI-Fix-audit-issue-with-npm-brace-expansi.patch
Patch: 0005-Issue-7549-Substring-index-should-validate-minimum-n.patch
Patch: 0006-Issue-7539-Server-shutdown-during-online-reindex-may.patch
Patch: 0007-Issue-7562-Error-NssSsl.add_cert-got-an-unexpected-k.patch
Patch: 0008-Issue-7500-Prevent-unsigned-integer-underflow-during.patch
Patch: 0009-Issue-7558-During-online-import-the-IDL-should-be-cr.patch
Patch: 0010-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Patch: 0011-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch
Patch: 0012-Issue-7284-Automated-test-for-creating-local-passwor.patch
Patch: 0013-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch
Patch: 0014-Issue-7406-Fix-ldap-agent-SNMP-stats-file-loading-76.patch
Patch: 0015-Issue-7633-RFE-Add-offline-diagnostics-for-thread-po.patch
Patch: 0016-Issue-7583-Compressed-logs-are-prematurely-deleted-7.patch
Patch: 0017-Issue-7573-Post-import-cache-autotuning-does-not-rec.patch
%description
389 Directory Server is an LDAPv3 compliant server. The base package includes