Bump version to 2.7.0-5

- Resolves: RHEL-89762 - dsidm Error: float() argument must be a string or a number, not 'NoneType' [rhel-9]
- Resolves: RHEL-92041 - Memory leak in roles_cache_create_object_from_entry
- Resolves: RHEL-95444 - ns-slapd[xxxx]: segfault at 10d7d0d0 ip 00007ff734050cdb sp 00007ff6de9f1430 error 6 in libslapd.so.0.1.0[7ff733ec0000+1b3000] [rhel-9]
- Resolves: RHEL-104821 - ipa-restore fails to restore SELinux contexts, causes ns-slapd AVC denials on /dev/shm after restore.
- Resolves: RHEL-107005 - Failure to get Server monitoring data when NDN cache is disabled. [rhel-9]
- Resolves: RHEL-107581 - segfault - error 4 in libpthread-2.28.so [rhel-9]
- Resolves: RHEL-107585 - ns-slapd crashed when we add nsslapd-referral [rhel-9]
- Resolves: RHEL-107586 - CWE-284 dirsrv log rotation creates files with world readable permission [rhel-9]
- Resolves: RHEL-107587 - CWE-532 Created user password hash available to see in audit log [rhel-9]
- Resolves: RHEL-107588 - CWE-778 Log doesn't show what user gets password changed by administrator [rhel-9]
This commit is contained in:
Viktor Ashirov 2025-08-05 19:38:33 +02:00
parent ecd5366aec
commit ceeccf7e2e
30 changed files with 7136 additions and 22 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
/389-ds-base-*.tar.bz2
/jemalloc-*.tar.bz2
/libdb-5.3.28-59.tar.bz2
/Cargo-*.lock
/vendor-*.tar.gz

View File

@ -16,7 +16,7 @@ Reviewed by: @droideck, @tbordaz (thanks!)
3 files changed, 247 insertions(+), 13 deletions(-)
diff --git a/dirsrvtests/tests/suites/config/regression_test.py b/dirsrvtests/tests/suites/config/regression_test.py
index 8dbba8cd2f..6e313ac8ab 100644
index 8dbba8cd2..6e313ac8a 100644
--- a/dirsrvtests/tests/suites/config/regression_test.py
+++ b/dirsrvtests/tests/suites/config/regression_test.py
@@ -28,6 +28,8 @@ CUSTOM_MEM = '9100100100'
@ -91,7 +91,7 @@ index 8dbba8cd2f..6e313ac8ab 100644
+ assert inst.config.get_attr_val_utf8(RO_ATTR) == "off"
+ inst.config.replace(attr, val)
diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c
index e3157c1ce5..0f266f0d70 100644
index e3157c1ce..0f266f0d7 100644
--- a/ldap/servers/slapd/dse.c
+++ b/ldap/servers/slapd/dse.c
@@ -1031,6 +1031,114 @@ dse_check_for_readonly_error(Slapi_PBlock *pb, struct dse *pdse)
@ -219,7 +219,7 @@ index e3157c1ce5..0f266f0d70 100644
}
diff --git a/ldap/servers/slapd/mapping_tree.c b/ldap/servers/slapd/mapping_tree.c
index dd7b1af37c..e51b3b9484 100644
index dd7b1af37..e51b3b948 100644
--- a/ldap/servers/slapd/mapping_tree.c
+++ b/ldap/servers/slapd/mapping_tree.c
@@ -2058,6 +2058,82 @@ slapi_dn_write_needs_referral(Slapi_DN *target_sdn, Slapi_Entry **referral)

View File

@ -0,0 +1,125 @@
From 5613937623f0037a54490b22c60f7eb1aa52cf4e Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Wed, 25 Jun 2025 14:11:05 +0000
Subject: [PATCH] =?UTF-8?q?Issue=206825=20-=20RootDN=20Access=20Control=20?=
=?UTF-8?q?Plugin=20with=20wildcards=20for=20IP=20addre=E2=80=A6=20(#6826)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bug description:
RootDN Access Control Plugin with wildcards for IP addresses fails withi
an error "Invalid IP address"
socket.inet_aton() validates IPv4 IP addresses and does not support wildcards.
Fix description:
Add a regex pattern to match wildcard IP addresses, check each octet is
between 0-255
Fixes: https://github.com/389ds/389-ds-base/issues/6825
Reviewed by: @droideck (Thank you)
---
.../lib389/cli_conf/plugins/rootdn_ac.py | 16 +++-----
src/lib389/lib389/utils.py | 40 +++++++++++++++++++
2 files changed, 45 insertions(+), 11 deletions(-)
diff --git a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
index 65486fff8..1456f5ebe 100644
--- a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
+++ b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
@@ -8,7 +8,7 @@
import socket
from lib389.plugins import RootDNAccessControlPlugin
-from lib389.utils import is_valid_hostname
+from lib389.utils import is_valid_hostname, is_valid_ip
from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit
from lib389.cli_base import CustomHelpFormatter
@@ -62,19 +62,13 @@ def validate_args(args):
if args.allow_ip is not None:
for ip in args.allow_ip:
- if ip != "delete":
- try:
- socket.inet_aton(ip)
- except socket.error:
- raise ValueError(f"Invalid IP address ({ip}) for '--allow-ip'")
+ if ip != "delete" and not is_valid_ip(ip):
+ raise ValueError(f"Invalid IP address ({ip}) for '--allow-ip'")
if args.deny_ip is not None and args.deny_ip != "delete":
for ip in args.deny_ip:
- if ip != "delete":
- try:
- socket.inet_aton(ip)
- except socket.error:
- raise ValueError(f"Invalid IP address ({ip}) for '--deny-ip'")
+ if ip != "delete" and not is_valid_ip(ip):
+ raise ValueError(f"Invalid IP address ({ip}) for '--deny-ip'")
if args.allow_host is not None:
for hostname in args.allow_host:
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index afc282e94..3937fc1a8 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -31,6 +31,7 @@ import logging
import shutil
import ldap
import socket
+import ipaddress
import time
import stat
from datetime import (datetime, timedelta)
@@ -1707,6 +1708,45 @@ def is_valid_hostname(hostname):
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in hostname.split("."))
+def is_valid_ip(ip):
+ """ Validate an IPv4 or IPv6 address, including asterisks for wildcards. """
+ if '*' in ip and '.' in ip:
+ ipv4_pattern = r'^(\d{1,3}|\*)\.(\d{1,3}|\*)\.(\d{1,3}|\*)\.(\d{1,3}|\*)$'
+ if re.match(ipv4_pattern, ip):
+ octets = ip.split('.')
+ for octet in octets:
+ if octet != '*':
+ try:
+ val = int(octet, 10)
+ if not (0 <= val <= 255):
+ return False
+ except ValueError:
+ return False
+ return True
+ else:
+ return False
+
+ if '*' in ip and ':' in ip:
+ ipv6_pattern = r'^([0-9a-fA-F]{1,4}|\*)(:([0-9a-fA-F]{1,4}|\*)){0,7}$'
+ if re.match(ipv6_pattern, ip):
+ octets = ip.split(':')
+ for octet in octets:
+ if octet != '*':
+ try:
+ val = int(octet, 16)
+ if not (0 <= val <= 0xFFFF):
+ return False
+ except ValueError:
+ return False
+ return True
+ else:
+ return False
+
+ try:
+ ipaddress.ip_address(ip)
+ return True
+ except ValueError:
+ return False
def parse_size(size):
"""
--
2.49.0

View File

@ -0,0 +1,50 @@
From b8cac173ca2549d2142332107e06fcb4bd34bd65 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Fri, 8 Mar 2024 16:15:52 +0000
Subject: [PATCH] Issue 6119 - Synchronise accept_thread with slapd_daemon
(#6120)
Bug Description: A corner cases exists, where the slapd_daemon has
begun its shutdown process but the accept_thread is still running
and capable of handling new connections. When this scenario occurs,
the connection subsystem has been partially deallocated and is in
an unstable state. A segfault is generated when attempting to get a
new connection from the connection table.
Fix Description: The connection table is only deallocated when the
number of active threads is 0. Modify the accept_thread to adjust the
the active thread count during creation/destruction, meaning the connection
table can only be freed when the accept_thread has completed
Relates: https://github.com/389ds/389-ds-base/issues/6119
Reviewed by: @tbordaz, @Firstyear , @mreynolds389 (Thank you)
---
ldap/servers/slapd/daemon.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index 5d01a2526..a43fc9285 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -868,6 +868,8 @@ accept_thread(void *vports)
slapi_ch_free((void **)&listener_idxs);
slapd_sockets_ports_free(ports);
slapi_ch_free((void **)&fds);
+ g_decr_active_threadcnt();
+ slapi_log_err(SLAPI_LOG_INFO, "slapd_daemon", "slapd shutting down - accept_thread\n");
}
void
@@ -1158,6 +1160,8 @@ slapd_daemon(daemon_ports_t *ports)
slapi_log_err(SLAPI_LOG_EMERG, "slapd_daemon", "Unable to fd accept thread - Shutting Down (" SLAPI_COMPONENT_NAME_NSPR " error %d - %s)\n",
errorCode, slapd_pr_strerror(errorCode));
g_set_shutdown(SLAPI_SHUTDOWN_EXIT);
+ } else{
+ g_incr_active_threadcnt();
}
#ifdef WITH_SYSTEMD
--
2.49.0

View File

@ -0,0 +1,127 @@
From 7943443bb92fca6676922349fb12503a527cb6b1 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 15 May 2025 10:35:27 -0400
Subject: [PATCH] Issue 6782 - Improve paged result locking
Description:
When cleaning a slot, instead of mem setting everything to Zero and restoring
the mutex, manually reset all the values leaving the mutex pointer
intact.
There is also a deadlock possibility when checking for abandoned PR search
in opshared.c, and we were checking a flag value outside of the per_conn
lock.
Relates: https://github.com/389ds/389-ds-base/issues/6782
Reviewed by: progier & spichugi(Thanks!!)
---
ldap/servers/slapd/opshared.c | 10 +++++++++-
ldap/servers/slapd/pagedresults.c | 27 +++++++++++++++++----------
2 files changed, 26 insertions(+), 11 deletions(-)
diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c
index 7dc2d5983..14a7dcdfb 100644
--- a/ldap/servers/slapd/opshared.c
+++ b/ldap/servers/slapd/opshared.c
@@ -592,6 +592,14 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
int32_t tlimit;
slapi_pblock_get(pb, SLAPI_SEARCH_TIMELIMIT, &tlimit);
pagedresults_set_timelimit(pb_conn, operation, (time_t)tlimit, pr_idx);
+ /* When using this mutex in conjunction with the main paged
+ * result lock, you must do so in this order:
+ *
+ * --> pagedresults_lock()
+ * --> pagedresults_mutex
+ * <-- pagedresults_mutex
+ * <-- pagedresults_unlock()
+ */
pagedresults_mutex = pageresult_lock_get_addr(pb_conn);
}
@@ -717,11 +725,11 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
pr_search_result = pagedresults_get_search_result(pb_conn, operation, 1 /*locked*/, pr_idx);
if (pr_search_result) {
if (pagedresults_is_abandoned_or_notavailable(pb_conn, 1 /*locked*/, pr_idx)) {
+ pthread_mutex_unlock(pagedresults_mutex);
pagedresults_unlock(pb_conn, pr_idx);
/* Previous operation was abandoned and the simplepaged object is not in use. */
send_ldap_result(pb, 0, NULL, "Simple Paged Results Search abandoned", 0, NULL);
rc = LDAP_SUCCESS;
- pthread_mutex_unlock(pagedresults_mutex);
goto free_and_return;
} else {
slapi_pblock_set(pb, SLAPI_SEARCH_RESULT_SET, pr_search_result);
diff --git a/ldap/servers/slapd/pagedresults.c b/ldap/servers/slapd/pagedresults.c
index 642aefb3d..c3f3aae01 100644
--- a/ldap/servers/slapd/pagedresults.c
+++ b/ldap/servers/slapd/pagedresults.c
@@ -48,7 +48,6 @@ pageresult_lock_get_addr(Connection *conn)
static void
_pr_cleanup_one_slot(PagedResults *prp)
{
- PRLock *prmutex = NULL;
if (!prp) {
return;
}
@@ -56,13 +55,17 @@ _pr_cleanup_one_slot(PagedResults *prp)
/* sr is left; release it. */
prp->pr_current_be->be_search_results_release(&(prp->pr_search_result_set));
}
- /* clean up the slot */
- if (prp->pr_mutex) {
- /* pr_mutex is reused; back it up and reset it. */
- prmutex = prp->pr_mutex;
- }
- memset(prp, '\0', sizeof(PagedResults));
- prp->pr_mutex = prmutex;
+
+ /* clean up the slot except the mutex */
+ prp->pr_current_be = NULL;
+ prp->pr_search_result_set = NULL;
+ prp->pr_search_result_count = 0;
+ prp->pr_search_result_set_size_estimate = 0;
+ prp->pr_sort_result_code = 0;
+ prp->pr_timelimit_hr.tv_sec = 0;
+ prp->pr_timelimit_hr.tv_nsec = 0;
+ prp->pr_flags = 0;
+ prp->pr_msgid = 0;
}
/*
@@ -1007,7 +1010,8 @@ op_set_pagedresults(Operation *op)
/*
* pagedresults_lock/unlock -- introduced to protect search results for the
- * asynchronous searches.
+ * asynchronous searches. Do not call these functions while the PR conn lock
+ * is held (e.g. pageresult_lock_get_addr(conn))
*/
void
pagedresults_lock(Connection *conn, int index)
@@ -1045,6 +1049,8 @@ int
pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int index)
{
PagedResults *prp;
+ int32_t result;
+
if (!conn || (index < 0) || (index >= conn->c_pagedresults.prl_maxlen)) {
return 1; /* not abandoned, but do not want to proceed paged results op. */
}
@@ -1052,10 +1058,11 @@ pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int inde
pthread_mutex_lock(pageresult_lock_get_addr(conn));
}
prp = conn->c_pagedresults.prl_list + index;
+ result = prp->pr_flags & CONN_FLAG_PAGEDRESULTS_ABANDONED;
if (!locked) {
pthread_mutex_unlock(pageresult_lock_get_addr(conn));
}
- return prp->pr_flags & CONN_FLAG_PAGEDRESULTS_ABANDONED;
+ return result;
}
int
--
2.49.0

View File

@ -0,0 +1,488 @@
From b6729a99f3a3d4c6ebe82d4bb60ea2a6f8727782 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 27 Jun 2025 18:43:39 -0700
Subject: [PATCH] Issue 6822 - Backend creation cleanup and Database UI tab
error handling (#6823)
Description: Add rollback functionality when mapping tree creation fails
during backend creation to prevent orphaned backends.
Improve error handling in Database, Replication and Monitoring UI tabs
to gracefully handle backend get-tree command failures.
Fixes: https://github.com/389ds/389-ds-base/issues/6822
Reviewed by: @mreynolds389 (Thanks!)
---
src/cockpit/389-console/src/database.jsx | 119 ++++++++------
src/cockpit/389-console/src/monitor.jsx | 172 +++++++++++---------
src/cockpit/389-console/src/replication.jsx | 55 ++++---
src/lib389/lib389/backend.py | 18 +-
4 files changed, 210 insertions(+), 154 deletions(-)
diff --git a/src/cockpit/389-console/src/database.jsx b/src/cockpit/389-console/src/database.jsx
index c0c4be414..276125dfc 100644
--- a/src/cockpit/389-console/src/database.jsx
+++ b/src/cockpit/389-console/src/database.jsx
@@ -478,6 +478,59 @@ export class Database extends React.Component {
}
loadSuffixTree(fullReset) {
+ const treeData = [
+ {
+ name: _("Global Database Configuration"),
+ icon: <CogIcon />,
+ id: "dbconfig",
+ },
+ {
+ name: _("Chaining Configuration"),
+ icon: <ExternalLinkAltIcon />,
+ id: "chaining-config",
+ },
+ {
+ name: _("Backups & LDIFs"),
+ icon: <CopyIcon />,
+ id: "backups",
+ },
+ {
+ name: _("Password Policies"),
+ id: "pwp",
+ icon: <KeyIcon />,
+ children: [
+ {
+ name: _("Global Policy"),
+ icon: <HomeIcon />,
+ id: "pwpolicy",
+ },
+ {
+ name: _("Local Policies"),
+ icon: <UsersIcon />,
+ id: "localpwpolicy",
+ },
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: _("Suffixes"),
+ icon: <CatalogIcon />,
+ id: "suffixes-tree",
+ children: [],
+ defaultExpanded: true,
+ action: (
+ <Button
+ onClick={this.handleShowSuffixModal}
+ variant="plain"
+ aria-label="Create new suffix"
+ title={_("Create new suffix")}
+ >
+ <PlusIcon />
+ </Button>
+ ),
+ }
+ ];
+
const cmd = [
"dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
"backend", "get-tree",
@@ -491,58 +544,20 @@ export class Database extends React.Component {
suffixData = JSON.parse(content);
this.processTree(suffixData);
}
- const treeData = [
- {
- name: _("Global Database Configuration"),
- icon: <CogIcon />,
- id: "dbconfig",
- },
- {
- name: _("Chaining Configuration"),
- icon: <ExternalLinkAltIcon />,
- id: "chaining-config",
- },
- {
- name: _("Backups & LDIFs"),
- icon: <CopyIcon />,
- id: "backups",
- },
- {
- name: _("Password Policies"),
- id: "pwp",
- icon: <KeyIcon />,
- children: [
- {
- name: _("Global Policy"),
- icon: <HomeIcon />,
- id: "pwpolicy",
- },
- {
- name: _("Local Policies"),
- icon: <UsersIcon />,
- id: "localpwpolicy",
- },
- ],
- defaultExpanded: true
- },
- {
- name: _("Suffixes"),
- icon: <CatalogIcon />,
- id: "suffixes-tree",
- children: suffixData,
- defaultExpanded: true,
- action: (
- <Button
- onClick={this.handleShowSuffixModal}
- variant="plain"
- aria-label="Create new suffix"
- title={_("Create new suffix")}
- >
- <PlusIcon />
- </Button>
- ),
- }
- ];
+
+ let current_node = this.state.node_name;
+ if (fullReset) {
+ current_node = DB_CONFIG;
+ }
+
+ treeData[4].children = suffixData; // suffixes node
+ this.setState(() => ({
+ nodes: treeData,
+ node_name: current_node,
+ }), this.loadAttrs);
+ })
+ .fail(err => {
+ // Handle backend get-tree failure gracefully
let current_node = this.state.node_name;
if (fullReset) {
current_node = DB_CONFIG;
diff --git a/src/cockpit/389-console/src/monitor.jsx b/src/cockpit/389-console/src/monitor.jsx
index ad48d1f87..91a8e3e37 100644
--- a/src/cockpit/389-console/src/monitor.jsx
+++ b/src/cockpit/389-console/src/monitor.jsx
@@ -200,6 +200,84 @@ export class Monitor extends React.Component {
}
loadSuffixTree(fullReset) {
+ const basicData = [
+ {
+ name: _("Server Statistics"),
+ icon: <ClusterIcon />,
+ id: "server-monitor",
+ type: "server",
+ },
+ {
+ name: _("Replication"),
+ icon: <TopologyIcon />,
+ id: "replication-monitor",
+ type: "replication",
+ defaultExpanded: true,
+ children: [
+ {
+ name: _("Synchronization Report"),
+ icon: <MonitoringIcon />,
+ id: "sync-report",
+ item: "sync-report",
+ type: "repl-mon",
+ },
+ {
+ name: _("Log Analysis"),
+ icon: <MonitoringIcon />,
+ id: "log-analysis",
+ item: "log-analysis",
+ type: "repl-mon",
+ }
+ ],
+ },
+ {
+ name: _("Database"),
+ icon: <DatabaseIcon />,
+ id: "database-monitor",
+ type: "database",
+ children: [], // Will be populated with treeData on success
+ defaultExpanded: true,
+ },
+ {
+ name: _("Logging"),
+ icon: <CatalogIcon />,
+ id: "log-monitor",
+ defaultExpanded: true,
+ children: [
+ {
+ name: _("Access Log"),
+ icon: <BookIcon size="sm" />,
+ id: "access-log-monitor",
+ type: "log",
+ },
+ {
+ name: _("Audit Log"),
+ icon: <BookIcon size="sm" />,
+ id: "audit-log-monitor",
+ type: "log",
+ },
+ {
+ name: _("Audit Failure Log"),
+ icon: <BookIcon size="sm" />,
+ id: "auditfail-log-monitor",
+ type: "log",
+ },
+ {
+ name: _("Errors Log"),
+ icon: <BookIcon size="sm" />,
+ id: "error-log-monitor",
+ type: "log",
+ },
+ {
+ name: _("Security Log"),
+ icon: <BookIcon size="sm" />,
+ id: "security-log-monitor",
+ type: "log",
+ },
+ ]
+ },
+ ];
+
const cmd = [
"dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
"backend", "get-tree",
@@ -210,83 +288,7 @@ export class Monitor extends React.Component {
.done(content => {
const treeData = JSON.parse(content);
this.processTree(treeData);
- const basicData = [
- {
- name: _("Server Statistics"),
- icon: <ClusterIcon />,
- id: "server-monitor",
- type: "server",
- },
- {
- name: _("Replication"),
- icon: <TopologyIcon />,
- id: "replication-monitor",
- type: "replication",
- defaultExpanded: true,
- children: [
- {
- name: _("Synchronization Report"),
- icon: <MonitoringIcon />,
- id: "sync-report",
- item: "sync-report",
- type: "repl-mon",
- },
- {
- name: _("Log Analysis"),
- icon: <MonitoringIcon />,
- id: "log-analysis",
- item: "log-analysis",
- type: "repl-mon",
- }
- ],
- },
- {
- name: _("Database"),
- icon: <DatabaseIcon />,
- id: "database-monitor",
- type: "database",
- children: [],
- defaultExpanded: true,
- },
- {
- name: _("Logging"),
- icon: <CatalogIcon />,
- id: "log-monitor",
- defaultExpanded: true,
- children: [
- {
- name: _("Access Log"),
- icon: <BookIcon size="sm" />,
- id: "access-log-monitor",
- type: "log",
- },
- {
- name: _("Audit Log"),
- icon: <BookIcon size="sm" />,
- id: "audit-log-monitor",
- type: "log",
- },
- {
- name: _("Audit Failure Log"),
- icon: <BookIcon size="sm" />,
- id: "auditfail-log-monitor",
- type: "log",
- },
- {
- name: _("Errors Log"),
- icon: <BookIcon size="sm" />,
- id: "error-log-monitor",
- type: "log",
- },
- {
- name: _("Security Log"),
- icon: <BookIcon size="sm" />,
- id: "security-log-monitor",
- type: "log",
- },
- ]
- },
- ];
+
let current_node = this.state.node_name;
let type = this.state.node_type;
if (fullReset) {
@@ -296,6 +298,22 @@ export class Monitor extends React.Component {
basicData[2].children = treeData; // database node
this.processReplSuffixes(basicData[1].children);
+ this.setState(() => ({
+ nodes: basicData,
+ node_name: current_node,
+ node_type: type,
+ }), this.update_tree_nodes);
+ })
+ .fail(err => {
+ // Handle backend get-tree failure gracefully
+ let current_node = this.state.node_name;
+ let type = this.state.node_type;
+ if (fullReset) {
+ current_node = "server-monitor";
+ type = "server";
+ }
+ this.processReplSuffixes(basicData[1].children);
+
this.setState(() => ({
nodes: basicData,
node_name: current_node,
diff --git a/src/cockpit/389-console/src/replication.jsx b/src/cockpit/389-console/src/replication.jsx
index fa492fd2a..aa535bfc7 100644
--- a/src/cockpit/389-console/src/replication.jsx
+++ b/src/cockpit/389-console/src/replication.jsx
@@ -177,6 +177,16 @@ export class Replication extends React.Component {
loaded: false
});
+ const basicData = [
+ {
+ name: _("Suffixes"),
+ icon: <TopologyIcon />,
+ id: "repl-suffixes",
+ children: [],
+ defaultExpanded: true
+ }
+ ];
+
const cmd = [
"dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
"backend", "get-tree",
@@ -199,15 +209,7 @@ export class Replication extends React.Component {
}
}
}
- const basicData = [
- {
- name: _("Suffixes"),
- icon: <TopologyIcon />,
- id: "repl-suffixes",
- children: [],
- defaultExpanded: true
- }
- ];
+
let current_node = this.state.node_name;
let current_type = this.state.node_type;
let replicated = this.state.node_replicated;
@@ -258,6 +260,19 @@ export class Replication extends React.Component {
}
basicData[0].children = treeData;
+ this.setState({
+ nodes: basicData,
+ node_name: current_node,
+ node_type: current_type,
+ node_replicated: replicated,
+ }, () => { this.update_tree_nodes() });
+ })
+ .fail(err => {
+ // Handle backend get-tree failure gracefully
+ let current_node = this.state.node_name;
+ let current_type = this.state.node_type;
+ let replicated = this.state.node_replicated;
+
this.setState({
nodes: basicData,
node_name: current_node,
@@ -905,18 +920,18 @@ export class Replication extends React.Component {
disableTree: false
});
});
- })
- .fail(err => {
- const errMsg = JSON.parse(err);
- this.props.addNotification(
- "error",
- cockpit.format(_("Error loading replication agreements configuration - $0"), errMsg.desc)
- );
- this.setState({
- suffixLoading: false,
- disableTree: false
+ })
+ .fail(err => {
+ const errMsg = JSON.parse(err);
+ this.props.addNotification(
+ "error",
+ cockpit.format(_("Error loading replication agreements configuration - $0"), errMsg.desc)
+ );
+ this.setState({
+ suffixLoading: false,
+ disableTree: false
+ });
});
- });
})
.fail(err => {
// changelog failure
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 1319fa0cd..5bff61c58 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -694,24 +694,32 @@ class Backend(DSLdapObject):
parent_suffix = properties.pop('parent', False)
# Okay, now try to make the backend.
- super(Backend, self).create(dn, properties, basedn)
+ backend_obj = super(Backend, self).create(dn, properties, basedn)
# We check if the mapping tree exists in create, so do this *after*
if create_mapping_tree is True:
- properties = {
+ mapping_tree_properties = {
'cn': self._nprops_stash['nsslapd-suffix'],
'nsslapd-state': 'backend',
'nsslapd-backend': self._nprops_stash['cn'],
}
if parent_suffix:
# This is a subsuffix, set the parent suffix
- properties['nsslapd-parent-suffix'] = parent_suffix
- self._mts.create(properties=properties)
+ mapping_tree_properties['nsslapd-parent-suffix'] = parent_suffix
+
+ try:
+ self._mts.create(properties=mapping_tree_properties)
+ except Exception as e:
+ try:
+ backend_obj.delete()
+ except Exception as cleanup_error:
+ self._instance.log.error(f"Failed to cleanup backend after mapping tree creation failure: {cleanup_error}")
+ raise e
# We can't create the sample entries unless a mapping tree was installed.
if sample_entries is not False and create_mapping_tree is True:
self.create_sample_entries(sample_entries)
- return self
+ return backend_obj
def delete(self):
"""Deletes the backend, it's mapping tree and all related indices.
--
2.49.0

View File

@ -0,0 +1,45 @@
From 0a7fe7c6e18759459499f468443ded4313ebdeab Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Wed, 9 Jul 2025 12:08:09 +0300
Subject: [PATCH] Issue 6857 - uiduniq: allow specifying match rules in the
filter
Allow uniqueness plugin to work with attributes where uniqueness should
be enforced using different matching rule than the one defined for the
attribute itself.
Since uniqueness plugin configuration can contain multiple attributes,
add matching rule right to the attribute as it is used in the LDAP rule
(e.g. 'attribute:caseIgnoreMatch:' to force 'attribute' to be searched
with case-insensitive matching rule instead of the original matching
rule.
Fixes: https://github.com/389ds/389-ds-base/issues/6857
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
---
ldap/servers/plugins/uiduniq/uid.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/ldap/servers/plugins/uiduniq/uid.c b/ldap/servers/plugins/uiduniq/uid.c
index 053af4f9d..887e79d78 100644
--- a/ldap/servers/plugins/uiduniq/uid.c
+++ b/ldap/servers/plugins/uiduniq/uid.c
@@ -1030,7 +1030,14 @@ preop_add(Slapi_PBlock *pb)
}
for (i = 0; attrNames && attrNames[i]; i++) {
+ char *attr_match = strchr(attrNames[i], ':');
+ if (attr_match != NULL) {
+ attr_match[0] = '\0';
+ }
err = slapi_entry_attr_find(e, attrNames[i], &attr);
+ if (attr_match != NULL) {
+ attr_match[0] = ':';
+ }
if (!err) {
/*
* Passed all the requirements - this is an operation we
--
2.49.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,399 @@
From 5198da59d622dbc39afe2ece9c6f40f4fb249d52 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 9 Jul 2025 14:18:50 -0400
Subject: [PATCH] Issue 6859 - str2filter is not fully applying matching rules
Description:
When we have an extended filter, one with a MR applied, it is ignored during
internal searches:
"(cn:CaseExactMatch:=Value)"
For internal searches we use str2filter() and it doesn't fully apply extended
search filter matching rules
Also needed to update attr uniqueness plugin to apply this change for mod
operations (previously only Adds were correctly handling these attribute
filters)
Relates: https://github.com/389ds/389-ds-base/issues/6857
Relates: https://github.com/389ds/389-ds-base/issues/6859
Reviewed by: spichugi & tbordaz(Thanks!!)
---
.../tests/suites/plugins/attruniq_test.py | 295 +++++++++++++++++-
ldap/servers/plugins/uiduniq/uid.c | 7 +
ldap/servers/slapd/plugin_mr.c | 2 +-
ldap/servers/slapd/str2filter.c | 8 +
4 files changed, 309 insertions(+), 3 deletions(-)
diff --git a/dirsrvtests/tests/suites/plugins/attruniq_test.py b/dirsrvtests/tests/suites/plugins/attruniq_test.py
index b190e0ec1..b338f405f 100644
--- a/dirsrvtests/tests/suites/plugins/attruniq_test.py
+++ b/dirsrvtests/tests/suites/plugins/attruniq_test.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2021 Red Hat, Inc.
+# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -80,4 +80,295 @@ def test_modrdn_attr_uniqueness(topology_st):
log.debug(excinfo.value)
log.debug('Move user2 to group1')
- user2.rename(f'uid={user2.rdn}', group1.dn)
\ No newline at end of file
+
+ user2.rename(f'uid={user2.rdn}', group1.dn)
+
+ # Cleanup for next test
+ user1.delete()
+ user2.delete()
+ attruniq.disable()
+ attruniq.delete()
+
+
+def test_multiple_attr_uniqueness(topology_st):
+ """ Test that attribute uniqueness works properly with multiple attributes
+
+ :id: c49aa5c1-7e65-45fd-b064-55e0b815e9bc
+ :setup: Standalone instance
+ :steps:
+ 1. Setup attribute uniqueness plugin to ensure uniqueness of attributes 'mail' and 'mailAlternateAddress'
+ 2. Add user with unique 'mail=non-uniq@value.net' and 'mailAlternateAddress=alt-mail@value.net'
+ 3. Try adding another user with 'mail=non-uniq@value.net'
+ 4. Try adding another user with 'mailAlternateAddress=alt-mail@value.net'
+ 5. Try adding another user with 'mail=alt-mail@value.net'
+ 6. Try adding another user with 'mailAlternateAddress=non-uniq@value.net'
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Should raise CONSTRAINT_VIOLATION
+ 4. Should raise CONSTRAINT_VIOLATION
+ 5. Should raise CONSTRAINT_VIOLATION
+ 6. Should raise CONSTRAINT_VIOLATION
+ """
+ attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
+
+ try:
+ log.debug(f'Setup PLUGIN_ATTR_UNIQUENESS plugin for {MAIL_ATTR_VALUE} attribute for the group2')
+ attruniq.create(properties={'cn': 'attruniq'})
+ attruniq.add_unique_attribute('mail')
+ attruniq.add_unique_attribute('mailAlternateAddress')
+ attruniq.add_unique_subtree(DEFAULT_SUFFIX)
+ attruniq.enable_all_subtrees()
+ log.debug(f'Enable PLUGIN_ATTR_UNIQUENESS plugin as "ON"')
+ attruniq.enable()
+ except ldap.LDAPError as e:
+ log.fatal('test_multiple_attribute_uniqueness: Failed to configure plugin for "mail": error {}'.format(e.args[0]['desc']))
+ assert False
+
+ topology_st.standalone.restart()
+
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX)
+
+ testuser1 = users.create_test_user(100,100)
+ testuser1.add('objectclass', 'extensibleObject')
+ testuser1.add('mail', MAIL_ATTR_VALUE)
+ testuser1.add('mailAlternateAddress', MAIL_ATTR_VALUE_ALT)
+
+ testuser2 = users.create_test_user(200, 200)
+ testuser2.add('objectclass', 'extensibleObject')
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mail', MAIL_ATTR_VALUE)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mailAlternateAddress', MAIL_ATTR_VALUE_ALT)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mail', MAIL_ATTR_VALUE_ALT)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mailAlternateAddress', MAIL_ATTR_VALUE)
+
+ # Cleanup
+ testuser1.delete()
+ testuser2.delete()
+ attruniq.disable()
+ attruniq.delete()
+
+
+def test_exclude_subtrees(topology_st):
+ """ Test attribute uniqueness with exclude scope
+
+ :id: 43d29a60-40e1-4ebd-b897-6ef9f20e9f27
+ :setup: Standalone instance
+ :steps:
+ 1. Setup and enable attribute uniqueness plugin for telephonenumber unique attribute
+ 2. Create subtrees and test users
+ 3. Add a unique attribute to a user within uniqueness scope
+ 4. Add exclude subtree
+ 5. Try to add existing value attribute to an entry within uniqueness scope
+ 6. Try to add existing value attribute to an entry within exclude scope
+ 7. Remove the attribute from affected entries
+ 8. Add a unique attribute to a user within exclude scope
+ 9. Try to add existing value attribute to an entry within uniqueness scope
+ 10. Try to add existing value attribute to another entry within uniqueness scope
+ 11. Remove the attribute from affected entries
+ 12. Add another exclude subtree
+ 13. Add a unique attribute to a user within uniqueness scope
+ 14. Try to add existing value attribute to an entry within uniqueness scope
+ 15. Try to add existing value attribute to an entry within exclude scope
+ 16. Try to add existing value attribute to an entry within another exclude scope
+ 17. Clean up entries
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Should raise CONSTRAINT_VIOLATION
+ 6. Success
+ 7. Success
+ 8. Success
+ 9. Success
+ 10. Should raise CONSTRAINT_VIOLATION
+ 11. Success
+ 12. Success
+ 13. Success
+ 14. Should raise CONSTRAINT_VIOLATION
+ 15. Success
+ 16. Success
+ 17. Success
+ """
+ log.info('Setup attribute uniqueness plugin')
+ attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
+ attruniq.create(properties={'cn': 'attruniq'})
+ attruniq.add_unique_attribute('telephonenumber')
+ attruniq.add_unique_subtree(DEFAULT_SUFFIX)
+ attruniq.enable_all_subtrees()
+ attruniq.enable()
+ topology_st.standalone.restart()
+
+ log.info('Create subtrees container')
+ containers = nsContainers(topology_st.standalone, DEFAULT_SUFFIX)
+ cont1 = containers.create(properties={'cn': EXCLUDED_CONTAINER_CN})
+ cont2 = containers.create(properties={'cn': EXCLUDED_BIS_CONTAINER_CN})
+ cont3 = containers.create(properties={'cn': ENFORCED_CONTAINER_CN})
+
+ log.info('Create test users')
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX,
+ rdn='cn={}'.format(ENFORCED_CONTAINER_CN))
+ users_excluded = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX,
+ rdn='cn={}'.format(EXCLUDED_CONTAINER_CN))
+ users_excluded2 = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX,
+ rdn='cn={}'.format(EXCLUDED_BIS_CONTAINER_CN))
+
+ user1 = users.create(properties={'cn': USER_1_CN,
+ 'uid': USER_1_CN,
+ 'sn': USER_1_CN,
+ 'uidNumber': '1',
+ 'gidNumber': '11',
+ 'homeDirectory': '/home/{}'.format(USER_1_CN)})
+ user2 = users.create(properties={'cn': USER_2_CN,
+ 'uid': USER_2_CN,
+ 'sn': USER_2_CN,
+ 'uidNumber': '2',
+ 'gidNumber': '22',
+ 'homeDirectory': '/home/{}'.format(USER_2_CN)})
+ user3 = users_excluded.create(properties={'cn': USER_3_CN,
+ 'uid': USER_3_CN,
+ 'sn': USER_3_CN,
+ 'uidNumber': '3',
+ 'gidNumber': '33',
+ 'homeDirectory': '/home/{}'.format(USER_3_CN)})
+ user4 = users_excluded2.create(properties={'cn': USER_4_CN,
+ 'uid': USER_4_CN,
+ 'sn': USER_4_CN,
+ 'uidNumber': '4',
+ 'gidNumber': '44',
+ 'homeDirectory': '/home/{}'.format(USER_4_CN)})
+
+ UNIQUE_VALUE = '1234'
+
+ try:
+ log.info('Create user with unique attribute')
+ user1.add('telephonenumber', UNIQUE_VALUE)
+ assert user1.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Add exclude subtree')
+ attruniq.add_exclude_subtree(EXCLUDED_CONTAINER_DN)
+ topology_st.standalone.restart()
+
+ log.info('Verify an already used attribute value cannot be added within the same subtree')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user2.add('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify an entry with same attribute value can be added within exclude subtree')
+ user3.add('telephonenumber', UNIQUE_VALUE)
+ assert user3.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Cleanup unique attribute values')
+ user1.remove_all('telephonenumber')
+ user3.remove_all('telephonenumber')
+
+ log.info('Add a unique value to an entry in excluded scope')
+ user3.add('telephonenumber', UNIQUE_VALUE)
+ assert user3.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify the same value can be added to an entry within uniqueness scope')
+ user1.add('telephonenumber', UNIQUE_VALUE)
+ assert user1.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify that yet another same value cannot be added to another entry within uniqueness scope')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user2.add('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Cleanup unique attribute values')
+ user1.remove_all('telephonenumber')
+ user3.remove_all('telephonenumber')
+
+ log.info('Add another exclude subtree')
+ attruniq.add_exclude_subtree(EXCLUDED_BIS_CONTAINER_DN)
+ topology_st.standalone.restart()
+
+ user1.add('telephonenumber', UNIQUE_VALUE)
+ log.info('Verify an already used attribute value cannot be added within the same subtree')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user2.add('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify an already used attribute can be added to an entry in exclude scope')
+ user3.add('telephonenumber', UNIQUE_VALUE)
+ assert user3.present('telephonenumber', UNIQUE_VALUE)
+ user4.add('telephonenumber', UNIQUE_VALUE)
+ assert user4.present('telephonenumber', UNIQUE_VALUE)
+
+ finally:
+ log.info('Clean up users, containers and attribute uniqueness plugin')
+ user1.delete()
+ user2.delete()
+ user3.delete()
+ user4.delete()
+ cont1.delete()
+ cont2.delete()
+ cont3.delete()
+ attruniq.disable()
+ attruniq.delete()
+
+
+def test_matchingrule_attr(topology_st):
+ """ Test list extension MR attribute. Check for "cn" using CES (versus it
+ being defined as CIS)
+
+ :id: 5cde4342-6fa3-4225-b23d-0af918981075
+ :setup: Standalone instance
+ :steps:
+ 1. Setup and enable attribute uniqueness plugin to use CN attribute
+ with a matching rule of CaseExactMatch.
+ 2. Add user with CN value is lowercase
+ 3. Add second user with same lowercase CN which should be rejected
+ 4. Add second user with same CN value but with mixed case
+ 5. Modify second user replacing CN value to lc which should be rejected
+
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ """
+
+ inst = topology_st.standalone
+
+ attruniq = AttributeUniquenessPlugin(inst,
+ dn="cn=attribute uniqueness,cn=plugins,cn=config")
+ attruniq.add_unique_attribute('cn:CaseExactMatch:')
+ attruniq.enable_all_subtrees()
+ attruniq.enable()
+ inst.restart()
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ users.create(properties={'cn': "common_name",
+ 'uid': "uid_name",
+ 'sn': "uid_name",
+ 'uidNumber': '1',
+ 'gidNumber': '11',
+ 'homeDirectory': '/home/uid_name'})
+
+ log.info('Add entry with the exact CN value which should be rejected')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ users.create(properties={'cn': "common_name",
+ 'uid': "uid_name2",
+ 'sn': "uid_name2",
+ 'uidNumber': '11',
+ 'gidNumber': '111',
+ 'homeDirectory': '/home/uid_name2'})
+
+ log.info('Add entry with the mixed case CN value which should be allowed')
+ user = users.create(properties={'cn': "Common_Name",
+ 'uid': "uid_name2",
+ 'sn': "uid_name2",
+ 'uidNumber': '11',
+ 'gidNumber': '111',
+ 'homeDirectory': '/home/uid_name2'})
+
+ log.info('Mod entry with exact case CN value which should be rejected')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user.replace('cn', 'common_name')
diff --git a/ldap/servers/plugins/uiduniq/uid.c b/ldap/servers/plugins/uiduniq/uid.c
index 887e79d78..fdb1404a0 100644
--- a/ldap/servers/plugins/uiduniq/uid.c
+++ b/ldap/servers/plugins/uiduniq/uid.c
@@ -1178,6 +1178,10 @@ preop_modify(Slapi_PBlock *pb)
for (; mods && *mods; mods++) {
mod = *mods;
for (i = 0; attrNames && attrNames[i]; i++) {
+ char *attr_match = strchr(attrNames[i], ':');
+ if (attr_match != NULL) {
+ attr_match[0] = '\0';
+ }
if ((slapi_attr_type_cmp(mod->mod_type, attrNames[i], 1) == 0) && /* mod contains target attr */
(mod->mod_op & LDAP_MOD_BVALUES) && /* mod is bval encoded (not string val) */
(mod->mod_bvalues && mod->mod_bvalues[0]) && /* mod actually contains some values */
@@ -1186,6 +1190,9 @@ preop_modify(Slapi_PBlock *pb)
{
addMod(&checkmods, &checkmodsCapacity, &modcount, mod);
}
+ if (attr_match != NULL) {
+ attr_match[0] = ':';
+ }
}
}
if (modcount == 0) {
diff --git a/ldap/servers/slapd/plugin_mr.c b/ldap/servers/slapd/plugin_mr.c
index b262820c5..67051a5ff 100644
--- a/ldap/servers/slapd/plugin_mr.c
+++ b/ldap/servers/slapd/plugin_mr.c
@@ -626,7 +626,7 @@ attempt_mr_filter_create(mr_filter_t *f, struct slapdplugin *mrp, Slapi_PBlock *
int rc;
IFP mrf_create = NULL;
f->mrf_match = NULL;
- pblock_init(pb);
+ slapi_pblock_init(pb);
if (!(rc = slapi_pblock_set(pb, SLAPI_PLUGIN, mrp)) &&
!(rc = slapi_pblock_get(pb, SLAPI_PLUGIN_MR_FILTER_CREATE_FN, &mrf_create)) &&
mrf_create != NULL &&
diff --git a/ldap/servers/slapd/str2filter.c b/ldap/servers/slapd/str2filter.c
index 9fdc500f7..5620b7439 100644
--- a/ldap/servers/slapd/str2filter.c
+++ b/ldap/servers/slapd/str2filter.c
@@ -344,6 +344,14 @@ str2simple(char *str, int unescape_filter)
return NULL; /* error */
} else {
f->f_choice = LDAP_FILTER_EXTENDED;
+ if (f->f_mr_oid) {
+ /* apply the MR indexers */
+ rc = plugin_mr_filter_create(&f->f_mr);
+ if (rc) {
+ slapi_filter_free(f, 1);
+ return NULL; /* error */
+ }
+ }
}
} else if (str_find_star(value) == NULL) {
f->f_choice = LDAP_FILTER_EQUALITY;
--
2.49.0

View File

@ -0,0 +1,163 @@
From 406563c136d78235751e34a3c7e22ccaf114f754 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Tue, 15 Jul 2025 17:56:18 -0400
Subject: [PATCH] Issue 6872 - compressed log rotation creates files with world
readable permission
Description:
When compressing a log file, first create the empty file using open()
so we can set the correct permissions right from the start. gzopen()
always uses permission 644 and that is not safe. So after creating it
with open(), with the correct permissions, then pass the FD to gzdopen()
and write the compressed content.
relates: https://github.com/389ds/389-ds-base/issues/6872
Reviewed by: progier(Thanks!)
---
.../logging/logging_compression_test.py | 15 ++++++++--
ldap/servers/slapd/log.c | 28 +++++++++++++------
ldap/servers/slapd/schema.c | 2 +-
3 files changed, 33 insertions(+), 12 deletions(-)
diff --git a/dirsrvtests/tests/suites/logging/logging_compression_test.py b/dirsrvtests/tests/suites/logging/logging_compression_test.py
index e30874cc0..3a987d62c 100644
--- a/dirsrvtests/tests/suites/logging/logging_compression_test.py
+++ b/dirsrvtests/tests/suites/logging/logging_compression_test.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2022 Red Hat, Inc.
+# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -22,12 +22,21 @@ log = logging.getLogger(__name__)
pytestmark = pytest.mark.tier1
+
def log_rotated_count(log_type, log_dir, check_compressed=False):
- # Check if the log was rotated
+ """
+ Check if the log was rotated and has the correct permissions
+ """
log_file = f'{log_dir}/{log_type}.2*'
if check_compressed:
log_file += ".gz"
- return len(glob.glob(log_file))
+ log_files = glob.glob(log_file)
+ for logf in log_files:
+ # Check permissions
+ st = os.stat(logf)
+ assert oct(st.st_mode) == '0o100600' # 0600
+
+ return len(log_files)
def update_and_sleep(inst, suffix, sleep=True):
diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
index a018ca2d5..178d29b89 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -172,17 +172,28 @@ get_syslog_loglevel(int loglevel)
}
static int
-compress_log_file(char *log_name)
+compress_log_file(char *log_name, int32_t mode)
{
char gzip_log[BUFSIZ] = {0};
char buf[LOG_CHUNK] = {0};
size_t bytes_read = 0;
gzFile outfile = NULL;
FILE *source = NULL;
+ int fd = 0;
PR_snprintf(gzip_log, sizeof(gzip_log), "%s.gz", log_name);
- if ((outfile = gzopen(gzip_log,"wb")) == NULL) {
- /* Failed to open new gzip file */
+
+ /*
+ * Try to open the file as we may have an incorrect path. We also need to
+ * set the permissions using open() as gzopen() creates the file with
+ * 644 permissions (world readable - bad). So we create an empty file with
+ * the correct permissions, then we pass the FD to gzdopen() to write the
+ * compressed content.
+ */
+ if ((fd = open(gzip_log, O_WRONLY|O_CREAT|O_TRUNC, mode)) >= 0) {
+ /* FIle successfully created, now pass the FD to gzdopen() */
+ outfile = gzdopen(fd, "ab");
+ } else {
return -1;
}
@@ -191,6 +202,7 @@ compress_log_file(char *log_name)
gzclose(outfile);
return -1;
}
+
bytes_read = fread(buf, 1, LOG_CHUNK, source);
while (bytes_read > 0) {
int bytes_written = gzwrite(outfile, buf, bytes_read);
@@ -3291,7 +3303,7 @@ log__open_accesslogfile(int logfile_state, int locked)
return LOG_UNABLE_TO_OPENFILE;
}
} else if (loginfo.log_access_compress) {
- if (compress_log_file(newfile) != 0) {
+ if (compress_log_file(newfile, loginfo.log_access_mode) != 0) {
slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile",
"failed to compress rotated access log (%s)\n",
newfile);
@@ -3455,7 +3467,7 @@ log__open_securitylogfile(int logfile_state, int locked)
return LOG_UNABLE_TO_OPENFILE;
}
} else if (loginfo.log_security_compress) {
- if (compress_log_file(newfile) != 0) {
+ if (compress_log_file(newfile, loginfo.log_security_mode) != 0) {
slapi_log_err(SLAPI_LOG_ERR, "log__open_securitylogfile",
"failed to compress rotated security audit log (%s)\n",
newfile);
@@ -6172,7 +6184,7 @@ log__open_errorlogfile(int logfile_state, int locked)
return LOG_UNABLE_TO_OPENFILE;
}
} else if (loginfo.log_error_compress) {
- if (compress_log_file(newfile) != 0) {
+ if (compress_log_file(newfile, loginfo.log_error_mode) != 0) {
PR_snprintf(buffer, sizeof(buffer), "Failed to compress errors log file (%s)\n", newfile);
log__error_emergency(buffer, 1, 1);
} else {
@@ -6355,7 +6367,7 @@ log__open_auditlogfile(int logfile_state, int locked)
return LOG_UNABLE_TO_OPENFILE;
}
} else if (loginfo.log_audit_compress) {
- if (compress_log_file(newfile) != 0) {
+ if (compress_log_file(newfile, loginfo.log_audit_mode) != 0) {
slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile",
"failed to compress rotated audit log (%s)\n",
newfile);
@@ -6514,7 +6526,7 @@ log__open_auditfaillogfile(int logfile_state, int locked)
return LOG_UNABLE_TO_OPENFILE;
}
} else if (loginfo.log_auditfail_compress) {
- if (compress_log_file(newfile) != 0) {
+ if (compress_log_file(newfile, loginfo.log_auditfail_mode) != 0) {
slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile",
"failed to compress rotated auditfail log (%s)\n",
newfile);
diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
index a8e6b1210..9ef4ee4bf 100644
--- a/ldap/servers/slapd/schema.c
+++ b/ldap/servers/slapd/schema.c
@@ -903,7 +903,7 @@ oc_check_allowed_sv(Slapi_PBlock *pb, Slapi_Entry *e, const char *type, struct o
if (pb) {
PR_snprintf(errtext, sizeof(errtext),
- "attribute \"%s\" not allowed\n",
+ "attribute \"%s\" not allowed",
escape_string(type, ebuf));
slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, errtext);
}
--
2.49.0

View File

@ -0,0 +1,116 @@
From 9b8b23f6d46f16fbc1784b26cfc04dd6b4fa94e1 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 18 Jul 2025 18:50:33 -0700
Subject: [PATCH] Issue 6878 - Prevent repeated disconnect logs during shutdown
(#6879)
Description: Avoid logging non-active initialized connections via CONN in disconnect_server_nomutex_ext by adding a check to skip invalid conn=0 with invalid sockets, preventing excessive repeated messages.
Update ds_logs_test.py by adding test_no_repeated_disconnect_messages to verify the fix.
Fixes: https://github.com/389ds/389-ds-base/issues/6878
Reviewed by: @mreynolds389 (Thanks!)
---
.../tests/suites/ds_logs/ds_logs_test.py | 51 ++++++++++++++++++-
ldap/servers/slapd/connection.c | 15 +++---
2 files changed, 59 insertions(+), 7 deletions(-)
diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
index 2c22347bb..b86c72687 100644
--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
+++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
@@ -24,7 +24,7 @@ from lib389.plugins import AutoMembershipPlugin, ReferentialIntegrityPlugin, Aut
from lib389.idm.user import UserAccounts, UserAccount
from lib389.idm.group import Groups
from lib389.idm.organizationalunit import OrganizationalUnits
-from lib389._constants import DEFAULT_SUFFIX, LOG_ACCESS_LEVEL, PASSWORD
+from lib389._constants import DEFAULT_SUFFIX, LOG_ACCESS_LEVEL, PASSWORD, ErrorLog
from lib389.utils import ds_is_older, ds_is_newer
from lib389.config import RSA
from lib389.dseldif import DSEldif
@@ -1435,6 +1435,55 @@ def test_errorlog_buffering(topology_st, request):
assert inst.ds_error_log.match(".*slapd_daemon - slapd started.*")
+def test_no_repeated_disconnect_messages(topology_st):
+ """Test that there are no repeated "Not setting conn 0 to be disconnected: socket is invalid" messages on restart
+
+ :id: 72b5e1ce-2db8-458f-b2cd-0a0b6525f51f
+ :setup: Standalone Instance
+ :steps:
+ 1. Set error log level to CONNECTION
+ 2. Clear existing error logs
+ 3. Restart the server with 30 second timeout
+ 4. Check error log for repeated disconnect messages
+ 5. Verify there are no more than 10 occurrences of the disconnect message
+ :expectedresults:
+ 1. Error log level should be set successfully
+ 2. Error logs should be cleared
+ 3. Server should restart successfully within 30 seconds
+ 4. Error log should be accessible
+ 5. There should be no more than 10 repeated disconnect messages
+ """
+
+ inst = topology_st.standalone
+
+ log.info('Set error log level to CONNECTION')
+ inst.config.loglevel([ErrorLog.CONNECT])
+ current_level = inst.config.get_attr_val_int('nsslapd-errorlog-level')
+ log.info(f'Error log level set to: {current_level}')
+
+ log.info('Clear existing error logs')
+ inst.deleteErrorLogs()
+
+ log.info('Restart the server with 30 second timeout')
+ inst.restart(timeout=30)
+
+ log.info('Check error log for repeated disconnect messages')
+ disconnect_message = "Not setting conn 0 to be disconnected: socket is invalid"
+
+ # Count occurrences of the disconnect message
+ error_log_lines = inst.ds_error_log.readlines()
+ disconnect_count = 0
+
+ for line in error_log_lines:
+ if disconnect_message in line:
+ disconnect_count += 1
+
+ log.info(f'Found {disconnect_count} occurrences of disconnect message')
+
+ log.info('Verify there are no more than 10 occurrences')
+ assert disconnect_count <= 10, f"Found {disconnect_count} repeated disconnect messages, expected <= 10"
+
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/connection.c b/ldap/servers/slapd/connection.c
index bb4fcd77f..2967de15b 100644
--- a/ldap/servers/slapd/connection.c
+++ b/ldap/servers/slapd/connection.c
@@ -2465,12 +2465,15 @@ disconnect_server_nomutex_ext(Connection *conn, PRUint64 opconnid, int opid, PRE
}
} else {
- slapi_log_err(SLAPI_LOG_CONNS, "disconnect_server_nomutex_ext",
- "Not setting conn %d to be disconnected: %s\n",
- conn->c_sd,
- (conn->c_sd == SLAPD_INVALID_SOCKET) ? "socket is invalid" :
- ((conn->c_connid != opconnid) ? "conn id does not match op conn id" :
- ((conn->c_flags & CONN_FLAG_CLOSING) ? "conn is closing" : "unknown")));
+ /* We avoid logging an invalid conn=0 connection as it is not a real connection. */
+ if (!(conn->c_sd == SLAPD_INVALID_SOCKET && conn->c_connid == 0)) {
+ slapi_log_err(SLAPI_LOG_CONNS, "disconnect_server_nomutex_ext",
+ "Not setting conn %d to be disconnected: %s\n",
+ conn->c_sd,
+ (conn->c_sd == SLAPD_INVALID_SOCKET) ? "socket is invalid" :
+ ((conn->c_connid != opconnid) ? "conn id does not match op conn id" :
+ ((conn->c_flags & CONN_FLAG_CLOSING) ? "conn is closing" : "unknown")));
+ }
}
}
--
2.49.0

View File

@ -0,0 +1,67 @@
From fef4875a9c3d67ef424a1fb1698ae011152735b1 Mon Sep 17 00:00:00 2001
From: Anuar Beisembayev <111912342+abeisemb@users.noreply.github.com>
Date: Wed, 23 Jul 2025 23:48:11 -0400
Subject: [PATCH] Issue 6772 - dsconf - Replicas with the "consumer" role allow
for viewing and modification of their changelog. (#6773)
dsconf currently allows users to set and retrieve changelogs in consumer replicas, which do not have officially supported changelogs. This can lead to undefined behavior and confusion.
This commit prints a warning message if the user tries to interact with a changelog on a consumer replica.
Resolves: https://github.com/389ds/389-ds-base/issues/6772
Reviewed by: @droideck
---
src/lib389/lib389/cli_conf/replication.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index 6f77f34ca..a18bf83ca 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -686,6 +686,9 @@ def set_per_backend_cl(inst, basedn, log, args):
replace_list = []
did_something = False
+ if (is_replica_role_consumer(inst, suffix)):
+ log.info("Warning: Changelogs are not supported for consumer replicas. You may run into undefined behavior.")
+
if args.encrypt:
cl.replace('nsslapd-encryptionalgorithm', 'AES')
del args.encrypt
@@ -715,6 +718,10 @@ def set_per_backend_cl(inst, basedn, log, args):
# that means there is a changelog config entry per backend (aka suffix)
def get_per_backend_cl(inst, basedn, log, args):
suffix = args.suffix
+
+ if (is_replica_role_consumer(inst, suffix)):
+ log.info("Warning: Changelogs are not supported for consumer replicas. You may run into undefined behavior.")
+
cl = Changelog(inst, suffix)
if args and args.json:
log.info(cl.get_all_attrs_json())
@@ -822,6 +829,22 @@ def del_repl_manager(inst, basedn, log, args):
log.info("Successfully deleted replication manager: " + manager_dn)
+def is_replica_role_consumer(inst, suffix):
+ """Helper function for get_per_backend_cl and set_per_backend_cl.
+ Makes sure the instance in question is not a consumer, which is a role that
+ does not support changelogs.
+ """
+ replicas = Replicas(inst)
+ try:
+ replica = replicas.get(suffix)
+ role = replica.get_role()
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError(f"Backend \"{suffix}\" is not enabled for replication")
+
+ if role == ReplicaRole.CONSUMER:
+ return True
+ else:
+ return False
#
# Agreements
--
2.49.0

View File

@ -0,0 +1,143 @@
From 4cb50f83397e6a5e14a9b75ed15f24189ee2792b Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 21 Jul 2025 18:07:21 -0400
Subject: [PATCH] Issue 6893 - Log user that is updated during password modify
extended operation
Description:
When a user's password is updated via an extended operation (password modify
plugin) we only log the bind DN and not what user was updated. While "internal
operation" logging will display the the user it should be logged by the default
logging level.
Add access logging using "EXT_INFO" where we display the bind dn, target
dn, and message.
Relates: https://github.com/389ds/389-ds-base/issues/6893
Reviewed by: spichugi & tbordaz(Thanks!!)
---
ldap/servers/slapd/passwd_extop.c | 56 +++++++++++++++----------------
1 file changed, 28 insertions(+), 28 deletions(-)
diff --git a/ldap/servers/slapd/passwd_extop.c b/ldap/servers/slapd/passwd_extop.c
index 4bb60afd6..0296d64fb 100644
--- a/ldap/servers/slapd/passwd_extop.c
+++ b/ldap/servers/slapd/passwd_extop.c
@@ -465,12 +465,13 @@ passwd_modify_extop(Slapi_PBlock *pb)
BerElement *response_ber = NULL;
Slapi_Entry *targetEntry = NULL;
Connection *conn = NULL;
+ Operation *pb_op = NULL;
LDAPControl **req_controls = NULL;
LDAPControl **resp_controls = NULL;
passwdPolicy *pwpolicy = NULL;
Slapi_DN *target_sdn = NULL;
Slapi_Entry *referrals = NULL;
- /* Slapi_DN sdn; */
+ Slapi_Backend *be = NULL;
slapi_log_err(SLAPI_LOG_TRACE, "passwd_modify_extop", "=>\n");
@@ -647,7 +648,7 @@ parse_req_done:
}
dn = slapi_sdn_get_ndn(target_sdn);
if (dn == NULL || *dn == '\0') {
- /* Refuse the operation because they're bound anonymously */
+ /* Invalid DN - refuse the operation */
errMesg = "Invalid dn.";
rc = LDAP_INVALID_DN_SYNTAX;
goto free_and_return;
@@ -724,14 +725,19 @@ parse_req_done:
ber_free(response_ber, 1);
}
- slapi_pblock_set(pb, SLAPI_ORIGINAL_TARGET, (void *)dn);
+ slapi_pblock_get(pb, SLAPI_OPERATION, &pb_op);
+ if (pb_op == NULL) {
+ slapi_log_err(SLAPI_LOG_ERR, "passwd_modify_extop", "pb_op is NULL\n");
+ goto free_and_return;
+ }
+ slapi_pblock_set(pb, SLAPI_ORIGINAL_TARGET, (void *)dn);
/* Now we have the DN, look for the entry */
ret = passwd_modify_getEntry(dn, &targetEntry);
/* If we can't find the entry, then that's an error */
if (ret) {
/* Couldn't find the entry, fail */
- errMesg = "No such Entry exists.";
+ errMesg = "No such entry exists.";
rc = LDAP_NO_SUCH_OBJECT;
goto free_and_return;
}
@@ -742,30 +748,18 @@ parse_req_done:
leak any useful information to the client such as current password
wrong, etc.
*/
- Operation *pb_op = NULL;
- slapi_pblock_get(pb, SLAPI_OPERATION, &pb_op);
- if (pb_op == NULL) {
- slapi_log_err(SLAPI_LOG_ERR, "passwd_modify_extop", "pb_op is NULL\n");
- goto free_and_return;
- }
-
operation_set_target_spec(pb_op, slapi_entry_get_sdn(targetEntry));
slapi_pblock_set(pb, SLAPI_REQUESTOR_ISROOT, &pb_op->o_isroot);
- /* In order to perform the access control check , we need to select a backend (even though
- * we don't actually need it otherwise).
- */
- {
- Slapi_Backend *be = NULL;
-
- be = slapi_mapping_tree_find_backend_for_sdn(slapi_entry_get_sdn(targetEntry));
- if (NULL == be) {
- errMesg = "Failed to find backend for target entry";
- rc = LDAP_OPERATIONS_ERROR;
- goto free_and_return;
- }
- slapi_pblock_set(pb, SLAPI_BACKEND, be);
+ /* In order to perform the access control check, we need to select a backend (even though
+ * we don't actually need it otherwise). */
+ be = slapi_mapping_tree_find_backend_for_sdn(slapi_entry_get_sdn(targetEntry));
+ if (NULL == be) {
+ errMesg = "Failed to find backend for target entry";
+ rc = LDAP_NO_SUCH_OBJECT;
+ goto free_and_return;
}
+ slapi_pblock_set(pb, SLAPI_BACKEND, be);
/* Check if the pwpolicy control is present */
slapi_pblock_get(pb, SLAPI_PWPOLICY, &need_pwpolicy_ctrl);
@@ -797,10 +791,7 @@ parse_req_done:
/* Check if password policy allows users to change their passwords. We need to do
* this here since the normal modify code doesn't perform this check for
* internal operations. */
-
- Connection *pb_conn;
- slapi_pblock_get(pb, SLAPI_CONNECTION, &pb_conn);
- if (!pb_op->o_isroot && !pb_conn->c_needpw && !pwpolicy->pw_change) {
+ if (!pb_op->o_isroot && !conn->c_needpw && !pwpolicy->pw_change) {
if (NULL == bindSDN) {
bindSDN = slapi_sdn_new_normdn_byref(bindDN);
}
@@ -848,6 +839,15 @@ free_and_return:
slapi_log_err(SLAPI_LOG_PLUGIN, "passwd_modify_extop",
"%s\n", errMesg ? errMesg : "success");
+ if (dn) {
+ /* Log the target ndn (if we have a target ndn) */
+ slapi_log_access(LDAP_DEBUG_STATS,
+ "conn=%" PRIu64 " op=%d EXT_INFO name=\"passwd_modify_plugin\" bind_dn=\"%s\" target_dn=\"%s\" msg=\"%s\" rc=%d\n",
+ conn ? conn->c_connid : -1, pb_op ? pb_op->o_opid : -1,
+ bindDN ? bindDN : "", dn,
+ errMesg ? errMesg : "success", rc);
+ }
+
if ((rc == LDAP_REFERRAL) && (referrals)) {
send_referrals_from_entry(pb, referrals);
} else {
--
2.49.0

View File

@ -0,0 +1,98 @@
From ffc3a81ed5852b7f1fbaed79b9b776af23d65b7c Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 23 Jul 2025 19:35:32 -0400
Subject: [PATCH] Issue 6895 - Crash if repl keep alive entry can not be
created
Description:
Heap use after free when logging that the replicaton keep-alive entry can not
be created. slapi_add_internal_pb() frees the slapi entry, then
we try and get the dn from the entry and we get a use-after-free crash.
Relates: https://github.com/389ds/389-ds-base/issues/6895
Reviewed by: spichugi(Thanks!)
---
ldap/servers/plugins/chainingdb/cb_config.c | 3 +--
ldap/servers/plugins/posix-winsync/posix-winsync.c | 1 -
ldap/servers/plugins/replication/repl5_init.c | 3 ---
ldap/servers/plugins/replication/repl5_replica.c | 8 ++++----
4 files changed, 5 insertions(+), 10 deletions(-)
diff --git a/ldap/servers/plugins/chainingdb/cb_config.c b/ldap/servers/plugins/chainingdb/cb_config.c
index 40a7088d7..24fa1bcb3 100644
--- a/ldap/servers/plugins/chainingdb/cb_config.c
+++ b/ldap/servers/plugins/chainingdb/cb_config.c
@@ -44,8 +44,7 @@ cb_config_add_dse_entries(cb_backend *cb, char **entries, char *string1, char *s
slapi_pblock_get(util_pb, SLAPI_PLUGIN_INTOP_RESULT, &res);
if (LDAP_SUCCESS != res && LDAP_ALREADY_EXISTS != res) {
slapi_log_err(SLAPI_LOG_ERR, CB_PLUGIN_SUBSYSTEM,
- "cb_config_add_dse_entries - Unable to add config entry (%s) to the DSE: %s\n",
- slapi_entry_get_dn(e),
+ "cb_config_add_dse_entries - Unable to add config entry to the DSE: %s\n",
ldap_err2string(res));
rc = res;
slapi_pblock_destroy(util_pb);
diff --git a/ldap/servers/plugins/posix-winsync/posix-winsync.c b/ldap/servers/plugins/posix-winsync/posix-winsync.c
index 51a55b643..3a002bb70 100644
--- a/ldap/servers/plugins/posix-winsync/posix-winsync.c
+++ b/ldap/servers/plugins/posix-winsync/posix-winsync.c
@@ -1626,7 +1626,6 @@ posix_winsync_end_update_cb(void *cbdata __attribute__((unused)),
"posix_winsync_end_update_cb: "
"add task entry\n");
}
- /* slapi_entry_free(e_task); */
slapi_pblock_destroy(pb);
pb = NULL;
posix_winsync_config_reset_MOFTaskCreated();
diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c
index 8bc0b5372..5047fb8dc 100644
--- a/ldap/servers/plugins/replication/repl5_init.c
+++ b/ldap/servers/plugins/replication/repl5_init.c
@@ -682,7 +682,6 @@ create_repl_schema_policy(void)
repl_schema_top,
ldap_err2string(return_value));
rc = -1;
- slapi_entry_free(e); /* The entry was not consumed */
goto done;
}
slapi_pblock_destroy(pb);
@@ -703,7 +702,6 @@ create_repl_schema_policy(void)
repl_schema_supplier,
ldap_err2string(return_value));
rc = -1;
- slapi_entry_free(e); /* The entry was not consumed */
goto done;
}
slapi_pblock_destroy(pb);
@@ -724,7 +722,6 @@ create_repl_schema_policy(void)
repl_schema_consumer,
ldap_err2string(return_value));
rc = -1;
- slapi_entry_free(e); /* The entry was not consumed */
goto done;
}
slapi_pblock_destroy(pb);
diff --git a/ldap/servers/plugins/replication/repl5_replica.c b/ldap/servers/plugins/replication/repl5_replica.c
index 59062b46b..a97c807e9 100644
--- a/ldap/servers/plugins/replication/repl5_replica.c
+++ b/ldap/servers/plugins/replication/repl5_replica.c
@@ -465,10 +465,10 @@ replica_subentry_create(const char *repl_root, ReplicaId rid)
if (return_value != LDAP_SUCCESS &&
return_value != LDAP_ALREADY_EXISTS &&
return_value != LDAP_REFERRAL /* CONSUMER */) {
- slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_subentry_create - Unable to "
- "create replication keep alive entry %s: error %d - %s\n",
- slapi_entry_get_dn_const(e),
- return_value, ldap_err2string(return_value));
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_subentry_create - "
+ "Unable to create replication keep alive entry 'cn=%s %d,%s': error %d - %s\n",
+ KEEP_ALIVE_ENTRY, rid, repl_root,
+ return_value, ldap_err2string(return_value));
rc = -1;
goto done;
}
--
2.49.0

View File

@ -0,0 +1,352 @@
From 191634746fdcb7e26a154cd00a22324e02a10110 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Mon, 28 Jul 2025 10:50:26 -0700
Subject: [PATCH] Issue 6250 - Add test for entryUSN overflow on failed add
operations (#6821)
Description: Add comprehensive test to reproduce the entryUSN
overflow issue where failed attempts to add existing entries followed by
modify operations cause entryUSN values to underflow/overflow instead of
incrementing properly.
Related: https://github.com/389ds/389-ds-base/issues/6250
Reviewed by: @tbordaz (Thanks!)
---
.../suites/plugins/entryusn_overflow_test.py | 323 ++++++++++++++++++
1 file changed, 323 insertions(+)
create mode 100644 dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py
diff --git a/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py
new file mode 100644
index 000000000..a23d734ca
--- /dev/null
+++ b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py
@@ -0,0 +1,323 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import ldap
+import logging
+import pytest
+import time
+import random
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.config import Config
+from lib389.plugins import USNPlugin
+from lib389.idm.user import UserAccounts
+from lib389.topologies import topology_st
+from lib389.rootdse import RootDSE
+
+pytestmark = pytest.mark.tier2
+
+log = logging.getLogger(__name__)
+
+# Test constants
+DEMO_USER_BASE_DN = "uid=demo_user,ou=people," + DEFAULT_SUFFIX
+TEST_USER_PREFIX = "Demo User"
+MAX_USN_64BIT = 18446744073709551615 # 2^64 - 1
+ITERATIONS = 10
+ADD_EXISTING_ENTRY_MAX_ATTEMPTS = 5
+
+
+@pytest.fixture(scope="module")
+def setup_usn_test(topology_st, request):
+ """Setup USN plugin and test data for entryUSN overflow testing"""
+
+ inst = topology_st.standalone
+
+ log.info("Enable the USN plugin...")
+ plugin = USNPlugin(inst)
+ plugin.enable()
+ plugin.enable_global_mode()
+
+ inst.restart()
+
+ # Create initial test users
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ created_users = []
+
+ log.info("Creating initial test users...")
+ for i in range(3):
+ user_props = {
+ 'uid': f'{TEST_USER_PREFIX}-{i}',
+ 'cn': f'{TEST_USER_PREFIX}-{i}',
+ 'sn': f'User{i}',
+ 'uidNumber': str(1000 + i),
+ 'gidNumber': str(1000 + i),
+ 'homeDirectory': f'/home/{TEST_USER_PREFIX}-{i}',
+ 'userPassword': 'password123'
+ }
+ try:
+ user = users.create(properties=user_props)
+ created_users.append(user)
+ log.info(f"Created user: {user.dn}")
+ except ldap.ALREADY_EXISTS:
+ log.info(f"User {user_props['uid']} already exists, skipping creation")
+ user = users.get(user_props['uid'])
+ created_users.append(user)
+
+ def fin():
+ log.info("Cleaning up test users...")
+ for user in created_users:
+ try:
+ user.delete()
+ except ldap.NO_SUCH_OBJECT:
+ pass
+
+ request.addfinalizer(fin)
+
+ return created_users
+
+
+def test_entryusn_overflow_on_add_existing_entries(topology_st, setup_usn_test):
+ """Test that reproduces entryUSN overflow when adding existing entries
+
+ :id: a5a8c33d-82f3-4113-be2b-027de51791c8
+ :setup: Standalone instance with USN plugin enabled and test users
+ :steps:
+ 1. Record initial entryUSN values for existing users
+ 2. Attempt to add existing entries multiple times (should fail)
+ 3. Perform modify operations on the entries
+ 4. Check that entryUSN values increment correctly without overflow
+ 5. Verify lastusn values are consistent
+ :expectedresults:
+ 1. Initial entryUSN values are recorded successfully
+ 2. Add operations fail with ALREADY_EXISTS error
+ 3. Modify operations succeed
+ 4. EntryUSN values increment properly without underflow/overflow
+ 5. LastUSN values are consistent and increasing
+ """
+
+ inst = topology_st.standalone
+ users = setup_usn_test
+
+ # Enable detailed logging for debugging
+ config = Config(inst)
+ config.replace('nsslapd-accesslog-level', '260') # Internal op logging
+ config.replace('nsslapd-errorlog-level', '65536')
+ config.replace('nsslapd-plugin-logging', 'on')
+
+ root_dse = RootDSE(inst)
+
+ log.info("Starting entryUSN overflow reproduction test")
+
+ # Record initial state
+ initial_usn_values = {}
+ for user in users:
+ initial_usn = user.get_attr_val_int('entryusn')
+ initial_usn_values[user.dn] = initial_usn
+ log.info(f"Initial entryUSN for {user.get_attr_val_utf8('cn')}: {initial_usn}")
+
+ initial_lastusn = root_dse.get_attr_val_int("lastusn")
+ log.info(f"Initial lastUSN: {initial_lastusn}")
+
+ # Perform test iterations
+ for iteration in range(1, ITERATIONS + 1):
+ log.info(f"\n--- Iteration {iteration} ---")
+
+ # Step 1: Try to add existing entries multiple times
+ selected_user = random.choice(users)
+ cn_value = selected_user.get_attr_val_utf8('cn')
+ attempts = random.randint(1, ADD_EXISTING_ENTRY_MAX_ATTEMPTS)
+
+ log.info(f"Attempting to add existing entry '{cn_value}' {attempts} times")
+
+ # Get user attributes for recreation attempt
+ user_attrs = {
+ 'uid': selected_user.get_attr_val_utf8('uid'),
+ 'cn': selected_user.get_attr_val_utf8('cn'),
+ 'sn': selected_user.get_attr_val_utf8('sn'),
+ 'uidNumber': selected_user.get_attr_val_utf8('uidNumber'),
+ 'gidNumber': selected_user.get_attr_val_utf8('gidNumber'),
+ 'homeDirectory': selected_user.get_attr_val_utf8('homeDirectory'),
+ 'userPassword': 'password123'
+ }
+
+ users_collection = UserAccounts(inst, DEFAULT_SUFFIX)
+
+ # Try to add the existing user multiple times
+ for attempt in range(attempts):
+ try:
+ users_collection.create(properties=user_attrs)
+ log.error(f"ERROR: Add operation should have failed but succeeded on attempt {attempt + 1}")
+ assert False, "Add operation should have failed with ALREADY_EXISTS"
+ except ldap.ALREADY_EXISTS:
+ log.info(f"Attempt {attempt + 1}: Got expected ALREADY_EXISTS error")
+ except Exception as e:
+ log.error(f"Unexpected error on attempt {attempt + 1}: {e}")
+ raise
+
+ # Step 2: Perform modify operation
+ target_user = random.choice(users)
+ cn_value = target_user.get_attr_val_utf8('cn')
+ old_usn = target_user.get_attr_val_int('entryusn')
+
+ # Modify the user entry
+ new_description = f"Modified in iteration {iteration} - {time.time()}"
+ target_user.replace('description', new_description)
+
+ # Get new USN value
+ new_usn = target_user.get_attr_val_int('entryusn')
+
+ log.info(f"Modified entry '{cn_value}': old USN = {old_usn}, new USN = {new_usn}")
+
+ # Step 3: Validate USN values
+ # Check for overflow/underflow conditions
+ assert new_usn > 0, f"EntryUSN should be positive, got {new_usn}"
+ assert new_usn < MAX_USN_64BIT, f"EntryUSN overflow detected: {new_usn} >= {MAX_USN_64BIT}"
+
+ # Check that USN didn't wrap around (underflow detection)
+ usn_diff = new_usn - old_usn
+ assert usn_diff < 1000, f"USN increment too large, possible overflow: {usn_diff}"
+
+ # Verify lastUSN is also reasonable
+ current_lastusn = root_dse.get_attr_val_int("lastusn")
+ assert current_lastusn >= new_usn, f"LastUSN ({current_lastusn}) should be >= entryUSN ({new_usn})"
+ assert current_lastusn < MAX_USN_64BIT, f"LastUSN overflow detected: {current_lastusn}"
+
+ log.info(f"USN validation passed for iteration {iteration}")
+
+ # Add a new entry occasionally to increase USN diversity
+ if iteration % 3 == 0:
+ new_user_props = {
+ 'uid': f'{TEST_USER_PREFIX}-new-{iteration}',
+ 'cn': f'{TEST_USER_PREFIX}-new-{iteration}',
+ 'sn': f'NewUser{iteration}',
+ 'uidNumber': str(2000 + iteration),
+ 'gidNumber': str(2000 + iteration),
+ 'homeDirectory': f'/home/{TEST_USER_PREFIX}-new-{iteration}',
+ 'userPassword': 'newpassword123'
+ }
+ try:
+ new_user = users_collection.create(properties=new_user_props)
+ new_user_usn = new_user.get_attr_val_int('entryusn')
+ log.info(f"Created new entry '{new_user.get_attr_val_utf8('cn')}' with USN: {new_user_usn}")
+ users.append(new_user) # Add to cleanup list
+ except Exception as e:
+ log.warning(f"Failed to create new user in iteration {iteration}: {e}")
+
+ # Final validation: Check all USN values are reasonable
+ log.info("\nFinal USN validation")
+ final_lastusn = root_dse.get_attr_val_int("lastusn")
+
+ for user in users:
+ try:
+ final_usn = user.get_attr_val_int('entryusn')
+ cn_value = user.get_attr_val_utf8('cn')
+ log.info(f"Final entryUSN for '{cn_value}': {final_usn}")
+
+ # Ensure no overflow occurred
+ assert final_usn > 0, f"Final entryUSN should be positive for {cn_value}: {final_usn}"
+ assert final_usn < MAX_USN_64BIT, f"EntryUSN overflow for {cn_value}: {final_usn}"
+
+ except ldap.NO_SUCH_OBJECT:
+ log.info(f"User {user.dn} was deleted during test")
+
+ log.info(f"Final lastUSN: {final_lastusn}")
+ assert final_lastusn > initial_lastusn, "LastUSN should have increased during test"
+ assert final_lastusn < MAX_USN_64BIT, f"LastUSN overflow detected: {final_lastusn}"
+
+ log.info("EntryUSN overflow test completed successfully")
+
+
+def test_entryusn_consistency_after_failed_adds(topology_st, setup_usn_test):
+ """Test that entryUSN remains consistent after failed add operations
+
+ :id: e380ccad-527b-427e-a331-df5c41badbed
+ :setup: Standalone instance with USN plugin enabled and test users
+ :steps:
+ 1. Record entryUSN values before failed add attempts
+ 2. Attempt to add existing entries (should fail)
+ 3. Verify entryUSN values haven't changed due to failed operations
+ 4. Perform successful modify operations
+ 5. Verify entryUSN increments correctly
+ :expectedresults:
+ 1. Initial entryUSN values recorded
+ 2. Add operations fail as expected
+ 3. EntryUSN values unchanged after failed adds
+ 4. Modify operations succeed
+ 5. EntryUSN values increment correctly without overflow
+ """
+
+ inst = topology_st.standalone
+ users = setup_usn_test
+
+ log.info("Testing entryUSN consistency after failed adds")
+
+ # Record USN values before any operations
+ pre_operation_usns = {}
+ for user in users:
+ usn = user.get_attr_val_int('entryusn')
+ pre_operation_usns[user.dn] = usn
+ log.info(f"Pre-operation entryUSN for {user.get_attr_val_utf8('cn')}: {usn}")
+
+ # Attempt to add existing entries - these should fail
+ users_collection = UserAccounts(inst, DEFAULT_SUFFIX)
+
+ for user in users:
+ cn_value = user.get_attr_val_utf8('cn')
+ log.info(f"Attempting to add existing user: {cn_value}")
+
+ user_attrs = {
+ 'uid': user.get_attr_val_utf8('uid'),
+ 'cn': cn_value,
+ 'sn': user.get_attr_val_utf8('sn'),
+ 'uidNumber': user.get_attr_val_utf8('uidNumber'),
+ 'gidNumber': user.get_attr_val_utf8('gidNumber'),
+ 'homeDirectory': user.get_attr_val_utf8('homeDirectory'),
+ 'userPassword': 'password123'
+ }
+
+ try:
+ users_collection.create(properties=user_attrs)
+ assert False, f"Add operation should have failed for existing user {cn_value}"
+ except ldap.ALREADY_EXISTS:
+ log.info(f"Got expected ALREADY_EXISTS for {cn_value}")
+
+ # Verify USN values haven't changed after failed adds
+ log.info("Verifying entryUSN values after failed add operations...")
+ for user in users:
+ current_usn = user.get_attr_val_int('entryusn')
+ expected_usn = pre_operation_usns[user.dn]
+ cn_value = user.get_attr_val_utf8('cn')
+
+ assert current_usn == expected_usn, \
+ f"EntryUSN changed after failed add for {cn_value}: was {expected_usn}, now {current_usn}"
+ log.info(f"EntryUSN unchanged for {cn_value}: {current_usn}")
+
+ # Now perform successful modify operations
+ log.info("Performing successful modify operations...")
+ for i, user in enumerate(users):
+ cn_value = user.get_attr_val_utf8('cn')
+ old_usn = user.get_attr_val_int('entryusn')
+
+ # Modify the user
+ user.replace('description', f'Consistency test modification {i + 1}')
+
+ new_usn = user.get_attr_val_int('entryusn')
+ log.info(f"Modified {cn_value}: USN {old_usn} -> {new_usn}")
+
+ # Verify proper increment
+ assert (new_usn - old_usn) == 1, f"EntryUSN should increment by 1 for {cn_value}: {old_usn} -> {new_usn}"
+ assert new_usn < MAX_USN_64BIT, f"EntryUSN overflow for {cn_value}: {new_usn}"
+
+ log.info("EntryUSN consistency test completed successfully")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
\ No newline at end of file
--
2.49.0

View File

@ -0,0 +1,172 @@
From 37a56f75afac2805e1ba958eebd496e77b7079e7 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Mon, 28 Jul 2025 15:35:50 -0700
Subject: [PATCH] Issue 6594 - Add test for numSubordinates replication
consistency with tombstones (#6862)
Description: Add a comprehensive test to verify that numSubordinates and
tombstoneNumSubordinates attributes are correctly replicated between
instances when tombstone entries are present.
Fixes: https://github.com/389ds/389-ds-base/issues/6594
Reviewed by: @progier389 (Thanks!)
---
.../numsubordinates_replication_test.py | 144 ++++++++++++++++++
1 file changed, 144 insertions(+)
create mode 100644 dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py
diff --git a/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py
new file mode 100644
index 000000000..9ba10657d
--- /dev/null
+++ b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py
@@ -0,0 +1,144 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import logging
+import pytest
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.replica import ReplicationManager
+from lib389.idm.organizationalunit import OrganizationalUnits
+from lib389.idm.user import UserAccounts
+from lib389.topologies import topology_i2 as topo_i2
+
+
+pytestmark = pytest.mark.tier1
+
+DEBUGGING = os.getenv("DEBUGGING", default=False)
+if DEBUGGING:
+ logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+ logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+
+def test_numsubordinates_tombstone_replication_mismatch(topo_i2):
+ """Test that numSubordinates values match between replicas after tombstone creation
+
+ :id: c43ecc7a-d706-42e8-9179-1ff7d0e7163a
+ :setup: Two standalone instances
+ :steps:
+ 1. Create a container (organizational unit) on the first instance
+ 2. Create a user object in that container
+ 3. Delete the user object (this creates a tombstone)
+ 4. Set up replication between the two instances
+ 5. Wait for replication to complete
+ 6. Check numSubordinates on both instances
+ 7. Check tombstoneNumSubordinates on both instances
+ 8. Verify that numSubordinates values match on both instances
+ :expectedresults:
+ 1. Container should be created successfully
+ 2. User object should be created successfully
+ 3. User object should be deleted successfully
+ 4. Replication should be set up successfully
+ 5. Replication should complete successfully
+ 6. numSubordinates should be accessible on both instances
+ 7. tombstoneNumSubordinates should be accessible on both instances
+ 8. numSubordinates values should match on both instances
+ """
+
+ instance1 = topo_i2.ins["standalone1"]
+ instance2 = topo_i2.ins["standalone2"]
+
+ log.info("Create a container (organizational unit) on the first instance")
+ ous1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX)
+ container = ous1.create(properties={
+ 'ou': 'test_container',
+ 'description': 'Test container for numSubordinates replication test'
+ })
+ container_rdn = container.rdn
+ log.info(f"Created container: {container_rdn}")
+
+ log.info("Create a user object in that container")
+ users1 = UserAccounts(instance1, DEFAULT_SUFFIX, rdn=f"ou={container_rdn}")
+ test_user = users1.create_test_user(uid=1001)
+ log.info(f"Created user: {test_user.dn}")
+
+ log.info("Checking initial numSubordinates on container")
+ container_obj1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX).get(container_rdn)
+ initial_numsubordinates = container_obj1.get_attr_val_int('numSubordinates')
+ log.info(f"Initial numSubordinates: {initial_numsubordinates}")
+ assert initial_numsubordinates == 1
+
+ log.info("Delete the user object (this creates a tombstone)")
+ test_user.delete()
+
+ log.info("Checking numSubordinates after deletion")
+ after_delete_numsubordinates = container_obj1.get_attr_val_int('numSubordinates')
+ log.info(f"numSubordinates after deletion: {after_delete_numsubordinates}")
+
+ log.info("Checking tombstoneNumSubordinates after deletion")
+ try:
+ tombstone_numsubordinates = container_obj1.get_attr_val_int('tombstoneNumSubordinates')
+ log.info(f"tombstoneNumSubordinates: {tombstone_numsubordinates}")
+ except Exception as e:
+ log.info(f"tombstoneNumSubordinates not found or error: {e}")
+ tombstone_numsubordinates = 0
+
+ log.info("Set up replication between the two instances")
+ repl = ReplicationManager(DEFAULT_SUFFIX)
+ repl.create_first_supplier(instance1)
+ repl.join_supplier(instance1, instance2)
+
+ log.info("Wait for replication to complete")
+ repl.wait_for_replication(instance1, instance2)
+
+ log.info("Check numSubordinates on both instances")
+ container_obj1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX).get(container_rdn)
+ numsubordinates_instance1 = container_obj1.get_attr_val_int('numSubordinates')
+ log.info(f"numSubordinates on instance1: {numsubordinates_instance1}")
+
+ container_obj2 = OrganizationalUnits(instance2, DEFAULT_SUFFIX).get(container_rdn)
+ numsubordinates_instance2 = container_obj2.get_attr_val_int('numSubordinates')
+ log.info(f"numSubordinates on instance2: {numsubordinates_instance2}")
+
+ log.info("Check tombstoneNumSubordinates on both instances")
+ try:
+ tombstone_numsubordinates_instance1 = container_obj1.get_attr_val_int('tombstoneNumSubordinates')
+ log.info(f"tombstoneNumSubordinates on instance1: {tombstone_numsubordinates_instance1}")
+ except Exception as e:
+ log.info(f"tombstoneNumSubordinates not found on instance1: {e}")
+ tombstone_numsubordinates_instance1 = 0
+
+ try:
+ tombstone_numsubordinates_instance2 = container_obj2.get_attr_val_int('tombstoneNumSubordinates')
+ log.info(f"tombstoneNumSubordinates on instance2: {tombstone_numsubordinates_instance2}")
+ except Exception as e:
+ log.info(f"tombstoneNumSubordinates not found on instance2: {e}")
+ tombstone_numsubordinates_instance2 = 0
+
+ log.info("Verify that numSubordinates values match on both instances")
+ log.info(f"Comparison: instance1 numSubordinates={numsubordinates_instance1}, "
+ f"instance2 numSubordinates={numsubordinates_instance2}")
+ log.info(f"Comparison: instance1 tombstoneNumSubordinates={tombstone_numsubordinates_instance1}, "
+ f"instance2 tombstoneNumSubordinates={tombstone_numsubordinates_instance2}")
+
+ assert numsubordinates_instance1 == numsubordinates_instance2, (
+ f"numSubordinates mismatch: instance1 has {numsubordinates_instance1}, "
+ f"instance2 has {numsubordinates_instance2}. "
+ )
+ assert tombstone_numsubordinates_instance1 == tombstone_numsubordinates_instance2, (
+ f"tombstoneNumSubordinates mismatch: instance1 has {tombstone_numsubordinates_instance1}, "
+ f"instance2 has {tombstone_numsubordinates_instance2}. "
+ )
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
\ No newline at end of file
--
2.49.0

View File

@ -0,0 +1,814 @@
From e05653cbff500c47b89e43e4a1c85b7cb30321ff Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Mon, 28 Jul 2025 15:41:29 -0700
Subject: [PATCH] Issue 6884 - Mask password hashes in audit logs (#6885)
Description: Fix the audit log functionality to mask password hash values for
userPassword, nsslapd-rootpw, nsmultiplexorcredentials, nsds5ReplicaCredentials,
and nsds5ReplicaBootstrapCredentials attributes in ADD and MODIFY operations.
Update auditlog.c to detect password attributes and replace their values with
asterisks (**********************) in both LDIF and JSON audit log formats.
Add a comprehensive test suite audit_password_masking_test.py to verify
password masking works correctly across all log formats and operation types.
Fixes: https://github.com/389ds/389-ds-base/issues/6884
Reviewed by: @mreynolds389, @vashirov (Thanks!!)
---
.../logging/audit_password_masking_test.py | 501 ++++++++++++++++++
ldap/servers/slapd/auditlog.c | 170 +++++-
ldap/servers/slapd/slapi-private.h | 1 +
src/lib389/lib389/chaining.py | 3 +-
4 files changed, 652 insertions(+), 23 deletions(-)
create mode 100644 dirsrvtests/tests/suites/logging/audit_password_masking_test.py
diff --git a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
new file mode 100644
index 000000000..3b6a54849
--- /dev/null
+++ b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
@@ -0,0 +1,501 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import logging
+import pytest
+import os
+import re
+import time
+import ldap
+from lib389._constants import DEFAULT_SUFFIX, DN_DM, PW_DM
+from lib389.topologies import topology_m2 as topo
+from lib389.idm.user import UserAccounts
+from lib389.dirsrv_log import DirsrvAuditJSONLog
+from lib389.plugins import ChainingBackendPlugin
+from lib389.chaining import ChainingLinks
+from lib389.agreement import Agreements
+from lib389.replica import ReplicationManager, Replicas
+from lib389.idm.directorymanager import DirectoryManager
+
+log = logging.getLogger(__name__)
+
+MASKED_PASSWORD = "**********************"
+TEST_PASSWORD = "MySecret123"
+TEST_PASSWORD_2 = "NewPassword789"
+TEST_PASSWORD_3 = "NewPassword101"
+
+
+def setup_audit_logging(inst, log_format='default', display_attrs=None):
+ """Configure audit logging settings"""
+ inst.config.replace('nsslapd-auditlog-logbuffering', 'off')
+ inst.config.replace('nsslapd-auditlog-logging-enabled', 'on')
+ inst.config.replace('nsslapd-auditlog-log-format', log_format)
+
+ if display_attrs is not None:
+ inst.config.replace('nsslapd-auditlog-display-attrs', display_attrs)
+
+ inst.deleteAuditLogs()
+
+
+def check_password_masked(inst, log_format, expected_password, actual_password):
+ """Helper function to check password masking in audit logs"""
+
+ time.sleep(1) # Allow log to flush
+
+ # List of all password/credential attributes that should be masked
+ password_attributes = [
+ 'userPassword',
+ 'nsslapd-rootpw',
+ 'nsmultiplexorcredentials',
+ 'nsDS5ReplicaCredentials',
+ 'nsDS5ReplicaBootstrapCredentials'
+ ]
+
+ # Get password schemes to check for hash leakage
+ user_password_scheme = inst.config.get_attr_val_utf8('passwordStorageScheme')
+ root_password_scheme = inst.config.get_attr_val_utf8('nsslapd-rootpwstoragescheme')
+
+ if log_format == 'json':
+ # Check JSON format logs
+ audit_log = DirsrvAuditJSONLog(inst)
+ log_lines = audit_log.readlines()
+
+ found_masked = False
+ found_actual = False
+ found_hashed = False
+
+ for line in log_lines:
+ # Check if any password attribute is present in the line
+ for attr in password_attributes:
+ if attr in line:
+ if expected_password in line:
+ found_masked = True
+ if actual_password in line:
+ found_actual = True
+ # Check for password scheme indicators (hashed passwords)
+ if user_password_scheme and f'{{{user_password_scheme}}}' in line:
+ found_hashed = True
+ if root_password_scheme and f'{{{root_password_scheme}}}' in line:
+ found_hashed = True
+ break # Found a password attribute, no need to check others for this line
+
+ else:
+ # Check LDIF format logs
+ found_masked = False
+ found_actual = False
+ found_hashed = False
+
+ # Check each password attribute for masked password
+ for attr in password_attributes:
+ if inst.ds_audit_log.match(f"{attr}: {re.escape(expected_password)}"):
+ found_masked = True
+ if inst.ds_audit_log.match(f"{attr}: {actual_password}"):
+ found_actual = True
+
+ # Check for hashed passwords in LDIF format
+ if user_password_scheme:
+ if inst.ds_audit_log.match(f"userPassword: {{{user_password_scheme}}}"):
+ found_hashed = True
+ if root_password_scheme:
+ if inst.ds_audit_log.match(f"nsslapd-rootpw: {{{root_password_scheme}}}"):
+ found_hashed = True
+
+ # Delete audit logs to avoid interference with other tests
+ # We need to reset the root password to default as deleteAuditLogs()
+ # opens a new connection with the default password
+ dm = DirectoryManager(inst)
+ dm.change_password(PW_DM)
+ inst.deleteAuditLogs()
+
+ return found_masked, found_actual, found_hashed
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "userPassword"),
+ ("json", None),
+ ("json", "*"),
+ ("json", "userPassword")
+])
+def test_password_masking_add_operation(topo, log_format, display_attrs):
+ """Test password masking in ADD operations
+
+ :id: 4358bd75-bcc7-401c-b492-d3209b10412d
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Add user with password
+ 3. Check that password is masked in audit log
+ 4. Verify actual password does not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Password should be masked with asterisks
+ 4. Actual password should not be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = None
+
+ try:
+ user = users.create(properties={
+ 'uid': 'test_add_pwd_mask',
+ 'cn': 'Test Add User',
+ 'sn': 'User',
+ 'uidNumber': '1000',
+ 'gidNumber': '1000',
+ 'homeDirectory': '/home/test_add',
+ 'userPassword': TEST_PASSWORD
+ })
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+
+ assert found_masked, f"Masked password not found in {log_format} ADD operation"
+ assert not found_actual, f"Actual password found in {log_format} ADD log (should be masked)"
+ assert not found_hashed, f"Hashed password found in {log_format} ADD log (should be masked)"
+
+ finally:
+ if user is not None:
+ try:
+ user.delete()
+ except:
+ pass
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "userPassword"),
+ ("json", None),
+ ("json", "*"),
+ ("json", "userPassword")
+])
+def test_password_masking_modify_operation(topo, log_format, display_attrs):
+ """Test password masking in MODIFY operations
+
+ :id: e6963aa9-7609-419c-aae2-1d517aa434bd
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Add user without password
+ 3. Add password via MODIFY operation
+ 4. Check that password is masked in audit log
+ 5. Modify password to new value
+ 6. Check that new password is also masked
+ 7. Verify actual passwords do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Password should be masked with asterisks
+ 5. Success
+ 6. New password should be masked with asterisks
+ 7. No actual password values should be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = None
+
+ try:
+ user = users.create(properties={
+ 'uid': 'test_modify_pwd_mask',
+ 'cn': 'Test Modify User',
+ 'sn': 'User',
+ 'uidNumber': '2000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/test_modify'
+ })
+
+ user.replace('userPassword', TEST_PASSWORD)
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked password not found in {log_format} MODIFY operation (first password)"
+ assert not found_actual, f"Actual password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed, f"Hashed password found in {log_format} MODIFY log (should be masked)"
+
+ user.replace('userPassword', TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked password not found in {log_format} MODIFY operation (second password)"
+ assert not found_actual_2, f"Second actual password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Second hashed password found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ if user is not None:
+ try:
+ user.delete()
+ except:
+ pass
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsslapd-rootpw"),
+ ("json", None),
+ ("json", "*"),
+ ("json", "nsslapd-rootpw")
+])
+def test_password_masking_rootpw_modify_operation(topo, log_format, display_attrs):
+ """Test password masking for nsslapd-rootpw MODIFY operations
+
+ :id: ec8c9fd4-56ba-4663-ab65-58efb3b445e4
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Modify nsslapd-rootpw in configuration
+ 3. Check that root password is masked in audit log
+ 4. Modify root password to new value
+ 5. Check that new root password is also masked
+ 6. Verify actual root passwords do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Root password should be masked with asterisks
+ 4. Success
+ 5. New root password should be masked with asterisks
+ 6. No actual root password values should be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+ dm = DirectoryManager(inst)
+
+ try:
+ dm.change_password(TEST_PASSWORD)
+ dm.rebind(TEST_PASSWORD)
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked root password not found in {log_format} MODIFY operation (first root password)"
+ assert not found_actual, f"Actual root password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed, f"Hashed root password found in {log_format} MODIFY log (should be masked)"
+
+ dm.change_password(TEST_PASSWORD_2)
+ dm.rebind(TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked root password not found in {log_format} MODIFY operation (second root password)"
+ assert not found_actual_2, f"Second actual root password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Second hashed root password found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ dm.change_password(PW_DM)
+ dm.rebind(PW_DM)
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsmultiplexorcredentials"),
+ ("json", None),
+ ("json", "*"),
+ ("json", "nsmultiplexorcredentials")
+])
+def test_password_masking_multiplexor_credentials(topo, log_format, display_attrs):
+ """Test password masking for nsmultiplexorcredentials in chaining/multiplexor configurations
+
+ :id: 161a9498-b248-4926-90be-a696a36ed36e
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Create a chaining backend configuration entry with nsmultiplexorcredentials
+ 3. Check that multiplexor credentials are masked in audit log
+ 4. Modify the credentials
+ 5. Check that updated credentials are also masked
+ 6. Verify actual credentials do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Multiplexor credentials should be masked with asterisks
+ 4. Success
+ 5. Updated credentials should be masked with asterisks
+ 6. No actual credential values should be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+
+ # Enable chaining plugin and create chaining link
+ chain_plugin = ChainingBackendPlugin(inst)
+ chain_plugin.enable()
+
+ chains = ChainingLinks(inst)
+ chain = None
+
+ try:
+ # Create chaining link with multiplexor credentials
+ chain = chains.create(properties={
+ 'cn': 'testchain',
+ 'nsfarmserverurl': 'ldap://localhost:389/',
+ 'nsslapd-suffix': 'dc=example,dc=com',
+ 'nsmultiplexorbinddn': 'cn=manager',
+ 'nsmultiplexorcredentials': TEST_PASSWORD,
+ 'nsCheckLocalACI': 'on',
+ 'nsConnectionLife': '30',
+ })
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked multiplexor credentials not found in {log_format} ADD operation"
+ assert not found_actual, f"Actual multiplexor credentials found in {log_format} ADD log (should be masked)"
+ assert not found_hashed, f"Hashed multiplexor credentials found in {log_format} ADD log (should be masked)"
+
+ # Modify the credentials
+ chain.replace('nsmultiplexorcredentials', TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked multiplexor credentials not found in {log_format} MODIFY operation"
+ assert not found_actual_2, f"Actual multiplexor credentials found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Hashed multiplexor credentials found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ chain_plugin.disable()
+ if chain is not None:
+ inst.delete_branch_s(chain.dn, ldap.SCOPE_ONELEVEL)
+ chain.delete()
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsDS5ReplicaCredentials"),
+ ("json", None),
+ ("json", "*"),
+ ("json", "nsDS5ReplicaCredentials")
+])
+def test_password_masking_replica_credentials(topo, log_format, display_attrs):
+ """Test password masking for nsDS5ReplicaCredentials in replication agreements
+
+ :id: 7bf9e612-1b7c-49af-9fc0-de4c7df84b2a
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Create a replication agreement entry with nsDS5ReplicaCredentials
+ 3. Check that replica credentials are masked in audit log
+ 4. Modify the credentials
+ 5. Check that updated credentials are also masked
+ 6. Verify actual credentials do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Replica credentials should be masked with asterisks
+ 4. Success
+ 5. Updated credentials should be masked with asterisks
+ 6. No actual credential values should be found in log
+ """
+ inst = topo.ms['supplier2']
+ setup_audit_logging(inst, log_format, display_attrs)
+ agmt = None
+
+ try:
+ replicas = Replicas(inst)
+ replica = replicas.get(DEFAULT_SUFFIX)
+ agmts = replica.get_agreements()
+ agmt = agmts.create(properties={
+ 'cn': 'testagmt',
+ 'nsDS5ReplicaHost': 'localhost',
+ 'nsDS5ReplicaPort': '389',
+ 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config',
+ 'nsDS5ReplicaCredentials': TEST_PASSWORD,
+ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX
+ })
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked replica credentials not found in {log_format} ADD operation"
+ assert not found_actual, f"Actual replica credentials found in {log_format} ADD log (should be masked)"
+ assert not found_hashed, f"Hashed replica credentials found in {log_format} ADD log (should be masked)"
+
+ # Modify the credentials
+ agmt.replace('nsDS5ReplicaCredentials', TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked replica credentials not found in {log_format} MODIFY operation"
+ assert not found_actual_2, f"Actual replica credentials found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Hashed replica credentials found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ if agmt is not None:
+ agmt.delete()
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsDS5ReplicaBootstrapCredentials"),
+ ("json", None),
+ ("json", "*"),
+ ("json", "nsDS5ReplicaBootstrapCredentials")
+])
+def test_password_masking_bootstrap_credentials(topo, log_format, display_attrs):
+ """Test password masking for nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials in replication agreements
+
+ :id: 248bd418-ffa4-4733-963d-2314c60b7c5b
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Create a replication agreement entry with both nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials
+ 3. Check that both credentials are masked in audit log
+ 4. Modify both credentials
+ 5. Check that both updated credentials are also masked
+ 6. Verify actual credentials do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Both credentials should be masked with asterisks
+ 4. Success
+ 5. Both updated credentials should be masked with asterisks
+ 6. No actual credential values should be found in log
+ """
+ inst = topo.ms['supplier2']
+ setup_audit_logging(inst, log_format, display_attrs)
+ agmt = None
+
+ try:
+ replicas = Replicas(inst)
+ replica = replicas.get(DEFAULT_SUFFIX)
+ agmts = replica.get_agreements()
+ agmt = agmts.create(properties={
+ 'cn': 'testbootstrapagmt',
+ 'nsDS5ReplicaHost': 'localhost',
+ 'nsDS5ReplicaPort': '389',
+ 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config',
+ 'nsDS5ReplicaCredentials': TEST_PASSWORD,
+ 'nsDS5replicabootstrapbinddn': 'cn=bootstrap manager,cn=config',
+ 'nsDS5ReplicaBootstrapCredentials': TEST_PASSWORD_2,
+ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX
+ })
+
+ found_masked_bootstrap, found_actual_bootstrap, found_hashed_bootstrap = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_bootstrap, f"Masked bootstrap credentials not found in {log_format} ADD operation"
+ assert not found_actual_bootstrap, f"Actual bootstrap credentials found in {log_format} ADD log (should be masked)"
+ assert not found_hashed_bootstrap, f"Hashed bootstrap credentials found in {log_format} ADD log (should be masked)"
+
+ agmt.replace('nsDS5ReplicaBootstrapCredentials', TEST_PASSWORD_3)
+
+ found_masked_bootstrap_2, found_actual_bootstrap_2, found_hashed_bootstrap_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_3)
+ assert found_masked_bootstrap_2, f"Masked bootstrap credentials not found in {log_format} MODIFY operation"
+ assert not found_actual_bootstrap_2, f"Actual bootstrap credentials found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_bootstrap_2, f"Hashed bootstrap credentials found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ if agmt is not None:
+ agmt.delete()
+
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
\ No newline at end of file
diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c
index 3945b0533..3a34959f6 100644
--- a/ldap/servers/slapd/auditlog.c
+++ b/ldap/servers/slapd/auditlog.c
@@ -39,6 +39,89 @@ static void write_audit_file(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype,
static const char *modrdn_changes[4];
+/* Helper function to check if an attribute is a password that needs masking */
+static int
+is_password_attribute(const char *attr_name)
+{
+ return (strcasecmp(attr_name, SLAPI_USERPWD_ATTR) == 0 ||
+ strcasecmp(attr_name, CONFIG_ROOTPW_ATTRIBUTE) == 0 ||
+ strcasecmp(attr_name, SLAPI_MB_CREDENTIALS) == 0 ||
+ strcasecmp(attr_name, SLAPI_REP_CREDENTIALS) == 0 ||
+ strcasecmp(attr_name, SLAPI_REP_BOOTSTRAP_CREDENTIALS) == 0);
+}
+
+/* Helper function to create a masked string representation of an entry */
+static char *
+create_masked_entry_string(Slapi_Entry *original_entry, int *len)
+{
+ Slapi_Attr *attr = NULL;
+ char *entry_str = NULL;
+ char *current_pos = NULL;
+ char *line_start = NULL;
+ char *next_line = NULL;
+ char *colon_pos = NULL;
+ int has_password_attrs = 0;
+
+ if (original_entry == NULL) {
+ return NULL;
+ }
+
+ /* Single pass through attributes to check for password attributes */
+ for (slapi_entry_first_attr(original_entry, &attr); attr != NULL;
+ slapi_entry_next_attr(original_entry, attr, &attr)) {
+
+ char *attr_name = NULL;
+ slapi_attr_get_type(attr, &attr_name);
+
+ if (is_password_attribute(attr_name)) {
+ has_password_attrs = 1;
+ break;
+ }
+ }
+
+ /* If no password attributes, return original string - no masking needed */
+ entry_str = slapi_entry2str(original_entry, len);
+ if (!has_password_attrs) {
+ return entry_str;
+ }
+
+ /* Process the string in-place, replacing password values */
+ current_pos = entry_str;
+ while ((line_start = current_pos) != NULL && *line_start != '\0') {
+ /* Find the end of current line */
+ next_line = strchr(line_start, '\n');
+ if (next_line != NULL) {
+ *next_line = '\0'; /* Temporarily terminate line */
+ current_pos = next_line + 1;
+ } else {
+ current_pos = NULL; /* Last line */
+ }
+
+ /* Find the colon that separates attribute name from value */
+ colon_pos = strchr(line_start, ':');
+ if (colon_pos != NULL) {
+ char saved_colon = *colon_pos;
+ *colon_pos = '\0'; /* Temporarily null-terminate attribute name */
+
+ /* Check if this is a password attribute that needs masking */
+ if (is_password_attribute(line_start)) {
+ strcpy(colon_pos + 1, " **********************");
+ }
+
+ *colon_pos = saved_colon; /* Restore colon */
+ }
+
+ /* Restore newline if it was there */
+ if (next_line != NULL) {
+ *next_line = '\n';
+ }
+ }
+
+ /* Update length since we may have shortened the string */
+ *len = strlen(entry_str);
+ return entry_str; /* Return the modified original string */
+}
+
void
write_audit_log_entry(Slapi_PBlock *pb)
{
@@ -279,10 +362,31 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object
{
slapi_entry_attr_find(entry, req_attr, &entry_attr);
if (entry_attr) {
- if (use_json) {
- log_entry_attr_json(entry_attr, req_attr, id_list);
+ if (strcmp(req_attr, PSEUDO_ATTR_UNHASHEDUSERPASSWORD) == 0) {
+ /* Do not write the unhashed clear-text password */
+ continue;
+ }
+
+ /* Check if this is a password attribute that needs masking */
+ if (is_password_attribute(req_attr)) {
+ /* userpassword/rootdn password - mask the value */
+ if (use_json) {
+ json_object *secret_obj = json_object_new_object();
+ json_object_object_add(secret_obj, req_attr,
+ json_object_new_string("**********************"));
+ json_object_array_add(id_list, secret_obj);
+ } else {
+ addlenstr(l, "#");
+ addlenstr(l, req_attr);
+ addlenstr(l, ": **********************\n");
+ }
} else {
- log_entry_attr(entry_attr, req_attr, l);
+ /* Regular attribute - log normally */
+ if (use_json) {
+ log_entry_attr_json(entry_attr, req_attr, id_list);
+ } else {
+ log_entry_attr(entry_attr, req_attr, l);
+ }
}
}
}
@@ -297,9 +401,7 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object
continue;
}
- if (strcasecmp(attr, SLAPI_USERPWD_ATTR) == 0 ||
- strcasecmp(attr, CONFIG_ROOTPW_ATTRIBUTE) == 0)
- {
+ if (is_password_attribute(attr)) {
/* userpassword/rootdn password - mask the value */
if (use_json) {
json_object *secret_obj = json_object_new_object();
@@ -309,7 +411,7 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object
} else {
addlenstr(l, "#");
addlenstr(l, attr);
- addlenstr(l, ": ****************************\n");
+ addlenstr(l, ": **********************\n");
}
continue;
}
@@ -478,6 +580,9 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype,
}
}
+ /* Check if this is a password attribute that needs masking */
+ int is_password_attr = is_password_attribute(mods[j]->mod_type);
+
mod = json_object_new_object();
switch (operationtype) {
case LDAP_MOD_ADD:
@@ -502,7 +607,12 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype,
json_object *val_list = NULL;
val_list = json_object_new_array();
for (size_t i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
- json_object_array_add(val_list, json_object_new_string(mods[j]->mod_bvalues[i]->bv_val));
+ if (is_password_attr) {
+ /* Mask password values */
+ json_object_array_add(val_list, json_object_new_string("**********************"));
+ } else {
+ json_object_array_add(val_list, json_object_new_string(mods[j]->mod_bvalues[i]->bv_val));
+ }
}
json_object_object_add(mod, "values", val_list);
}
@@ -514,8 +624,11 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype,
case SLAPI_OPERATION_ADD:
int len;
+
e = change;
- tmp = slapi_entry2str(e, &len);
+
+ /* Create a masked string representation for password attributes */
+ tmp = create_masked_entry_string(e, &len);
tmpsave = tmp;
while ((tmp = strchr(tmp, '\n')) != NULL) {
tmp++;
@@ -662,6 +775,10 @@ write_audit_file(
break;
}
}
+
+ /* Check if this is a password attribute that needs masking */
+ int is_password_attr = is_password_attribute(mods[j]->mod_type);
+
switch (operationtype) {
case LDAP_MOD_ADD:
addlenstr(l, "add: ");
@@ -686,18 +803,27 @@ write_audit_file(
break;
}
if (operationtype != LDAP_MOD_IGNORE) {
- for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
- char *buf, *bufp;
- len = strlen(mods[j]->mod_type);
- len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1;
- buf = slapi_ch_malloc(len);
- bufp = buf;
- slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type,
- mods[j]->mod_bvalues[i]->bv_val,
- mods[j]->mod_bvalues[i]->bv_len, 0);
- *bufp = '\0';
- addlenstr(l, buf);
- slapi_ch_free((void **)&buf);
+ if (is_password_attr) {
+ /* Add masked password */
+ for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
+ addlenstr(l, mods[j]->mod_type);
+ addlenstr(l, ": **********************\n");
+ }
+ } else {
+ /* Add actual values for non-password attributes */
+ for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
+ char *buf, *bufp;
+ len = strlen(mods[j]->mod_type);
+ len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1;
+ buf = slapi_ch_malloc(len);
+ bufp = buf;
+ slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type,
+ mods[j]->mod_bvalues[i]->bv_val,
+ mods[j]->mod_bvalues[i]->bv_len, 0);
+ *bufp = '\0';
+ addlenstr(l, buf);
+ slapi_ch_free((void **)&buf);
+ }
}
}
addlenstr(l, "-\n");
@@ -708,7 +834,7 @@ write_audit_file(
e = change;
addlenstr(l, attr_changetype);
addlenstr(l, ": add\n");
- tmp = slapi_entry2str(e, &len);
+ tmp = create_masked_entry_string(e, &len);
tmpsave = tmp;
while ((tmp = strchr(tmp, '\n')) != NULL) {
tmp++;
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index 7a3eb3fdf..fb88488b1 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -848,6 +848,7 @@ void task_cleanup(void);
/* for reversible encyrption */
#define SLAPI_MB_CREDENTIALS "nsmultiplexorcredentials"
#define SLAPI_REP_CREDENTIALS "nsds5ReplicaCredentials"
+#define SLAPI_REP_BOOTSTRAP_CREDENTIALS "nsds5ReplicaBootstrapCredentials"
int pw_rever_encode(Slapi_Value **vals, char *attr_name);
int pw_rever_decode(char *cipher, char **plain, const char *attr_name);
diff --git a/src/lib389/lib389/chaining.py b/src/lib389/lib389/chaining.py
index 533b83ebf..33ae78c8b 100644
--- a/src/lib389/lib389/chaining.py
+++ b/src/lib389/lib389/chaining.py
@@ -134,7 +134,7 @@ class ChainingLink(DSLdapObject):
"""
# Create chaining entry
- super(ChainingLink, self).create(rdn, properties, basedn)
+ link = super(ChainingLink, self).create(rdn, properties, basedn)
# Create mapping tree entry
dn_comps = ldap.explode_dn(properties['nsslapd-suffix'][0])
@@ -149,6 +149,7 @@ class ChainingLink(DSLdapObject):
self._mts.ensure_state(properties=mt_properties)
except ldap.ALREADY_EXISTS:
pass
+ return link
class ChainingLinks(DSLdapObjects):
--
2.49.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
From 574a5295e13cf01c34226d676104057468198616 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 4 Oct 2024 08:55:11 -0700
Subject: [PATCH] Issue 6339 - Address Coverity scan issues in memberof and
bdb_layer (#6353)
Description: Add null check for memberof attribute in memberof.c
Fix memory leak by freeing 'cookie' in memberof.c
Add null check for database environment in bdb_layer.c
Fix race condition by adding mutex lock/unlock in bdb_layer.c
Fixes: https://github.com/389ds/389-ds-base/issues/6339
Reviewed by: @progier389, @tbordaz (Thanks!)
---
ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
index b04cd68e2..4f069197e 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c
@@ -6987,6 +6987,7 @@ bdb_public_private_open(backend *be, const char *db_filename, int rw, dbi_env_t
bdb_config *conf = (bdb_config *)li->li_dblayer_config;
bdb_db_env **ppEnv = (bdb_db_env**)&priv->dblayer_env;
char dbhome[MAXPATHLEN];
+ bdb_db_env *pEnv = NULL;
DB_ENV *bdb_env = NULL;
DB *bdb_db = NULL;
struct stat st = {0};
@@ -7036,7 +7037,13 @@ bdb_public_private_open(backend *be, const char *db_filename, int rw, dbi_env_t
conf->bdb_tx_max = 50;
rc = bdb_start(li, DBLAYER_NORMAL_MODE);
if (rc == 0) {
- bdb_env = ((struct bdb_db_env*)(priv->dblayer_env))->bdb_DB_ENV;
+ pEnv = (bdb_db_env *)priv->dblayer_env;
+ if (pEnv == NULL) {
+ fprintf(stderr, "bdb_public_private_open: dbenv is not available (0x%p) for database %s\n",
+ (void *)pEnv, db_filename ? db_filename : "unknown");
+ return EINVAL;
+ }
+ bdb_env = pEnv->bdb_DB_ENV;
}
} else {
/* Setup minimal environment */
@@ -7080,8 +7087,12 @@ bdb_public_private_close(struct ldbminfo *li, dbi_env_t **env, dbi_db_t **db)
if (priv) {
/* Detect if db is fully set up in read write mode */
bdb_db_env *pEnv = (bdb_db_env *)priv->dblayer_env;
- if (pEnv && pEnv->bdb_thread_count>0) {
- rw = 1;
+ if (pEnv) {
+ pthread_mutex_lock(&pEnv->bdb_thread_count_lock);
+ if (pEnv->bdb_thread_count > 0) {
+ rw = 1;
+ }
+ pthread_mutex_unlock(&pEnv->bdb_thread_count_lock);
}
}
if (rw == 0) {
--
2.49.0

View File

@ -0,0 +1,31 @@
From 972ddeed2029975d5d89e165db1db554f2e8bc28 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 29 Jul 2025 08:00:00 +0200
Subject: [PATCH] Issue 6468 - CLI - Fix default error log level
Description:
Default error log level is 16384
Relates: https://github.com/389ds/389-ds-base/issues/6468
Reviewed by: @droideck (Thanks!)
---
src/lib389/lib389/cli_conf/logging.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib389/lib389/cli_conf/logging.py b/src/lib389/lib389/cli_conf/logging.py
index d1e32822c..c48c75faa 100644
--- a/src/lib389/lib389/cli_conf/logging.py
+++ b/src/lib389/lib389/cli_conf/logging.py
@@ -44,7 +44,7 @@ ERROR_LEVELS = {
+ "methods used for a SASL bind"
},
"default": {
- "level": 6384,
+ "level": 16384,
"desc": "Default logging level"
},
"filter": {
--
2.49.0

View File

@ -0,0 +1,222 @@
From f28deac93c552a9c4dc9dd9c18f449fcd5cc7731 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 1 Aug 2025 09:28:39 -0700
Subject: [PATCH] Issues 6913, 6886, 6250 - Adjust xfail marks (#6914)
Description: Some of the ACI invalid syntax issues were fixed,
so we need to remove xfail marks.
Disk space issue should have a 'skipif' mark.
Display all attrs (nsslapd-auditlog-display-attrs: *) fails because of a bug.
EntryUSN inconsistency and overflow bugs were exposed with the tests.
Related: https://github.com/389ds/389-ds-base/issues/6913
Related: https://github.com/389ds/389-ds-base/issues/6886
Related: https://github.com/389ds/389-ds-base/issues/6250
Reviewed by: @vashirov (Thanks!)
---
dirsrvtests/tests/suites/acl/syntax_test.py | 13 ++++++++--
.../tests/suites/import/regression_test.py | 18 +++++++-------
.../logging/audit_password_masking_test.py | 24 +++++++++----------
.../suites/plugins/entryusn_overflow_test.py | 2 ++
4 files changed, 34 insertions(+), 23 deletions(-)
diff --git a/dirsrvtests/tests/suites/acl/syntax_test.py b/dirsrvtests/tests/suites/acl/syntax_test.py
index 4edc7fa4b..ed9919ba3 100644
--- a/dirsrvtests/tests/suites/acl/syntax_test.py
+++ b/dirsrvtests/tests/suites/acl/syntax_test.py
@@ -190,10 +190,9 @@ FAILED = [('test_targattrfilters_18',
f'(all)userdn="ldap:///anyone";)'), ]
-@pytest.mark.xfail(reason='https://bugzilla.redhat.com/show_bug.cgi?id=1691473')
@pytest.mark.parametrize("real_value", [a[1] for a in FAILED],
ids=[a[0] for a in FAILED])
-def test_aci_invalid_syntax_fail(topo, real_value):
+def test_aci_invalid_syntax_fail(topo, real_value, request):
"""Try to set wrong ACI syntax.
:id: 83c40784-fff5-49c8-9535-7064c9c19e7e
@@ -206,6 +205,16 @@ def test_aci_invalid_syntax_fail(topo, real_value):
1. It should pass
2. It should not pass
"""
+ # Mark specific test cases as xfail
+ xfail_cases = [
+ 'test_targattrfilters_18',
+ 'test_targattrfilters_20',
+ 'test_bind_rule_set_with_more_than_three'
+ ]
+
+ if request.node.callspec.id in xfail_cases:
+ pytest.xfail("DS6913 - This test case is expected to fail")
+
domain = Domain(topo.standalone, DEFAULT_SUFFIX)
with pytest.raises(ldap.INVALID_SYNTAX):
domain.add("aci", real_value)
diff --git a/dirsrvtests/tests/suites/import/regression_test.py b/dirsrvtests/tests/suites/import/regression_test.py
index 2f850a19a..18611de35 100644
--- a/dirsrvtests/tests/suites/import/regression_test.py
+++ b/dirsrvtests/tests/suites/import/regression_test.py
@@ -323,7 +323,7 @@ ou: myDups00001
@pytest.mark.bz1749595
@pytest.mark.tier2
-@pytest.mark.xfail(not _check_disk_space(), reason="not enough disk space for lmdb map")
+@pytest.mark.skipif(not _check_disk_space(), reason="not enough disk space for lmdb map")
@pytest.mark.xfail(ds_is_older("1.3.10.1"), reason="bz1749595 not fixed on versions older than 1.3.10.1")
def test_large_ldif2db_ancestorid_index_creation(topo, _set_mdb_map_size):
"""Import with ldif2db a large file - check that the ancestorid index creation phase has a correct performance
@@ -399,39 +399,39 @@ def test_large_ldif2db_ancestorid_index_creation(topo, _set_mdb_map_size):
log.info('Starting the server')
topo.standalone.start()
- # With lmdb there is no more any special phase for ancestorid
+ # With lmdb there is no more any special phase for ancestorid
# because ancestorsid get updated on the fly while processing the
# entryrdn (by up the parents chain to compute the parentid
- #
+ #
# But there is still a numSubordinates generation phase
if get_default_db_lib() == "mdb":
log.info('parse the errors logs to check lines with "Generating numSubordinates complete." are present')
end_numsubordinates = str(topo.standalone.ds_error_log.match(r'.*Generating numSubordinates complete.*'))[1:-1]
assert len(end_numsubordinates) > 0
-
+
else:
log.info('parse the errors logs to check lines with "Starting sort of ancestorid" are present')
start_sort_str = str(topo.standalone.ds_error_log.match(r'.*Starting sort of ancestorid non-leaf IDs*'))[1:-1]
assert len(start_sort_str) > 0
-
+
log.info('parse the errors logs to check lines with "Finished sort of ancestorid" are present')
end_sort_str = str(topo.standalone.ds_error_log.match(r'.*Finished sort of ancestorid non-leaf IDs*'))[1:-1]
assert len(end_sort_str) > 0
-
+
log.info('parse the error logs for the line with "Gathering ancestorid non-leaf IDs"')
start_ancestorid_indexing_op_str = str(topo.standalone.ds_error_log.match(r'.*Gathering ancestorid non-leaf IDs*'))[1:-1]
assert len(start_ancestorid_indexing_op_str) > 0
-
+
log.info('parse the error logs for the line with "Created ancestorid index"')
end_ancestorid_indexing_op_str = str(topo.standalone.ds_error_log.match(r'.*Created ancestorid index*'))[1:-1]
assert len(end_ancestorid_indexing_op_str) > 0
-
+
log.info('get the ancestorid non-leaf IDs indexing start and end time from the collected strings')
# Collected lines look like : '[15/May/2020:05:30:27.245967313 -0400] - INFO - bdb_get_nonleaf_ids - import userRoot: Gathering ancestorid non-leaf IDs...'
# We are getting the sec.nanosec part of the date, '27.245967313' in the above example
start_time = (start_ancestorid_indexing_op_str.split()[0]).split(':')[3]
end_time = (end_ancestorid_indexing_op_str.split()[0]).split(':')[3]
-
+
log.info('Calculate the elapsed time for the ancestorid non-leaf IDs index creation')
etime = (Decimal(end_time) - Decimal(start_time))
# The time for the ancestorid index creation should be less than 10s for an offline import of an ldif file with 100000 entries / 5 entries per node
diff --git a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
index 3b6a54849..69a36cb5d 100644
--- a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
+++ b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
@@ -117,10 +117,10 @@ def check_password_masked(inst, log_format, expected_password, actual_password):
@pytest.mark.parametrize("log_format,display_attrs", [
("default", None),
- ("default", "*"),
+ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")),
("default", "userPassword"),
("json", None),
- ("json", "*"),
+ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")),
("json", "userPassword")
])
def test_password_masking_add_operation(topo, log_format, display_attrs):
@@ -173,10 +173,10 @@ def test_password_masking_add_operation(topo, log_format, display_attrs):
@pytest.mark.parametrize("log_format,display_attrs", [
("default", None),
- ("default", "*"),
+ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")),
("default", "userPassword"),
("json", None),
- ("json", "*"),
+ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")),
("json", "userPassword")
])
def test_password_masking_modify_operation(topo, log_format, display_attrs):
@@ -242,10 +242,10 @@ def test_password_masking_modify_operation(topo, log_format, display_attrs):
@pytest.mark.parametrize("log_format,display_attrs", [
("default", None),
- ("default", "*"),
+ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")),
("default", "nsslapd-rootpw"),
("json", None),
- ("json", "*"),
+ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")),
("json", "nsslapd-rootpw")
])
def test_password_masking_rootpw_modify_operation(topo, log_format, display_attrs):
@@ -297,10 +297,10 @@ def test_password_masking_rootpw_modify_operation(topo, log_format, display_attr
@pytest.mark.parametrize("log_format,display_attrs", [
("default", None),
- ("default", "*"),
+ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")),
("default", "nsmultiplexorcredentials"),
("json", None),
- ("json", "*"),
+ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")),
("json", "nsmultiplexorcredentials")
])
def test_password_masking_multiplexor_credentials(topo, log_format, display_attrs):
@@ -368,10 +368,10 @@ def test_password_masking_multiplexor_credentials(topo, log_format, display_attr
@pytest.mark.parametrize("log_format,display_attrs", [
("default", None),
- ("default", "*"),
+ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")),
("default", "nsDS5ReplicaCredentials"),
("json", None),
- ("json", "*"),
+ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")),
("json", "nsDS5ReplicaCredentials")
])
def test_password_masking_replica_credentials(topo, log_format, display_attrs):
@@ -432,10 +432,10 @@ def test_password_masking_replica_credentials(topo, log_format, display_attrs):
@pytest.mark.parametrize("log_format,display_attrs", [
("default", None),
- ("default", "*"),
+ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")),
("default", "nsDS5ReplicaBootstrapCredentials"),
("json", None),
- ("json", "*"),
+ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")),
("json", "nsDS5ReplicaBootstrapCredentials")
])
def test_password_masking_bootstrap_credentials(topo, log_format, display_attrs):
diff --git a/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py
index a23d734ca..8c3a537ab 100644
--- a/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py
+++ b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py
@@ -81,6 +81,7 @@ def setup_usn_test(topology_st, request):
return created_users
+@pytest.mark.xfail(reason="DS6250")
def test_entryusn_overflow_on_add_existing_entries(topology_st, setup_usn_test):
"""Test that reproduces entryUSN overflow when adding existing entries
@@ -232,6 +233,7 @@ def test_entryusn_overflow_on_add_existing_entries(topology_st, setup_usn_test):
log.info("EntryUSN overflow test completed successfully")
+@pytest.mark.xfail(reason="DS6250")
def test_entryusn_consistency_after_failed_adds(topology_st, setup_usn_test):
"""Test that entryUSN remains consistent after failed add operations
--
2.49.0

View File

@ -0,0 +1,32 @@
From 58a9e1083865e75bba3cf9867a3df109031d7810 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 28 Jul 2025 13:18:26 +0200
Subject: [PATCH] Issue 6181 - RFE - Allow system to manage uid/gid at startup
Description:
Expand CapabilityBoundingSet to include CAP_FOWNER
Relates: https://github.com/389ds/389-ds-base/issues/6181
Relates: https://github.com/389ds/389-ds-base/issues/6906
Reviewed by: @progier389 (Thanks!)
---
wrappers/systemd.template.service.in | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/wrappers/systemd.template.service.in b/wrappers/systemd.template.service.in
index fa05c9f60..6db1f6f8f 100644
--- a/wrappers/systemd.template.service.in
+++ b/wrappers/systemd.template.service.in
@@ -25,7 +25,7 @@ MemoryAccounting=yes
# Allow non-root instances to bind to low ports.
AmbientCapabilities=CAP_NET_BIND_SERVICE
-CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_DAC_OVERRIDE CAP_CHOWN
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER
PrivateTmp=on
# https://en.opensuse.org/openSUSE:Security_Features#Systemd_hardening_effort
--
2.49.0

View File

@ -0,0 +1,92 @@
From e03af0aa7e041fc2ca20caf3bcb5810e968043dc Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 13 May 2025 13:53:05 +0200
Subject: [PATCH] Issue 6778 - Memory leak in
roles_cache_create_object_from_entry
Bug Description:
`this_role` has internal allocations (`dn`, `rolescopedn`, etc.)
that are not freed.
Fix Description:
Use `roles_cache_role_object_free` to free `this_role` and all its
internal structures.
Fixes: https://github.com/389ds/389-ds-base/issues/6778
Reviewed by: @mreynolds389 (Thanks!)
---
ldap/servers/plugins/roles/roles_cache.c | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/ldap/servers/plugins/roles/roles_cache.c b/ldap/servers/plugins/roles/roles_cache.c
index bbed11802..60d7182e2 100644
--- a/ldap/servers/plugins/roles/roles_cache.c
+++ b/ldap/servers/plugins/roles/roles_cache.c
@@ -1098,7 +1098,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
/* We determine the role type by reading the objectclass */
if (roles_cache_is_role_entry(role_entry) == 0) {
/* Bad type */
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
return SLAPI_ROLE_DEFINITION_ERROR;
}
@@ -1108,7 +1108,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
this_role->type = type;
} else {
/* Bad type */
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
return SLAPI_ROLE_DEFINITION_ERROR;
}
@@ -1166,7 +1166,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
filter_attr_value = (char *)slapi_entry_attr_get_charptr(role_entry, ROLE_FILTER_ATTR_NAME);
if (filter_attr_value == NULL) {
/* Means probably no attribute or no value there */
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
return SLAPI_ROLE_ERROR_NO_FILTER_SPECIFIED;
}
@@ -1205,7 +1205,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
(char *)slapi_sdn_get_ndn(this_role->dn),
ROLE_FILTER_ATTR_NAME, filter_attr_value,
ROLE_FILTER_ATTR_NAME);
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
slapi_ch_free_string(&filter_attr_value);
return SLAPI_ROLE_ERROR_FILTER_BAD;
}
@@ -1217,7 +1217,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
filter = slapi_str2filter(filter_attr_value);
if (filter == NULL) {
/* An error has occured */
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
slapi_ch_free_string(&filter_attr_value);
return SLAPI_ROLE_ERROR_FILTER_BAD;
}
@@ -1228,7 +1228,8 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
(char *)slapi_sdn_get_ndn(this_role->dn),
filter_attr_value,
ROLE_FILTER_ATTR_NAME);
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
+ slapi_filter_free(filter, 1);
slapi_ch_free_string(&filter_attr_value);
return SLAPI_ROLE_ERROR_FILTER_BAD;
}
@@ -1285,7 +1286,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
if (rc == 0) {
*result = this_role;
} else {
- slapi_ch_free((void **)&this_role);
+ roles_cache_role_object_free((caddr_t)this_role);
}
slapi_log_err(SLAPI_LOG_PLUGIN, ROLES_PLUGIN_SUBSYSTEM,
--
2.49.0

View File

@ -0,0 +1,262 @@
From c8c9d8814bd328d9772b6a248aa142b72430cba1 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 16 Jul 2025 11:22:30 +0200
Subject: [PATCH] Issue 6778 - Memory leak in
roles_cache_create_object_from_entry part 2
Bug Description:
Everytime a role with scope DN is processed, we leak rolescopeDN.
Fix Description:
* Initialize all pointer variables to NULL
* Add additional NULL checks
* Free rolescopeDN
* Move test_rewriter_with_invalid_filter before the DB contains 90k entries
* Use task.wait() for import task completion instead of parsing logs,
increase the timeout
Fixes: https://github.com/389ds/389-ds-base/issues/6778
Reviewed by: @progier389 (Thanks!)
---
dirsrvtests/tests/suites/roles/basic_test.py | 164 +++++++++----------
ldap/servers/plugins/roles/roles_cache.c | 10 +-
2 files changed, 82 insertions(+), 92 deletions(-)
diff --git a/dirsrvtests/tests/suites/roles/basic_test.py b/dirsrvtests/tests/suites/roles/basic_test.py
index d92d6f0c3..ec208bae9 100644
--- a/dirsrvtests/tests/suites/roles/basic_test.py
+++ b/dirsrvtests/tests/suites/roles/basic_test.py
@@ -510,6 +510,76 @@ def test_vattr_on_managed_role(topo, request):
request.addfinalizer(fin)
+def test_rewriter_with_invalid_filter(topo, request):
+ """Test that server does not crash when having
+ invalid filter in filtered role
+
+ :id: 5013b0b2-0af6-11f0-8684-482ae39447e5
+ :setup: standalone server
+ :steps:
+ 1. Setup filtered role with good filter
+ 2. Setup nsrole rewriter
+ 3. Restart the server
+ 4. Search for entries
+ 5. Setup filtered role with bad filter
+ 6. Search for entries
+ :expectedresults:
+ 1. Operation should succeed
+ 2. Operation should succeed
+ 3. Operation should succeed
+ 4. Operation should succeed
+ 5. Operation should succeed
+ 6. Operation should succeed
+ """
+ inst = topo.standalone
+ entries = []
+
+ def fin():
+ inst.start()
+ for entry in entries:
+ entry.delete()
+ request.addfinalizer(fin)
+
+ # Setup filtered role
+ roles = FilteredRoles(inst, f'ou=people,{DEFAULT_SUFFIX}')
+ filter_ko = '(&((objectClass=top)(objectClass=nsPerson))'
+ filter_ok = '(&(objectClass=top)(objectClass=nsPerson))'
+ role_properties = {
+ 'cn': 'TestFilteredRole',
+ 'nsRoleFilter': filter_ok,
+ 'description': 'Test good filter',
+ }
+ role = roles.create(properties=role_properties)
+ entries.append(role)
+
+ # Setup nsrole rewriter
+ rewriters = Rewriters(inst)
+ rewriter_properties = {
+ "cn": "nsrole",
+ "nsslapd-libpath": 'libroles-plugin',
+ "nsslapd-filterrewriter": 'role_nsRole_filter_rewriter',
+ }
+ rewriter = rewriters.ensure_state(properties=rewriter_properties)
+ entries.append(rewriter)
+
+ # Restart thge instance
+ inst.restart()
+
+ # Search for entries
+ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
+
+ # Set bad filter
+ role_properties = {
+ 'cn': 'TestFilteredRole',
+ 'nsRoleFilter': filter_ko,
+ 'description': 'Test bad filter',
+ }
+ role.ensure_state(properties=role_properties)
+
+ # Search for entries
+ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
+
+
def test_managed_and_filtered_role_rewrite(topo, request):
"""Test that filter components containing 'nsrole=xxx'
are reworked if xxx is either a filtered role or a managed
@@ -581,17 +651,11 @@ def test_managed_and_filtered_role_rewrite(topo, request):
PARENT="ou=people,%s" % DEFAULT_SUFFIX
dbgen_users(topo.standalone, 90000, import_ldif, DEFAULT_SUFFIX, entry_name=RDN, generic=True, parent=PARENT)
- # online import
+ # Online import
import_task = ImportTask(topo.standalone)
import_task.import_suffix_from_ldif(ldiffile=import_ldif, suffix=DEFAULT_SUFFIX)
- # Check for up to 200sec that the completion
- for i in range(1, 20):
- if len(topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9000.*')) > 0:
- break
- time.sleep(10)
- import_complete = topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9000.*')
- assert (len(import_complete) == 1)
-
+ import_task.wait(timeout=400)
+ assert import_task.get_exit_code() == 0
# Restart server
topo.standalone.restart()
@@ -715,17 +779,11 @@ def test_not_such_entry_role_rewrite(topo, request):
PARENT="ou=people,%s" % DEFAULT_SUFFIX
dbgen_users(topo.standalone, 91000, import_ldif, DEFAULT_SUFFIX, entry_name=RDN, generic=True, parent=PARENT)
- # online import
+ # Online import
import_task = ImportTask(topo.standalone)
import_task.import_suffix_from_ldif(ldiffile=import_ldif, suffix=DEFAULT_SUFFIX)
- # Check for up to 200sec that the completion
- for i in range(1, 20):
- if len(topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9100.*')) > 0:
- break
- time.sleep(10)
- import_complete = topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9100.*')
- assert (len(import_complete) == 1)
-
+ import_task.wait(timeout=400)
+ assert import_task.get_exit_code() == 0
# Restart server
topo.standalone.restart()
@@ -769,76 +827,6 @@ def test_not_such_entry_role_rewrite(topo, request):
request.addfinalizer(fin)
-def test_rewriter_with_invalid_filter(topo, request):
- """Test that server does not crash when having
- invalid filter in filtered role
-
- :id: 5013b0b2-0af6-11f0-8684-482ae39447e5
- :setup: standalone server
- :steps:
- 1. Setup filtered role with good filter
- 2. Setup nsrole rewriter
- 3. Restart the server
- 4. Search for entries
- 5. Setup filtered role with bad filter
- 6. Search for entries
- :expectedresults:
- 1. Operation should succeed
- 2. Operation should succeed
- 3. Operation should succeed
- 4. Operation should succeed
- 5. Operation should succeed
- 6. Operation should succeed
- """
- inst = topo.standalone
- entries = []
-
- def fin():
- inst.start()
- for entry in entries:
- entry.delete()
- request.addfinalizer(fin)
-
- # Setup filtered role
- roles = FilteredRoles(inst, f'ou=people,{DEFAULT_SUFFIX}')
- filter_ko = '(&((objectClass=top)(objectClass=nsPerson))'
- filter_ok = '(&(objectClass=top)(objectClass=nsPerson))'
- role_properties = {
- 'cn': 'TestFilteredRole',
- 'nsRoleFilter': filter_ok,
- 'description': 'Test good filter',
- }
- role = roles.create(properties=role_properties)
- entries.append(role)
-
- # Setup nsrole rewriter
- rewriters = Rewriters(inst)
- rewriter_properties = {
- "cn": "nsrole",
- "nsslapd-libpath": 'libroles-plugin',
- "nsslapd-filterrewriter": 'role_nsRole_filter_rewriter',
- }
- rewriter = rewriters.ensure_state(properties=rewriter_properties)
- entries.append(rewriter)
-
- # Restart thge instance
- inst.restart()
-
- # Search for entries
- entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
-
- # Set bad filter
- role_properties = {
- 'cn': 'TestFilteredRole',
- 'nsRoleFilter': filter_ko,
- 'description': 'Test bad filter',
- }
- role.ensure_state(properties=role_properties)
-
- # Search for entries
- entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
-
-
if __name__ == "__main__":
CURRENT_FILE = os.path.realpath(__file__)
pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/ldap/servers/plugins/roles/roles_cache.c b/ldap/servers/plugins/roles/roles_cache.c
index 60d7182e2..60f5a919a 100644
--- a/ldap/servers/plugins/roles/roles_cache.c
+++ b/ldap/servers/plugins/roles/roles_cache.c
@@ -1117,16 +1117,17 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
rolescopeDN = slapi_entry_attr_get_charptr(role_entry, ROLE_SCOPE_DN);
if (rolescopeDN) {
- Slapi_DN *rolescopeSDN;
- Slapi_DN *top_rolescopeSDN, *top_this_roleSDN;
+ Slapi_DN *rolescopeSDN = NULL;
+ Slapi_DN *top_rolescopeSDN = NULL;
+ Slapi_DN *top_this_roleSDN = NULL;
/* Before accepting to use this scope, first check if it belongs to the same suffix */
rolescopeSDN = slapi_sdn_new_dn_byref(rolescopeDN);
- if ((strlen((char *)slapi_sdn_get_ndn(rolescopeSDN)) > 0) &&
+ if (rolescopeSDN && (strlen((char *)slapi_sdn_get_ndn(rolescopeSDN)) > 0) &&
(slapi_dn_syntax_check(NULL, (char *)slapi_sdn_get_ndn(rolescopeSDN), 1) == 0)) {
top_rolescopeSDN = roles_cache_get_top_suffix(rolescopeSDN);
top_this_roleSDN = roles_cache_get_top_suffix(this_role->dn);
- if (slapi_sdn_compare(top_rolescopeSDN, top_this_roleSDN) == 0) {
+ if (top_rolescopeSDN && top_this_roleSDN && slapi_sdn_compare(top_rolescopeSDN, top_this_roleSDN) == 0) {
/* rolescopeDN belongs to the same suffix as the role, we can use this scope */
this_role->rolescopedn = rolescopeSDN;
} else {
@@ -1148,6 +1149,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu
rolescopeDN);
slapi_sdn_free(&rolescopeSDN);
}
+ slapi_ch_free_string(&rolescopeDN);
}
/* Depending upon role type, pull out the remaining information we need */
--
2.49.0

View File

@ -0,0 +1,65 @@
From f83a1996e3438e471cec086d53fb94be0c8666aa Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 7 Jul 2025 23:11:17 +0200
Subject: [PATCH] Issue 6850 - AddressSanitizer: memory leak in mdb_init
Bug Description:
`dbmdb_componentid` can be allocated multiple times. To avoid a memory
leak, allocate it only once, and free at the cleanup.
Fixes: https://github.com/389ds/389-ds-base/issues/6850
Reviewed by: @mreynolds389, @tbordaz (Tnanks!)
---
ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c | 4 +++-
ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c | 2 +-
ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c | 5 +++++
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c
index 1f7b71442..bebc83b76 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c
@@ -146,7 +146,9 @@ dbmdb_compute_limits(struct ldbminfo *li)
int mdb_init(struct ldbminfo *li, config_info *config_array)
{
dbmdb_ctx_t *conf = (dbmdb_ctx_t *)slapi_ch_calloc(1, sizeof(dbmdb_ctx_t));
- dbmdb_componentid = generate_componentid(NULL, "db-mdb");
+ if (dbmdb_componentid == NULL) {
+ dbmdb_componentid = generate_componentid(NULL, "db-mdb");
+ }
li->li_dblayer_config = conf;
strncpy(conf->home, li->li_directory, MAXPATHLEN-1);
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 3ecc47170..c6e9f8b01 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
@@ -19,7 +19,7 @@
#include <prclist.h>
#include <glob.h>
-Slapi_ComponentId *dbmdb_componentid;
+Slapi_ComponentId *dbmdb_componentid = NULL;
#define BULKOP_MAX_RECORDS 100 /* Max records handled by a single bulk operations */
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 2d07db9b5..ae10ac7cf 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c
@@ -49,6 +49,11 @@ dbmdb_cleanup(struct ldbminfo *li)
}
slapi_ch_free((void **)&(li->li_dblayer_config));
+ if (dbmdb_componentid != NULL) {
+ release_componentid(dbmdb_componentid);
+ dbmdb_componentid = NULL;
+ }
+
return 0;
}
--
2.49.0

View File

@ -0,0 +1,58 @@
From e98acc1bfe2194fcdd0e420777eb65a20d55a64b Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 7 Jul 2025 22:01:09 +0200
Subject: [PATCH] Issue 6848 - AddressSanitizer: leak in do_search
Bug Description:
When there's a BER decoding error and the function goes to
`free_and_return`, the `attrs` variable is not being freed because it's
only freed if `!psearch || rc != 0 || err != 0`, but `err` is still 0 at
that point.
If we reach `free_and_return` from the `ber_scanf` error path, `attrs`
was never set in the pblock with `slapi_pblock_set()`, so the
`slapi_pblock_get()` call will not retrieve the potentially partially
allocated `attrs` from the BER decoding.
Fixes: https://github.com/389ds/389-ds-base/issues/6848
Reviewed by: @tbordaz, @droideck (Thanks!)
---
ldap/servers/slapd/search.c | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/ldap/servers/slapd/search.c b/ldap/servers/slapd/search.c
index e9b2c3670..f9d03c090 100644
--- a/ldap/servers/slapd/search.c
+++ b/ldap/servers/slapd/search.c
@@ -235,6 +235,7 @@ do_search(Slapi_PBlock *pb)
log_search_access(pb, base, scope, fstr, "decoding error");
send_ldap_result(pb, LDAP_PROTOCOL_ERROR, NULL, NULL, 0,
NULL);
+ err = 1; /* Make sure we free everything */
goto free_and_return;
}
@@ -420,8 +421,17 @@ free_and_return:
if (!psearch || rc != 0 || err != 0) {
slapi_ch_free_string(&fstr);
slapi_filter_free(filter, 1);
- slapi_pblock_get(pb, SLAPI_SEARCH_ATTRS, &attrs);
- charray_free(attrs); /* passing NULL is fine */
+
+ /* Get attrs from pblock if it was set there, otherwise use local attrs */
+ char **pblock_attrs = NULL;
+ slapi_pblock_get(pb, SLAPI_SEARCH_ATTRS, &pblock_attrs);
+ if (pblock_attrs != NULL) {
+ charray_free(pblock_attrs); /* Free attrs from pblock */
+ slapi_pblock_set(pb, SLAPI_SEARCH_ATTRS, NULL);
+ } else if (attrs != NULL) {
+ /* Free attrs that were allocated but never put in pblock */
+ charray_free(attrs);
+ }
charray_free(gerattrs); /* passing NULL is fine */
/*
* Fix for defect 526719 / 553356 : Persistent search op failed.
--
2.49.0

View File

@ -0,0 +1,58 @@
From 120bc2666b682a27ffd6ace5cc238b33fab32c21 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 11 Jul 2025 12:32:38 +0200
Subject: [PATCH] Issue 6865 - AddressSanitizer: leak in
agmt_update_init_status
Bug Description:
We allocate an array of `LDAPMod *` pointers, but never free it:
```
=================================================================
==2748356==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 24 byte(s) in 1 object(s) allocated from:
#0 0x7f05e8cb4a07 in __interceptor_malloc (/lib64/libasan.so.6+0xb4a07)
#1 0x7f05e85c0138 in slapi_ch_malloc (/usr/lib64/dirsrv/libslapd.so.0+0x1c0138)
#2 0x7f05e109e481 in agmt_update_init_status ldap/servers/plugins/replication/repl5_agmt.c:2583
#3 0x7f05e10a0aa5 in agmtlist_shutdown ldap/servers/plugins/replication/repl5_agmtlist.c:789
#4 0x7f05e10ab6bc in multisupplier_stop ldap/servers/plugins/replication/repl5_init.c:844
#5 0x7f05e10ab6bc in multisupplier_stop ldap/servers/plugins/replication/repl5_init.c:837
#6 0x7f05e862507d in plugin_call_func ldap/servers/slapd/plugin.c:2001
#7 0x7f05e8625be1 in plugin_call_one ldap/servers/slapd/plugin.c:1950
#8 0x7f05e8625be1 in plugin_dependency_closeall ldap/servers/slapd/plugin.c:1844
#9 0x55e1a7ff9815 in slapd_daemon ldap/servers/slapd/daemon.c:1275
#10 0x55e1a7fd36ef in main (/usr/sbin/ns-slapd+0x3e6ef)
#11 0x7f05e80295cf in __libc_start_call_main (/lib64/libc.so.6+0x295cf)
#12 0x7f05e802967f in __libc_start_main_alias_2 (/lib64/libc.so.6+0x2967f)
#13 0x55e1a7fd74a4 in _start (/usr/sbin/ns-slapd+0x424a4)
SUMMARY: AddressSanitizer: 24 byte(s) leaked in 1 allocation(s).
```
Fix Description:
Ensure `mods` is freed in the cleanup code.
Fixes: https://github.com/389ds/389-ds-base/issues/6865
Relates: https://github.com/389ds/389-ds-base/issues/6470
Reviewed by: @mreynolds389 (Thanks!)
---
ldap/servers/plugins/replication/repl5_agmt.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/ldap/servers/plugins/replication/repl5_agmt.c b/ldap/servers/plugins/replication/repl5_agmt.c
index 6ffb074d4..c6cfcda07 100644
--- a/ldap/servers/plugins/replication/repl5_agmt.c
+++ b/ldap/servers/plugins/replication/repl5_agmt.c
@@ -2653,6 +2653,7 @@ agmt_update_init_status(Repl_Agmt *ra)
} else {
PR_Unlock(ra->lock);
}
+ slapi_ch_free((void **)&mods);
slapi_mod_done(&smod_start_time);
slapi_mod_done(&smod_end_time);
slapi_mod_done(&smod_status);
--
2.49.0

View File

@ -0,0 +1,97 @@
From 5cc13c70dfe22d95686bec9214c53f1b4114cd90 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Fri, 1 Aug 2025 13:27:02 +0100
Subject: [PATCH] Issue 6768 - ns-slapd crashes when a referral is added
(#6780)
Bug description: When a paged result search is successfully run on a referred
suffix, we retrieve the search result set from the pblock and try to release
it. In this case the search result set is NULL, which triggers a SEGV during
the release.
Fix description: If the search result code is LDAP_REFERRAL, skip deletion of
the search result set. Added test case.
Fixes: https://github.com/389ds/389-ds-base/issues/6768
Reviewed by: @tbordaz, @progier389 (Thank you)
---
.../paged_results/paged_results_test.py | 46 +++++++++++++++++++
ldap/servers/slapd/opshared.c | 4 +-
2 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/paged_results/paged_results_test.py b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
index fca48db0f..1bb94b53a 100644
--- a/dirsrvtests/tests/suites/paged_results/paged_results_test.py
+++ b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
@@ -1271,6 +1271,52 @@ def test_search_stress_abandon(create_40k_users, create_user):
paged_search(conn, create_40k_users.suffix, [req_ctrl], search_flt, searchreq_attrlist, abandon_rate=abandon_rate)
+def test_search_referral(topology_st):
+ """Test a paged search on a referred suffix doesnt crash the server.
+
+ :id: c788bdbf-965b-4f12-ac24-d4d695e2cce2
+
+ :setup: Standalone instance
+
+ :steps:
+ 1. Configure a default referral.
+ 2. Create a paged result search control.
+ 3. Paged result search on referral suffix (doesnt exist on the instance, triggering a referral).
+ 4. Check the server is still running.
+ 5. Remove referral.
+
+ :expectedresults:
+ 1. Referral sucessfully set.
+ 2. Control created.
+ 3. Search returns ldap.REFERRAL (10).
+ 4. Server still running.
+ 5. Referral removed.
+ """
+
+ page_size = 5
+ SEARCH_SUFFIX = "dc=referme,dc=com"
+ REFERRAL = "ldap://localhost.localdomain:389/o%3dnetscaperoot"
+
+ log.info('Configuring referral')
+ topology_st.standalone.config.set('nsslapd-referral', REFERRAL)
+ referral = topology_st.standalone.config.get_attr_val_utf8('nsslapd-referral')
+ assert (referral == REFERRAL)
+
+ log.info('Create paged result search control')
+ req_ctrl = SimplePagedResultsControl(True, size=page_size, cookie='')
+
+ log.info('Perform a paged result search on referred suffix, no chase')
+ with pytest.raises(ldap.REFERRAL):
+ topology_st.standalone.search_ext_s(SEARCH_SUFFIX, ldap.SCOPE_SUBTREE, serverctrls=[req_ctrl])
+
+ log.info('Confirm instance is still running')
+ assert (topology_st.standalone.status())
+
+ log.info('Remove referral')
+ topology_st.standalone.config.remove_all('nsslapd-referral')
+ referral = topology_st.standalone.config.get_attr_val_utf8('nsslapd-referral')
+ assert (referral == None)
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c
index 14a7dcdfb..03ed60981 100644
--- a/ldap/servers/slapd/opshared.c
+++ b/ldap/servers/slapd/opshared.c
@@ -879,7 +879,9 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
/* Free the results if not "no_such_object" */
void *sr = NULL;
slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET, &sr);
- be->be_search_results_release(&sr);
+ if (be->be_search_results_release != NULL) {
+ be->be_search_results_release(&sr);
+ }
}
pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx);
rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, 1);
--
2.49.0

View File

@ -47,7 +47,7 @@ ExcludeArch: i686
Summary: 389 Directory Server (base)
Name: 389-ds-base
Version: 2.7.0
Release: 4%{?dist}
Release: 5%{?dist}
License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (0BSD OR Apache-2.0 OR MIT) AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT OR Zlib) AND (Apache-2.0 OR MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND Apache-2.0 AND MIT AND MPL-2.0 AND Zlib
URL: https://www.port389.org
Conflicts: selinux-policy-base < 3.9.8
@ -68,16 +68,16 @@ Provides: bundled(crate(base64)) = 0.13.1
Provides: bundled(crate(bitflags)) = 2.9.1
Provides: bundled(crate(byteorder)) = 1.5.0
Provides: bundled(crate(cbindgen)) = 0.26.0
Provides: bundled(crate(cc)) = 1.2.27
Provides: bundled(crate(cc)) = 1.2.31
Provides: bundled(crate(cfg-if)) = 1.0.1
Provides: bundled(crate(clap)) = 3.2.25
Provides: bundled(crate(clap_lex)) = 0.2.4
Provides: bundled(crate(concread)) = 0.5.6
Provides: bundled(crate(concread)) = 0.5.7
Provides: bundled(crate(crossbeam-epoch)) = 0.9.18
Provides: bundled(crate(crossbeam-queue)) = 0.3.12
Provides: bundled(crate(crossbeam-utils)) = 0.8.21
Provides: bundled(crate(equivalent)) = 1.0.2
Provides: bundled(crate(errno)) = 0.3.12
Provides: bundled(crate(errno)) = 0.3.13
Provides: bundled(crate(fastrand)) = 2.3.0
Provides: bundled(crate(fernet)) = 0.1.4
Provides: bundled(crate(foldhash)) = 0.1.5
@ -89,6 +89,7 @@ Provides: bundled(crate(hashbrown)) = 0.15.4
Provides: bundled(crate(heck)) = 0.4.1
Provides: bundled(crate(hermit-abi)) = 0.1.19
Provides: bundled(crate(indexmap)) = 1.9.3
Provides: bundled(crate(io-uring)) = 0.7.9
Provides: bundled(crate(itoa)) = 1.0.15
Provides: bundled(crate(jobserver)) = 0.1.33
Provides: bundled(crate(libc)) = 0.2.174
@ -97,6 +98,7 @@ Provides: bundled(crate(log)) = 0.4.27
Provides: bundled(crate(lru)) = 0.13.0
Provides: bundled(crate(memchr)) = 2.7.5
Provides: bundled(crate(miniz_oxide)) = 0.8.9
Provides: bundled(crate(mio)) = 1.0.4
Provides: bundled(crate(object)) = 0.36.7
Provides: bundled(crate(once_cell)) = 1.21.3
Provides: bundled(crate(openssl)) = 0.10.73
@ -111,21 +113,22 @@ Provides: bundled(crate(proc-macro-hack)) = 0.5.20+deprecated
Provides: bundled(crate(proc-macro2)) = 1.0.95
Provides: bundled(crate(quote)) = 1.0.40
Provides: bundled(crate(r-efi)) = 5.3.0
Provides: bundled(crate(rustc-demangle)) = 0.1.25
Provides: bundled(crate(rustix)) = 1.0.7
Provides: bundled(crate(rustc-demangle)) = 0.1.26
Provides: bundled(crate(rustix)) = 1.0.8
Provides: bundled(crate(ryu)) = 1.0.20
Provides: bundled(crate(serde)) = 1.0.219
Provides: bundled(crate(serde_derive)) = 1.0.219
Provides: bundled(crate(serde_json)) = 1.0.140
Provides: bundled(crate(serde_json)) = 1.0.142
Provides: bundled(crate(shlex)) = 1.3.0
Provides: bundled(crate(slab)) = 0.4.10
Provides: bundled(crate(smallvec)) = 1.15.1
Provides: bundled(crate(sptr)) = 0.3.2
Provides: bundled(crate(strsim)) = 0.10.0
Provides: bundled(crate(syn)) = 2.0.103
Provides: bundled(crate(syn)) = 2.0.104
Provides: bundled(crate(tempfile)) = 3.20.0
Provides: bundled(crate(termcolor)) = 1.4.1
Provides: bundled(crate(textwrap)) = 0.16.2
Provides: bundled(crate(tokio)) = 1.45.1
Provides: bundled(crate(tokio)) = 1.47.1
Provides: bundled(crate(toml)) = 0.5.11
Provides: bundled(crate(tracing)) = 0.1.41
Provides: bundled(crate(tracing-attributes)) = 0.1.30
@ -138,16 +141,17 @@ Provides: bundled(crate(winapi)) = 0.3.9
Provides: bundled(crate(winapi-i686-pc-windows-gnu)) = 0.4.0
Provides: bundled(crate(winapi-util)) = 0.1.9
Provides: bundled(crate(winapi-x86_64-pc-windows-gnu)) = 0.4.0
Provides: bundled(crate(windows-sys)) = 0.59.0
Provides: bundled(crate(windows-targets)) = 0.52.6
Provides: bundled(crate(windows_aarch64_gnullvm)) = 0.52.6
Provides: bundled(crate(windows_aarch64_msvc)) = 0.52.6
Provides: bundled(crate(windows_i686_gnu)) = 0.52.6
Provides: bundled(crate(windows_i686_gnullvm)) = 0.52.6
Provides: bundled(crate(windows_i686_msvc)) = 0.52.6
Provides: bundled(crate(windows_x86_64_gnu)) = 0.52.6
Provides: bundled(crate(windows_x86_64_gnullvm)) = 0.52.6
Provides: bundled(crate(windows_x86_64_msvc)) = 0.52.6
Provides: bundled(crate(windows-link)) = 0.1.3
Provides: bundled(crate(windows-sys)) = 0.60.2
Provides: bundled(crate(windows-targets)) = 0.53.3
Provides: bundled(crate(windows_aarch64_gnullvm)) = 0.53.0
Provides: bundled(crate(windows_aarch64_msvc)) = 0.53.0
Provides: bundled(crate(windows_i686_gnu)) = 0.53.0
Provides: bundled(crate(windows_i686_gnullvm)) = 0.53.0
Provides: bundled(crate(windows_i686_msvc)) = 0.53.0
Provides: bundled(crate(windows_x86_64_gnu)) = 0.53.0
Provides: bundled(crate(windows_x86_64_gnullvm)) = 0.53.0
Provides: bundled(crate(windows_x86_64_msvc)) = 0.53.0
Provides: bundled(crate(wit-bindgen-rt)) = 0.39.0
Provides: bundled(crate(zeroize)) = 1.8.1
Provides: bundled(crate(zeroize_derive)) = 1.4.2
@ -282,9 +286,38 @@ Source3: https://github.com/jemalloc/%{jemalloc_name}/releases/download
%endif
Source4: 389-ds-base.sysusers
Source5: vendor-%{version}-1.tar.gz
Source6: Cargo-%{version}-1.lock
Patch: 0001-Issue-6377-syntax-error-in-setup.py-6378.patch
Patch: 0002-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch
Patch: 0003-Issue-6680-instance-read-only-mode-is-broken-6681.patch
Patch: 0004-Issue-6825-RootDN-Access-Control-Plugin-with-wildcar.patch
Patch: 0005-Issue-6119-Synchronise-accept_thread-with-slapd_daem.patch
Patch: 0006-Issue-6782-Improve-paged-result-locking.patch
Patch: 0007-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch
Patch: 0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch
Patch: 0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch
Patch: 0010-Issue-6859-str2filter-is-not-fully-applying-matching.patch
Patch: 0011-Issue-6872-compressed-log-rotation-creates-files-wit.patch
Patch: 0012-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch
Patch: 0013-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch
Patch: 0014-Issue-6893-Log-user-that-is-updated-during-password-.patch
Patch: 0015-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch
Patch: 0016-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch
Patch: 0017-Issue-6594-Add-test-for-numSubordinates-replication-.patch
Patch: 0018-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch
Patch: 0019-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch
Patch: 0020-Issue-6339-Address-Coverity-scan-issues-in-memberof-.patch
Patch: 0021-Issue-6468-CLI-Fix-default-error-log-level.patch
Patch: 0022-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch
Patch: 0023-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch
Patch: 0024-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch
Patch: 0025-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch
Patch: 0026-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch
Patch: 0027-Issue-6848-AddressSanitizer-leak-in-do_search.patch
Patch: 0028-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch
Patch: 0029-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch
%description
389 Directory Server is an LDAPv3 compliant server. The base package includes
@ -388,6 +421,10 @@ A cockpit UI Plugin for configuring and administering the 389 Directory Server
%prep
%autosetup -p1 -n %{name}-%{version}
rm -rf vendor
tar xzf %{SOURCE5}
cp %{SOURCE6} src/Cargo.lock
%if %{bundle_jemalloc}
%setup -q -n %{name}-%{version} -T -D -b 3
%endif
@ -727,6 +764,18 @@ exit 0
%endif
%changelog
* Tue Aug 05 2025 Viktor Ashirov <vashirov@redhat.com> - 2.7.0-5
- Resolves: RHEL-89762 - dsidm Error: float() argument must be a string or a number, not 'NoneType' [rhel-9]
- Resolves: RHEL-92041 - Memory leak in roles_cache_create_object_from_entry
- Resolves: RHEL-95444 - ns-slapd[xxxx]: segfault at 10d7d0d0 ip 00007ff734050cdb sp 00007ff6de9f1430 error 6 in libslapd.so.0.1.0[7ff733ec0000+1b3000] [rhel-9]
- Resolves: RHEL-104821 - ipa-restore fails to restore SELinux contexts, causes ns-slapd AVC denials on /dev/shm after restore.
- Resolves: RHEL-107005 - Failure to get Server monitoring data when NDN cache is disabled. [rhel-9]
- Resolves: RHEL-107581 - segfault - error 4 in libpthread-2.28.so [rhel-9]
- Resolves: RHEL-107585 - ns-slapd crashed when we add nsslapd-referral [rhel-9]
- Resolves: RHEL-107586 - CWE-284 dirsrv log rotation creates files with world readable permission [rhel-9]
- Resolves: RHEL-107587 - CWE-532 Created user password hash available to see in audit log [rhel-9]
- Resolves: RHEL-107588 - CWE-778 Log doesn't show what user gets password changed by administrator [rhel-9]
* Mon Jul 21 2025 Viktor Ashirov <vashirov@redhat.com> - 2.7.0-4
- Resolves: RHEL-61347 - Directory Server is unavailable after a restart with nsslapd-readonly=on and consumes 100% CPU

View File

@ -1,2 +1,4 @@
SHA512 (jemalloc-5.3.0.tar.bz2) = 22907bb052096e2caffb6e4e23548aecc5cc9283dce476896a2b1127eee64170e3562fa2e7db9571298814a7a2c7df6e8d1fbe152bd3f3b0c1abec22a2de34b1
SHA512 (389-ds-base-2.7.0.tar.bz2) = 3aa6ea8b2c59c6188ede25a0c027fc32153d5276857699bc43368db1cb747fea09edeff326186d30fda30fe0a98574625ae0907d99065d1868b169256de1b035
SHA512 (Cargo-2.7.0-1.lock) = ea6db252e49de8aa2fe165f5cc773dc2eb227100d56953a36ca062680a3fc54870a961b05aaac1f7a761c69f3685cc8a7be474ac92377a1219c293fd1117f491
SHA512 (vendor-2.7.0-1.tar.gz) = dda0afe82289812440c3e89ddd305ba21bf89104aa7debfabd398d9719c8e0b48173f52b487f270b9cc40c0b010dc1e749eb7f3a709074a0d650bea811e0e87a