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:
parent
5a8e6532df
commit
b1018138a1
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
598
0009-Issue-7558-During-online-import-the-IDL-should-be-cr.patch
Normal file
598
0009-Issue-7558-During-online-import-the-IDL-should-be-cr.patch
Normal 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
|
||||
|
||||
164
0010-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Normal file
164
0010-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
241
0013-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch
Normal file
241
0013-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch
Normal 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, ¶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 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
|
||||
|
||||
202
0014-Issue-7406-Fix-ldap-agent-SNMP-stats-file-loading-76.patch
Normal file
202
0014-Issue-7406-Fix-ldap-agent-SNMP-stats-file-loading-76.patch
Normal 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
|
||||
|
||||
2595
0015-Issue-7633-RFE-Add-offline-diagnostics-for-thread-po.patch
Normal file
2595
0015-Issue-7633-RFE-Add-offline-diagnostics-for-thread-po.patch
Normal file
File diff suppressed because it is too large
Load Diff
303
0016-Issue-7583-Compressed-logs-are-prematurely-deleted-7.patch
Normal file
303
0016-Issue-7583-Compressed-logs-are-prematurely-deleted-7.patch
Normal 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
|
||||
|
||||
187
0017-Issue-7573-Post-import-cache-autotuning-does-not-rec.patch
Normal file
187
0017-Issue-7573-Post-import-cache-autotuning-does-not-rec.patch
Normal 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 doesn’t 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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user