Bump version to 2.9.0-3

- Resolves: RHEL-243052 - IPA - segfault with libjemalloc.so.2 [rhel-9]
- Resolves: RHEL-221410 - large IdM host group, missing memberof host group, authentication failures, high traffic and contention
- Resolves: RHEL-243050 - repl-agmt create doesn't set some parameters [rhel-9]
- Resolves: RHEL-243053 - 389-ds accountpolicy.py attribute name typo in help message [rhel-9]
- Resolves: RHEL-244870 - Local password policies can be created with unallowed values [rhel-9]
- Resolves: RHEL-238566 - lib389: join_supplier() sets nsDS5ReplicaBindDNGroup after ensure_agreement(), causing replication auth race [rhel-9]
- Resolves: RHEL-182153 - CVE-2026-11610 389-ds-base: 389-ds-base: Heap buffer overflow in sasl_io_recv() via padded SASL UNBIND [rhel-9.9]
This commit is contained in:
Simon Pichugin 2026-08-17 22:23:04 -07:00
parent a7590cdbcb
commit cd165d5f10
10 changed files with 1650 additions and 1 deletions

View File

@ -0,0 +1,171 @@
From 23f4b544c7db3fe3100b0f21454828f71891fee3 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 23 Jul 2026 09:58:40 +0200
Subject: [PATCH] Issue 7200 - repl-agmt create doesn't set some parameters
(#7663)
Bug Description:
The `add_agmt()` function in the dsconf CLI silently ignored flow
and timeout parameters:
--conn-timeout
--protocol-timeout
--wait-async-results
--busy-wait-time
--session-pause-time
--flow-control-window
--flow-control-pause
The CLI args and attribute mappings existed, but the properties dict was
never populated with these values during agreement creation.
Fix Description:
Add the missing args-to-properties assignments.
Fixes: https://github.com/389ds/389-ds-base/issues/7200
Reviewed by: @mreynolds389 (Thanks!)
---
.../clu/dsconf_agmt_timeout_attrs_test.py | 106 ++++++++++++++++++
src/lib389/lib389/cli_conf/replication.py | 14 +++
2 files changed, 120 insertions(+)
create mode 100644 dirsrvtests/tests/suites/clu/dsconf_agmt_timeout_attrs_test.py
diff --git a/dirsrvtests/tests/suites/clu/dsconf_agmt_timeout_attrs_test.py b/dirsrvtests/tests/suites/clu/dsconf_agmt_timeout_attrs_test.py
new file mode 100644
index 000000000..8c50e3dc2
--- /dev/null
+++ b/dirsrvtests/tests/suites/clu/dsconf_agmt_timeout_attrs_test.py
@@ -0,0 +1,106 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import os
+import logging
+import pytest
+from test389.topologies import topology_st as topo
+from lib389.cli_base import FakeArgs
+from lib389.cli_conf.replication import add_agmt
+from lib389.replica import Replicas
+from lib389._constants import DEFAULT_SUFFIX
+
+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_agmt_create_timeout_and_flow_control_attrs(topo):
+ """Verify add_agmt passes timeout and flow-control attributes
+ through to the agreement entry
+
+ :id: 2c95ce33-7f25-480a-b827-c03ee10da650
+ :setup: Standalone instance
+ :steps:
+ 1. Enable replication on the instance as a supplier
+ 2. Add agreement with all timeout/flow-control arguments set
+ 3. Read back the agreement and verify each attribute values
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ """
+ inst = topo.standalone
+
+ replicas = Replicas(inst)
+ replicas.create(properties={
+ 'cn': 'replica',
+ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX,
+ 'nsDS5ReplicaId': '1',
+ 'nsDS5ReplicaType': '3',
+ 'nsDS5Flags': '1',
+ })
+
+ EXPECTED = {
+ 'nsds5replicatimeout': '30',
+ 'nsds5replicaprotocoltimeout': '120',
+ 'nsds5replicawaitforasyncresults': '500',
+ 'nsds5replicabusywaittime': '5',
+ 'nsds5replicaSessionPauseTime': '3',
+ 'nsds5replicaflowcontrolwindow': '2000',
+ 'nsds5replicaflowcontrolpause': '4',
+ }
+
+ args = FakeArgs()
+ # Required args
+ args.AGMT_NAME = ['test-timeout-agmt']
+ args.suffix = DEFAULT_SUFFIX
+ args.host = 'localhost'
+ args.port = '33333'
+ args.conn_protocol = 'LDAP'
+ args.bind_dn = 'cn=replmgr,cn=config'
+ args.bind_passwd = 'replmgr'
+ args.bind_method = 'SIMPLE'
+ args.init = False
+
+ # Optional attributes that we're testing
+ args.conn_timeout = EXPECTED['nsds5replicatimeout']
+ args.protocol_timeout = EXPECTED['nsds5replicaprotocoltimeout']
+ args.wait_async_results = EXPECTED['nsds5replicawaitforasyncresults']
+ args.busy_wait_time = EXPECTED['nsds5replicabusywaittime']
+ args.session_pause_time = EXPECTED['nsds5replicaSessionPauseTime']
+ args.flow_control_window = EXPECTED['nsds5replicaflowcontrolwindow']
+ args.flow_control_pause = EXPECTED['nsds5replicaflowcontrolpause']
+
+ # The rest is None
+ args.__class__.__getattr__ = lambda self, name: None
+
+ # Create new agreement with optional attributes
+ add_agmt(inst, None, log, args)
+
+ # Read it back
+ replica = replicas.get(DEFAULT_SUFFIX)
+ agmt = replica.get_agreements().list()[0]
+
+ for attr, expected_val in EXPECTED.items():
+ actual = agmt.get_attr_val_utf8(attr)
+ log.info(f"Checking {attr}: expected={expected_val}, actual={actual}")
+ assert actual == expected_val, \
+ f"Attribute {attr}: expected '{expected_val}', got '{actual}'"
+
+ log.info("All timeout and flow-control attributes verified successfully")
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(f"-s {CURRENT_FILE}")
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index e6bc823c6..f57f34031 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -942,6 +942,20 @@ def add_agmt(inst, basedn, log, args):
properties['nsds5replicatedattributelisttotal'] = frac_total_list
if args.strip_list is not None:
properties['nsds5replicastripattrs'] = args.strip_list
+ if args.conn_timeout is not None:
+ properties['nsds5replicatimeout'] = args.conn_timeout
+ if args.protocol_timeout is not None:
+ properties['nsds5replicaprotocoltimeout'] = args.protocol_timeout
+ if args.wait_async_results is not None:
+ properties['nsds5replicawaitforasyncresults'] = args.wait_async_results
+ if args.busy_wait_time is not None:
+ properties['nsds5replicabusywaittime'] = args.busy_wait_time
+ if args.session_pause_time is not None:
+ properties['nsds5replicaSessionPauseTime'] = args.session_pause_time
+ if args.flow_control_window is not None:
+ properties['nsds5replicaflowcontrolwindow'] = args.flow_control_window
+ if args.flow_control_pause is not None:
+ properties['nsds5replicaflowcontrolpause'] = args.flow_control_pause
# Handle the optional bootstrap settings
if args.bootstrap_bind_dn is not None:
--
2.55.0

View File

@ -0,0 +1,183 @@
From 12c345712da92cc04a9ae0ff52f16847ee0cd00f Mon Sep 17 00:00:00 2001
From: Simon Pichugin <simon.pichugin@gmail.com>
Date: Mon, 17 Aug 2026 17:43:31 -0700
Subject: [PATCH] Issue 7705 - With memberOfEntryScope set, deferred memberOf
skips MODIFY operations (#7706)
Description: deferred_mod_func checks the entry scope against a pre-op entry
it never reads from the task pblock. With memberOfEntryScope set the check
always fails, so the memberOf fanout of every grouping attribute MODIFY
is silently skipped, for direct and replicated operations alike.
Possibly, a regression from the issue 7035 scoping refactor.
Read the post-op entry the way the direct modify path does, and add
a regression test.
Fixes: https://github.com/389ds/389-ds-base/issues/7705
Reviewed by: @tbordaz (Thanks!)
---
.../memberof_deferred_scope_test.py | 129 ++++++++++++++++++
ldap/servers/plugins/memberof/memberof.c | 3 +-
2 files changed, 131 insertions(+), 1 deletion(-)
create mode 100644 dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_scope_test.py
diff --git a/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_scope_test.py b/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_scope_test.py
new file mode 100644
index 000000000..f386fc3e0
--- /dev/null
+++ b/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_scope_test.py
@@ -0,0 +1,129 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import logging
+import pytest
+import os
+import time
+from lib389._constants import DEFAULT_SUFFIX
+from test389.topologies import topology_st as topo
+from lib389.plugins import MemberOfPlugin
+from lib389.idm.user import UserAccounts
+from lib389.idm.group import Groups
+from lib389.idm.nscontainer import nsContainers
+from lib389.utils import get_default_db_lib
+
+log = logging.getLogger(__name__)
+
+EXCLUDED_SUBTREE = f'cn=excluded,{DEFAULT_SUFFIX}'
+
+
+def configure_memberof(inst, scope=None, exclude=None):
+ memberof = MemberOfPlugin(inst)
+ memberof.enable()
+ memberof.set_autoaddoc('nsMemberOf')
+ memberof.set_memberofdeferredupdate('on')
+ memberof.remove_all('memberOfEntryScope')
+ memberof.remove_all('memberOfEntryScopeExcludeSubtree')
+ if scope:
+ memberof.set('memberOfEntryScope', scope)
+ if exclude:
+ memberof.set('memberOfEntryScopeExcludeSubtree', exclude)
+ inst.restart()
+
+
+def wait_for_memberof(user, group_dn, timeout=10):
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
+ values = {v.lower() for v in user.get_attr_vals_utf8('memberOf')}
+ if group_dn.lower() in values:
+ return True
+ time.sleep(0.5)
+ return False
+
+
+@pytest.mark.skipif(get_default_db_lib() == "mdb", reason="Not supported over mdb")
+def test_deferred_update_in_entry_scope(topo):
+ """Deferred memberOf updates are applied when memberOfEntryScope is set
+
+ :id: 2a4877f8-1e63-4a6c-9f35-3c6b0f9d7b41
+ :setup: Standalone Instance
+ :steps:
+ 1. Enable memberOf with deferred updates, memberOfEntryScope set to
+ the suffix and one exclude subtree
+ 2. Create a user and a group inside the scope
+ 3. Add the user as a member of the group
+ 4. Check the user's memberOf attribute
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. memberOf contains the group DN
+ """
+ inst = topo.standalone
+ configure_memberof(inst, scope=DEFAULT_SUFFIX, exclude=EXCLUDED_SUBTREE)
+
+ user = UserAccounts(inst, DEFAULT_SUFFIX).create_test_user(uid=1001)
+ group = Groups(inst, DEFAULT_SUFFIX).create(properties={
+ 'cn': 'deferred_scope_group',
+ 'description': 'group inside the entry scope',
+ })
+ group.add_member(user.dn)
+
+ assert wait_for_memberof(user, group.dn), \
+ 'memberOf was not applied by the deferred update with entry scope set'
+
+
+@pytest.mark.skipif(get_default_db_lib() == "mdb", reason="Not supported over mdb")
+def test_deferred_update_exclude_subtree_honored(topo):
+ """Deferred memberOf updates honor memberOfEntryScopeExcludeSubtree
+
+ :id: 8c9f4b02-55d1-4f7e-b6a9-70d3e2c1a9d4
+ :setup: Standalone Instance
+ :steps:
+ 1. Enable memberOf with deferred updates and only an exclude subtree
+ 2. Create a group inside the excluded subtree, a control group in
+ scope and a user outside the excluded subtree
+ 3. Add the user to the excluded group, then to the control group
+ 4. Wait for the control group's memberOf, then check the user
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. memberOf contains the control group but not the excluded group
+ """
+ inst = topo.standalone
+ configure_memberof(inst, exclude=EXCLUDED_SUBTREE)
+
+ containers = nsContainers(inst, DEFAULT_SUFFIX)
+ if not containers.exists('excluded'):
+ containers.create(properties={'cn': 'excluded'})
+ user = UserAccounts(inst, DEFAULT_SUFFIX).create_test_user(uid=1002)
+ excluded_group = Groups(inst, EXCLUDED_SUBTREE, rdn=None).create(properties={
+ 'cn': 'deferred_excluded_group',
+ 'description': 'group inside the excluded subtree',
+ })
+ control_group = Groups(inst, DEFAULT_SUFFIX).create(properties={
+ 'cn': 'deferred_control_group',
+ 'description': 'control group inside the scope',
+ })
+ excluded_group.add_member(user.dn)
+ control_group.add_member(user.dn)
+
+ # The deferred list is FIFO: once the control group's update is applied,
+ # the excluded group's update has already been processed
+ assert wait_for_memberof(user, control_group.dn), \
+ 'memberOf was not applied for the in-scope control group'
+ values = {v.lower() for v in user.get_attr_vals_utf8('memberOf')}
+ assert excluded_group.dn.lower() not in values, \
+ 'memberOf was applied for a group inside an excluded subtree'
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c
index a2fa4f61c..f71c0220c 100644
--- a/ldap/servers/plugins/memberof/memberof.c
+++ b/ldap/servers/plugins/memberof/memberof.c
@@ -770,6 +770,7 @@ deferred_mod_func(MemberofDeferredModTask *task)
pb = task->pb;
slapi_pblock_get(pb, SLAPI_TARGET_SDN, &sdn);
+ slapi_pblock_get(pb, SLAPI_ENTRY_POST_OP, &post_e);
slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
"deferred_mod_func: target %s\n", slapi_sdn_get_dn(sdn));
/* get the mod set */
@@ -792,7 +793,7 @@ deferred_mod_func(MemberofDeferredModTask *task)
mainConfig = memberof_get_config();
if (memberof_is_grouping_attr(type, mainConfig)) {
interested = 1;
- memberof_set_entry_info(pre_e, NULL, &entry_info);
+ memberof_set_entry_info(post_e, NULL, &entry_info);
if (!memberof_entry_in_scope(mainConfig, &entry_info)) {
/* Entry is not in scope */
memberof_unlock_config();
--
2.55.0

View File

@ -0,0 +1,37 @@
From be51259db9339cab7c7c3f4b48f1f49ee2caea31 Mon Sep 17 00:00:00 2001
From: Akshay Sakure <113896074+asakure@users.noreply.github.com>
Date: Mon, 17 Aug 2026 22:26:44 +0530
Subject: [PATCH] Issue 7711 - Fix typo in accountpolicy --login-history-size
help text (#7713)
Description: This patch corrects the typo in help text for
--login-history-size argument in the Account Policy plugin CLI.
It currently references lastLoginHistSize, which is not a valid
attribute name. The correct attribute name is lastLoginHistorySize.
It also removes an extra stray closing parenthesis in the same string.
Fixes: https://github.com/389ds/389-ds-base/issues/7711
Signed-off-by: Akshay Sakure <asakure@redhat.com>
Reviewed by: mreynolds
---
src/lib389/lib389/cli_conf/plugins/accountpolicy.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib389/lib389/cli_conf/plugins/accountpolicy.py b/src/lib389/lib389/cli_conf/plugins/accountpolicy.py
index 4cfa2718e..d700cd024 100644
--- a/src/lib389/lib389/cli_conf/plugins/accountpolicy.py
+++ b/src/lib389/lib389/cli_conf/plugins/accountpolicy.py
@@ -103,7 +103,7 @@ def _add_parser_args(parser):
parser.add_argument('--state-attr',
help='Specifies the primary time attribute used to evaluate an account policy (stateAttrName)')
parser.add_argument('--login-history-size',
- help='Specifies the number of login timestamps to store (lastLoginHistSize) )')
+ help='Specifies the number of login timestamps to store (lastLoginHistorySize)')
parser.add_argument('--check-all-state-attrs', choices=['yes', 'no'], type=str.lower,
help="Check both state and alternate state attributes for account state")
--
2.55.0

View File

@ -0,0 +1,645 @@
From 7e6b5c6d60d4ab527273936996c9d07ee4ff124f Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 4 Jun 2026 12:32:56 -0400
Subject: [PATCH] Issue 7658 - Heap Buffer Overflow in sasl_io_recv() via
Padded SASL UNBIND
Description:
In sasl_io.c, function sasl_io_recv(), when sasl_io_start_packet() returns
SASL_IO_BUFFER_NOT_ENCRYPTED (triggered by sending an unencrypted LDAP UNBIND
after SASL layer setup) there is no check that
sp->encrypted_buffer_count <= len before the memcpy
relates: https://github.com/389ds/389-ds-base/issues/7658
Reviewed by: jchapman & spichugi(Thanks!)
---
.../suites/sasl/io_overflow_asan_test.py | 372 ++++++++++++++++++
ldap/servers/slapd/sasl_io.c | 159 +++++---
2 files changed, 472 insertions(+), 59 deletions(-)
create mode 100644 dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
diff --git a/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py b/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
new file mode 100644
index 000000000..65fc06303
--- /dev/null
+++ b/dirsrvtests/tests/suites/sasl/io_overflow_asan_test.py
@@ -0,0 +1,372 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import ldap
+import logging
+import pytest
+import os
+import socket
+import struct
+import sys
+import time
+from ldap import sasl as ldap_sasl
+from lib389._constants import DEFAULT_SUFFIX, PASSWORD
+from lib389.idm.user import UserAccounts
+from lib389.utils import check_asan_report
+from test389.topologies import topology_st as topo
+
+log = logging.getLogger(__name__)
+
+pytestmark = pytest.mark.tier1
+
+PADDED_UNBIND_SIZE = 9000
+STALL_PADDED_UNBIND_SIZE = 508
+STALL_IOBLOCK_TIMEOUT_MS = 30000
+STALL_CLIENT_TIMEOUT_SECONDS = 3
+UNBIND_CONTROL_VALUE_SIZE = 512
+
+
+def encode_ber_length(length):
+ """Encode a non-negative BER definite length."""
+ if length < 0x80:
+ return bytes([length])
+
+ encoded = length.to_bytes((length.bit_length() + 7) // 8, 'big')
+ return bytes([0x80 | len(encoded)]) + encoded
+
+
+def encode_ber_element(tag, value):
+ """Encode a single-byte BER tag and its value."""
+ return bytes([tag]) + encode_ber_length(len(value)) + value
+
+
+def build_padded_unbind(padding_size=PADDED_UNBIND_SIZE):
+ """Build an oversized LDAP UNBIND packet.
+
+ Normal UNBIND is 7 bytes:
+ 30 05 SEQUENCE, length 5
+ 02 01 01 INTEGER (msgid) = 1
+ 42 00 UNBIND request (app 2, primitive, length 0)
+
+ Padded UNBIND uses 4-byte BER definite length encoding to include
+ attacker-controlled padding after the UNBIND element. The outer SEQUENCE
+ length encompasses the msgid + unbind + padding. The server reads the
+ full packet into encrypted_buffer (based on the outer BER length), then
+ copies ALL of it into the caller's buf via memcpy without bounds check.
+
+ 30 84 XX XX XX XX SEQUENCE, 4-byte length = 5 + pad_size
+ 02 01 01 INTEGER (msgid) = 1
+ 42 00 UNBIND request
+ [pad_size bytes of padding]
+ """
+ inner = b'\x02\x01\x01' # msgid = 1
+ inner += b'\x42\x00' # UNBIND (application tag 2, primitive, length 0)
+ inner += b'A' * padding_size # attacker-controlled padding
+
+ # SEQUENCE (0x30) with 4-byte definite length encoding (0x84)
+ length = len(inner)
+ packet = b'\x30\x84' + struct.pack('>I', length) + inner
+ return packet
+
+
+def build_controlled_unbind():
+ """Build a valid UNBIND with a noncritical control larger than 512 bytes."""
+ control_oid = encode_ber_element(0x04, b'1.3.6.1.4.1.4203.666.11.999')
+ control_value = encode_ber_element(0x04, b'V' * UNBIND_CONTROL_VALUE_SIZE)
+ control = encode_ber_element(0x30, control_oid + control_value)
+ controls = encode_ber_element(0xa0, control)
+
+ message = b'\x02\x01\x01' # msgid = 1
+ message += b'\x42\x00' # UNBIND
+ message += controls
+ return encode_ber_element(0x30, message)
+
+
+def do_sasl_bind_and_get_fd(host, port, user):
+ """Perform SASL DIGEST-MD5 bind and return the raw socket FD.
+
+ Uses python-ldap for the SASL handshake, then extracts the underlying
+ socket file descriptor. After the SASL bind with SSF > 0, the server
+ has pushed the SASL I/O layer onto the connection.
+ """
+
+ uri = f"ldap://{host}:{port}"
+ log.info(f"[*] Connecting to {uri}")
+
+ conn = ldap.initialize(uri)
+ conn.protocol_version = ldap.VERSION3
+
+ # Set SASL options for DIGEST-MD5 with integrity/confidentiality protection
+ # This ensures SSF > 0, which triggers sasl_io_enable on the server
+ conn.set_option(ldap.OPT_X_SASL_SSF_MIN, 1)
+ conn.set_option(ldap.OPT_X_SASL_SSF_MAX, 256)
+
+ log.info(f"[*] SASL DIGEST-MD5 bind as user: {user}")
+
+ # DIGEST-MD5 SASL bind
+ auth = ldap_sasl.digest_md5(user, PASSWORD)
+ try:
+ conn.sasl_interactive_bind_s("", auth)
+ except ldap.LDAPError as e:
+ log.info(f"[-] SASL bind failed: {e}")
+ sys.exit(1)
+
+ # Verify SSF > 0 (SASL I/O layer is active)
+ ssf = conn.get_option(ldap.OPT_X_SASL_SSF)
+ log.info(f"[+] SASL bind successful, SSF = {ssf}")
+ if ssf == 0:
+ log.info("[-] SSF is 0 -- SASL I/O layer was NOT pushed. Exploit requires SSF > 0.")
+ sys.exit(1)
+
+ # Get the raw socket file descriptor
+ fd = conn.fileno()
+ log.info(f"[+] Raw socket FD: {fd}")
+
+ return conn, fd
+
+
+def send_padded_unbind(fd, padding_size=PADDED_UNBIND_SIZE):
+ """Send a padded UNBIND directly on the raw socket FD.
+
+ This bypasses the SASL framing layer on the CLIENT side. The server's
+ sasl_io_recv will see this as an unencrypted LDAP message (first byte
+ is 0x30 = LDAP_TAG_MESSAGE, not SASL framing).
+
+ The server code path:
+ 1. sasl_io_recv -> sasl_io_start_packet
+ 2. !sp->send_encrypted (PR_FALSE on first read) && *encrypted_buffer == 0x30
+ 3. Enters unencrypted LDAP path
+ 4. Reads full packet into encrypted_buffer (ber_len bytes)
+ 5. Checks tag == LDAP_REQ_UNBIND (0x42) -- passes
+ 6. Sets encrypted_buffer_count = encrypted_buffer_offset = ber_len + 2
+ 7. Returns SASL_IO_BUFFER_NOT_ENCRYPTED
+ 8. sasl_io_recv: memcpy(buf, encrypted_buffer, encrypted_buffer_count)
+ where buf is sized to len (caller's buffer), NOT encrypted_buffer_count
+ 9. OVERFLOW: encrypted_buffer_count (pad_size + 7) >> len (caller's buf size)
+ """
+ packet = build_padded_unbind(padding_size)
+ total_size = len(packet)
+
+ log.info(f"[*] Sending padded UNBIND: {total_size} bytes total")
+ log.info(f"[*] Padding size: {padding_size} bytes of attacker-controlled data")
+ log.info(f"[*] Packet header: {packet[:10].hex()}")
+
+ # Send directly on the raw FD, bypassing SASL wrapper
+ with socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) as sock:
+ sock.sendall(packet)
+ log.info(f"[+] Sent {total_size} bytes on raw socket (bypassing SASL layer)")
+
+ return total_size
+
+
+def send_raw_packet(fd, packet):
+ """Send a complete packet on the raw socket, bypassing the SASL layer."""
+ with socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) as sock:
+ sock.sendall(packet)
+
+
+def wait_for_connection_close(fd):
+ """Wait briefly for the server to close a raw client socket."""
+ with socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) as sock:
+ sock.settimeout(STALL_CLIENT_TIMEOUT_SECONDS)
+ try:
+ data = sock.recv(1)
+ except socket.timeout:
+ pytest.fail(
+ "Plaintext UNBIND stalled in sasl_io_recv instead of being "
+ "drained or rejected"
+ )
+
+ assert data == b'', "Server did not close the connection after UNBIND"
+
+def test_sasl_io_padded_unbind_does_not_stall(topo):
+ """Verify a padded plaintext UNBIND does not stall the SASL I/O layer
+
+ The LDAP connection buffer is 512 bytes. The padded UNBIND is 519 bytes,
+ so sasl_io_recv must return it over multiple calls. The server must drain
+ the already-buffered remainder or reject the PDU instead of waiting for
+ more network data until nsslapd-ioblocktimeout expires.
+
+ :id: ee28256e-5f97-4a19-8178-28d1e372695d
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure a fixed 512-byte connection buffer and a 30-second I/O timeout
+ 2. Create a user and perform a SASL DIGEST-MD5 bind with SSF greater than zero
+ 3. Send a 519-byte plaintext UNBIND directly on the raw socket
+ 4. Wait up to 3 seconds for the server to close the connection
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. The connection closes without waiting for the server I/O timeout
+ """
+
+ inst = topo.standalone
+ inst.config.set('passwordStorageScheme', 'CLEAR')
+ inst.config.set('nsslapd-connection-buffer', '1')
+ inst.config.set('nsslapd-ioblocktimeout', str(STALL_IOBLOCK_TIMEOUT_MS))
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = users.create_test_user(uid=2)
+ user.set('userPassword', PASSWORD)
+
+ conn, fd = do_sasl_bind_and_get_fd(inst.host, inst.port, 'test_user_2')
+ try:
+ send_padded_unbind(fd, STALL_PADDED_UNBIND_SIZE)
+
+ wait_for_connection_close(fd)
+ finally:
+ try:
+ conn.unbind_s()
+ except ldap.LDAPError:
+ pass
+
+
+def test_sasl_io_large_controlled_unbind_is_processed(topo):
+ """Verify a valid plaintext UNBIND larger than the read buffer is processed
+
+ LDAP permits controls on an UNBIND request. The SASL compatibility path must
+ return such a request over multiple recv calls instead of rejecting it based
+ on the size of the current connection read buffer.
+
+ :id: a0496a4c-70e0-4d04-92d6-20f3977489fe
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure a fixed 512-byte connection buffer and unbuffered access logging
+ 2. Create a user and perform a SASL DIGEST-MD5 bind with SSF greater than zero
+ 3. Send a valid plaintext UNBIND containing a large noncritical control
+ 4. Verify the connection closes and the server logs a processed UNBIND
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. The UNBIND is processed normally rather than rejected by the I/O layer
+ """
+
+ inst = topo.standalone
+ inst.config.set('passwordStorageScheme', 'CLEAR')
+ inst.config.set('nsslapd-connection-buffer', '1')
+ inst.config.set('nsslapd-ioblocktimeout', str(STALL_IOBLOCK_TIMEOUT_MS))
+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = users.create_test_user(uid=3)
+ user.set('userPassword', PASSWORD)
+
+ conn, fd = do_sasl_bind_and_get_fd(inst.host, inst.port, 'test_user_3')
+ unbind_count = len(inst.ds_access_log.match('.* UNBIND.*'))
+
+ try:
+ packet = build_controlled_unbind()
+ assert len(packet) > 512
+ send_raw_packet(fd, packet)
+ wait_for_connection_close(fd)
+
+ processed_unbinds = inst.ds_access_log.match('.* UNBIND.*')
+ assert len(processed_unbinds) == unbind_count + 1, (
+ "Large controlled UNBIND did not reach normal UNBIND processing"
+ )
+ finally:
+ try:
+ conn.unbind_s()
+ except ldap.LDAPError:
+ pass
+
+
+def test_sasl_io_overflow(topo):
+ """Verify the SASL I/O layer does not heap-overflow on a padded UNBIND
+
+ After a SASL bind with integrity protection (SSF > 0), the server pushes
+ the SASL I/O shim onto the connection. Send an oversized LDAP UNBIND whose
+ BER length includes attacker-controlled padding, directly on the raw socket
+ so it bypasses client-side SASL framing. The server must handle the
+ unencrypted UNBIND without copying past the caller buffer in sasl_io_recv.
+
+ Requires an ASAN build so a heap-buffer-overflow is reported instead of
+ silent memory corruption.
+
+ :id: 8ef3ea18-2c61-494c-b813-7044d0276adb
+ :setup: Standalone Instance with ASAN enabled
+ :steps:
+ 1. Create a test user with a clear-text password
+ 2. Perform a SASL DIGEST-MD5 bind with SSF > 0
+ 3. Send a padded UNBIND on the raw socket, bypassing SASL framing
+ 4. Read from the connection and verify the server did not crash
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Server remains running and the connection can be used or closed cleanly
+ """
+
+ inst = topo.standalone
+ if not inst.has_asan():
+ pytest.skip("ASAN is not enabled on this server")
+
+ # For digest-md5 we need clear text password
+ inst.config.set('passwordStorageScheme', 'CLEAR')
+
+ # Add a user
+ users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+ user = users.create_test_user(uid=1)
+ user.set('userPassword', PASSWORD)
+ user = 'test_user_1'
+
+ # Step 1: SASL DIGEST-MD5 bind (pushes SASL I/O layer on server)
+ conn, fd = do_sasl_bind_and_get_fd(inst.host, inst.port, user)
+ server_pid = inst.get_pid()
+
+ # Brief pause to ensure server has processed bind response
+ time.sleep(0.5)
+
+ # Step 2: Send padded UNBIND on raw socket (triggers overflow)
+ log.info("")
+ log.info("[*] Sending exploit payload...")
+ log.info(f"[*] The server's sasl_io_recv will memcpy {4096 + 7} bytes")
+ log.info(f"[*] into a buffer likely sized ~4096-8192 bytes (BER read buffer)")
+ log.info(f"[*] Overflow: ~{max(0, PADDED_UNBIND_SIZE - 4096)} bytes past buffer end")
+ log.info("")
+
+ send_padded_unbind(fd)
+
+ log.info("")
+ log.info("[*] Exploit sent. Check server status:")
+ log.info("[*] - PID change indicates crash (heap corruption -> SIGSEGV/SIGABRT)")
+ log.info("[*] - Error log may show ASAN heap-buffer-overflow if built with sanitizers")
+ log.info("[*] - With default (non-ASAN) build, crash may be delayed until next heap op")
+ log.info("")
+
+ try:
+ wait_for_connection_close(fd)
+ finally:
+ try:
+ conn.unbind_s()
+ except ldap.LDAPError:
+ pass
+
+ assert inst.status(), "Server crashed while processing padded UNBIND"
+ assert inst.get_pid() == server_pid, "Server restarted while processing padded UNBIND"
+
+ # Check ASAN report
+ log.info("[*] Checking ASAN report")
+ overflow_detected = False
+ try:
+ overflow_detected = check_asan_report(inst, 'heap-buffer-overflow')
+ except ValueError as e:
+ log.info('No ASAN report found (expected when no overflow): %s', e)
+
+ assert not overflow_detected, 'heap-buffer-overflow detected in ASAN report'
+
+ log.info("[*] Test passed")
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
diff --git a/ldap/servers/slapd/sasl_io.c b/ldap/servers/slapd/sasl_io.c
index 405c9186f..74641e8a9 100644
--- a/ldap/servers/slapd/sasl_io.c
+++ b/ldap/servers/slapd/sasl_io.c
@@ -54,6 +54,7 @@
const char *send_buffer; /* encrypted buffer to send to client */
unsigned int send_size; /* size of the encrypted buffer */
unsigned int send_offset; /* number of bytes sent so far */
+ bool unencrypted_buffer_ready;
};
typedef PRFilePrivate sasl_io_private;
@@ -153,6 +154,34 @@ sasl_io_finished_packet(sasl_io_private *sp)
return (sp->encrypted_buffer_count && (sp->encrypted_buffer_offset == sp->encrypted_buffer_count));
}
+static PRInt32
+sasl_io_drain_unencrypted_buffer(sasl_io_private *sp, void *buf, PRInt32 len)
+{
+ uint32_t bytes_to_copy;
+
+ if (len <= 0) {
+ return 0;
+ }
+
+ PR_ASSERT(sp->unencrypted_buffer_ready);
+ PR_ASSERT(sp->encrypted_buffer_offset <= sp->encrypted_buffer_count);
+
+ bytes_to_copy = sp->encrypted_buffer_count - sp->encrypted_buffer_offset;
+ if (bytes_to_copy > (uint32_t)len) {
+ bytes_to_copy = (uint32_t)len;
+ }
+ memcpy(buf, sp->encrypted_buffer + sp->encrypted_buffer_offset, bytes_to_copy);
+ sp->encrypted_buffer_offset += bytes_to_copy;
+
+ if (sp->encrypted_buffer_offset == sp->encrypted_buffer_count) {
+ sp->encrypted_buffer_offset = 0;
+ sp->encrypted_buffer_count = 0;
+ sp->unencrypted_buffer_ready = false;
+ }
+
+ return (PRInt32)bytes_to_copy;
+}
+
static const char *const sasl_LayerName = "SASL";
static PRDescIdentity sasl_LayerID;
static PRIOMethods sasl_IoMethods;
@@ -190,34 +219,33 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt
*err = 0;
debug_print_layers(fd);
- /* first we need the length bytes */
- ret = PR_Recv(fd->lower, buffer, amount, flags, timeout);
- slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
- "Read sasl packet length returned %d on connection %" PRIu64 "\n",
- ret, c->c_connid);
- if (ret <= 0) {
- *err = PR_GetError();
- if (ret == 0) {
- slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
- "Connection closed while reading sasl packet length on connection %" PRIu64 "\n",
- c->c_connid);
- } else {
+ /* First read enough bytes to distinguish an LDAP message from a SASL packet. */
+ if (sp->encrypted_buffer_offset < sizeof(buffer)) {
+ amount = sizeof(buffer) - sp->encrypted_buffer_offset;
+ ret = PR_Recv(fd->lower, buffer, amount, flags, timeout);
slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
- "Error reading sasl packet length on connection %" PRIu64 " %d:%s\n",
- c->c_connid, *err, slapd_pr_strerror(*err));
+ "Read sasl packet length returned %d on connection %" PRIu64 "\n",
+ ret, c->c_connid);
+ if (ret <= 0) {
+ *err = PR_GetError();
+ if (ret == 0) {
+ slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
+ "Connection closed while reading sasl packet length on connection %" PRIu64 "\n",
+ c->c_connid);
+ } else {
+ slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
+ "Error reading sasl packet length on connection %" PRIu64 " %d:%s\n",
+ c->c_connid, *err, slapd_pr_strerror(*err));
+ }
+ return ret;
}
- return ret;
- }
- /*
- * Read the bytes and add them to sp->encrypted_buffer
- * - if offset < 7, tell caller we didn't read enough bytes yet
- * - if offset >= 7, decode the length and proceed.
- */
- if ((ret + sp->encrypted_buffer_offset) > sp->encrypted_buffer_size) {
- sasl_io_resize_encrypted_buffer(sp, ret + sp->encrypted_buffer_offset);
+ if ((ret + sp->encrypted_buffer_offset) > sp->encrypted_buffer_size) {
+ sasl_io_resize_encrypted_buffer(sp, ret + sp->encrypted_buffer_offset);
+ }
+ memcpy(sp->encrypted_buffer + sp->encrypted_buffer_offset, buffer, ret);
+ sp->encrypted_buffer_offset += ret;
}
- memcpy(sp->encrypted_buffer + sp->encrypted_buffer_offset, buffer, ret);
- sp->encrypted_buffer_offset += ret;
+
if (sp->encrypted_buffer_offset < sizeof(buffer)) {
slapi_log_err(SLAPI_LOG_CONNS,
"sasl_io_start_packet", "Read only %d bytes of sasl packet "
@@ -242,9 +270,11 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt
ber_len_t maxbersize = config_get_maxbersize();
ber_len_t ber_len = 0;
ber_tag_t tag = 0;
+ uint32_t ber_header_len = 2;
+ uint32_t ber_packet_len;
- slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet", "conn=%" PRIu64 " fd=%d "
- "Sent an LDAP message that was not encrypted.\n",
+ slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
+ "conn=%" PRIu64 " fd=%d Sent an LDAP message that was not encrypted.\n",
c->c_connid,
c->c_sd);
@@ -265,11 +295,22 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt
PR_SetError(PR_IO_ERROR, 0);
return PR_FAILURE;
}
- /*
- * Bump the ber length by 2 for the tag/length we skipped over when calculating the berval length.
- * We now have the total "packet" size, so we know exactly what is left to read in.
- */
- ber_len += 2;
+ /* Include the tag and the complete short- or long-form BER length. */
+ if (((unsigned char *)sp->encrypted_buffer)[1] & 0x80U) {
+ ber_header_len += ((unsigned char *)sp->encrypted_buffer)[1] & 0x7fU;
+ }
+ if (ber_len > (ber_len_t)(UINT32_MAX - ber_header_len)) {
+ slapi_log_err(SLAPI_LOG_ERR, "sasl_io_start_packet",
+ "conn=%" PRIu64 " fd=%d Incoming BER Element length cannot be represented.\n",
+ c->c_connid, c->c_sd);
+ PR_SetError(PR_BUFFER_OVERFLOW_ERROR, 0);
+ return PR_FAILURE;
+ }
+ ber_packet_len = (uint32_t)ber_len + ber_header_len;
+ if (ber_packet_len < sp->encrypted_buffer_offset) {
+ goto done;
+ }
+ sasl_io_resize_encrypted_buffer(sp, ber_packet_len);
/*
* Read in the rest of the packet.
@@ -278,39 +319,35 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt
* to the buffer. Once we have the complete LDAP packet we'll set it back to zero,
* and adjust the sp->encrypted_buffer_count.
*/
- while (sp->encrypted_buffer_offset < ber_len) {
+ while (sp->encrypted_buffer_offset < ber_packet_len) {
+ uint32_t bytes_to_read = ber_packet_len - sp->encrypted_buffer_offset;
unsigned char mybuf[SASL_IO_BUFFER_SIZE];
- ret = PR_Recv(fd->lower, mybuf, SASL_IO_BUFFER_SIZE, flags, timeout);
- if (ret == PR_WOULD_BLOCK_ERROR || (ret == 0 && sp->encrypted_buffer_offset < ber_len)) {
-/*
- * Need more data, go back and try to get more data from connection_read_operation()
- * We can return and continue to update sp->encrypted_buffer because we have
- * maintained the current size in encrypted_buffer_offset.
- */
-#if defined(EWOULDBLOCK)
- errno = EWOULDBLOCK;
-#elif defined(EAGAIN)
- errno = EAGAIN;
-#endif
- PR_SetError(PR_WOULD_BLOCK_ERROR, errno);
- return PR_FAILURE;
- } else if (ret > 0) {
+ if (bytes_to_read > sizeof(mybuf)) {
+ bytes_to_read = sizeof(mybuf);
+ }
+
+ ret = PR_Recv(fd->lower, mybuf, (PRInt32)bytes_to_read, flags, timeout);
+ if (ret > 0) {
slapi_log_err(SLAPI_LOG_CONNS,
"sasl_io_start_packet",
"Continued: read sasl packet length returned %d on connection %" PRIu64 "\n",
ret, c->c_connid);
- if ((ret + sp->encrypted_buffer_offset) > sp->encrypted_buffer_size) {
- sasl_io_resize_encrypted_buffer(sp, ret + sp->encrypted_buffer_offset);
- }
memcpy(sp->encrypted_buffer + sp->encrypted_buffer_offset, mybuf, ret);
sp->encrypted_buffer_offset += ret;
- } else if (ret < 0) {
+ } else if (ret == 0) {
*err = PR_GetError();
slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
- "Error reading sasl packet length on connection "
- "%" PRIu64 " %d:%s\n",
- c->c_connid, *err, slapd_pr_strerror(*err));
+ "Connection closed while reading an LDAP packet on connection %" PRIu64 "\n",
+ c->c_connid);
+ return ret;
+ } else {
+ *err = PR_GetError();
+ if (*err != PR_WOULD_BLOCK_ERROR) {
+ slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_start_packet",
+ "Error reading LDAP packet on connection %" PRIu64 " %d:%s\n",
+ c->c_connid, *err, slapd_pr_strerror(*err));
+ }
return ret;
}
}
@@ -345,6 +382,7 @@ sasl_io_start_packet(PRFileDesc *fd, PRIntn flags, PRIntervalTime timeout, PRInt
c->c_sd);
sp->encrypted_buffer_count = sp->encrypted_buffer_offset;
sp->encrypted_buffer_offset = 0;
+ sp->unencrypted_buffer_ready = true;
ber_free(ber, 1);
return SASL_IO_BUFFER_NOT_ENCRYPTED;
}
@@ -444,6 +482,10 @@ sasl_io_recv(PRFileDesc *fd, void *buf, PRInt32 len, PRIntn flags, PRIntervalTim
uint32_t bytes_in_buffer = 0;
int32_t err = 0;
+ if (sp->unencrypted_buffer_ready) {
+ return sasl_io_drain_unencrypted_buffer(sp, buf, len);
+ }
+
/* Do we have decrypted data buffered from 'before' ? */
bytes_in_buffer = sp->decrypted_buffer_count - sp->decrypted_buffer_offset;
slapi_log_err(SLAPI_LOG_CONNS, "sasl_io_recv",
@@ -459,11 +501,10 @@ sasl_io_recv(PRFileDesc *fd, void *buf, PRInt32 len, PRIntn flags, PRIntervalTim
ret = sasl_io_start_packet(fd, flags, timeout, &err);
if (SASL_IO_BUFFER_NOT_ENCRYPTED == ret) {
/*
- * Special case: we received unencrypted data that was actually
- * an unbind. Copy it to the buffer and return its length.
+ * Special case: return the validated unencrypted UNBIND over as
+ * many calls as the caller's buffer requires.
*/
- memcpy(buf, sp->encrypted_buffer, sp->encrypted_buffer_count);
- return sp->encrypted_buffer_count;
+ return sasl_io_drain_unencrypted_buffer(sp, buf, len);
}
if (0 >= ret) {
/* timeout, connection closed, or error */
--
2.55.0

View File

@ -0,0 +1,50 @@
From f37eb3fb87a3955ab1a273a2951e5b12a1c46c83 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Thu, 26 Feb 2026 12:55:47 +0000
Subject: [PATCH] Issue 7284 - Creating local password policy succeeds with
incorrect passwordInHistory value (#7285)
Description:
attr_check_minmax used strtol(value, NULL, 0), which silently converted
invalid strings to 0, passing subsequent range checks.
Fix:
Add checks for NULL or empty values and uses strtol with endptr to
validate int input before range checks.
Fixes: https://github.com/389ds/389-ds-base/issues/7284
Reviewed by: @vashirov, @tbordaz (Thank you)
---
ldap/servers/slapd/attr.c | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/ldap/servers/slapd/attr.c b/ldap/servers/slapd/attr.c
index f5ea22eb5..ca2eb62c3 100644
--- a/ldap/servers/slapd/attr.c
+++ b/ldap/servers/slapd/attr.c
@@ -932,8 +932,20 @@ attr_check_minmax(const char *attr_name, char *value, long minval, long maxval,
{
int retVal = LDAP_SUCCESS;
long val;
+ char *endptr = NULL;
+
+ if (!value || *value == '\0') {
+ slapi_create_errormsg(errorbuf, ebuflen, "%s: attr value is NULL.", attr_name);
+ return LDAP_CONSTRAINT_VIOLATION;
+ }
+
+ errno = 0;
+ val = strtol(value, &endptr, 0);
+ if (endptr == value || *endptr != '\0' || errno != 0) {
+ slapi_create_errormsg(errorbuf, ebuflen, "%s: invalid value \"%s\".", attr_name, value);
+ return LDAP_CONSTRAINT_VIOLATION;
+ }
- val = strtol(value, NULL, 0);
if ((minval != -1 ? (val < minval ? 1 : 0) : 0) ||
(maxval != -1 ? (val > maxval ? 1 : 0) : 0)) {
slapi_create_errormsg(errorbuf, ebuflen, "%s: invalid value \"%s\".", attr_name, value);
--
2.55.0

View File

@ -0,0 +1,39 @@
From e0f7cd98f9cc80ac2b7c3eed0f663c1c25de0e3f Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Thu, 26 Mar 2026 21:37:11 +0530
Subject: [PATCH] Issue 7284 - CI - Fix test_grace_limit_section after pwpolicy
validation fix (#7357)
Description:
The test replaced passwordGraceLimit with a space to remove it, which is
now correctly rejected after attr_check_minmax validation was tightened.
Use remove_all() to properly delete the attribute instead.
Fixes: #7284
Reviewed by: progier389, droideck
---
dirsrvtests/tests/suites/password/password_policy_test.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/password_policy_test.py b/dirsrvtests/tests/suites/password/password_policy_test.py
index 8622ea0cb..20e2fa973 100644
--- a/dirsrvtests/tests/suites/password/password_policy_test.py
+++ b/dirsrvtests/tests/suites/password/password_policy_test.py
@@ -1427,8 +1427,11 @@ def test_grace_limit_section(topo, policy_setup, _fixture_for_grace_limit):
for att1, att2 in [('passwordGraceLimit', '10')]:
_do_transaction_for_pwp(topo, att1, att2)
# removing the passwordgracelimit attribute should make it default to 0
- for att1, att2 in [('passwordGraceLimit', ' ')]:
- _do_transaction_for_pwp(topo, att1, att2)
+ pwp = PwPolicyManager(topo.standalone)
+ for dn in [f'uid=orla,ou=dirsec,{DEFAULT_SUFFIX}',
+ f'uid=joe,ou=people,{DEFAULT_SUFFIX}',
+ f'ou=people,{DEFAULT_SUFFIX}']:
+ pwp.get_pwpolicy_entry(dn).remove_all('passwordGraceLimit')
change_password_with_admin(topo, [
('uid=orla,ou=dirsec', '000rLb1'),
('uid=joe,ou=people', '00J0e1'),
--
2.55.0

View File

@ -0,0 +1,91 @@
From e362965090ae7687318ce94345e5bf7278073051 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <mirielka@users.noreply.github.com>
Date: Wed, 8 Jul 2026 14:39:38 +0200
Subject: [PATCH] Issue 7284 - Automated test for creating local password
policy with incorrect passwordInHistory value (#7608)
Description:
Adding automated test for creating local password policy with incorrect passwordInHistory value
Relates: #7284
Author: Lenka Doudova
Assisted by: Cursor
Reviewer: @progier389
---
.../suites/password/password_policy_test.py | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/dirsrvtests/tests/suites/password/password_policy_test.py b/dirsrvtests/tests/suites/password/password_policy_test.py
index 20e2fa973..709e30b1c 100644
--- a/dirsrvtests/tests/suites/password/password_policy_test.py
+++ b/dirsrvtests/tests/suites/password/password_policy_test.py
@@ -1563,6 +1563,66 @@ def test_additional_corner_cases(topo, policy_setup, _fixture_for_additional_cas
])
+@pytest.mark.parametrize('value,result',
+ [('0', ldap.SUCCESS),
+ ('24', ldap.SUCCESS),
+ pytest.param('-1', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284')),
+ pytest.param('30', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284')),
+ pytest.param('a', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284'))])
+def test_create_local_pwp_with_passwordInHistory(topo, value, result):
+ """Verify local password policy passwordInHistory accepts only values 0-24
+
+ :id: e7c4a1b2-3d5f-4a8e-9c1b-2f6e8d4a7b03
+ :parametrized: yes
+ :setup: Standalone instance
+ :steps:
+ 1. Enable nsslapd-pwpolicy-local
+ 2. Create a dedicated OU under the default suffix
+ 3. Create a subtree local password policy with passwordInHistory set to
+ value using PwPolicyManager.create_subtree_policy
+ 4. For successful creation, verify passwordInHistory on the policy entry
+ 5. Delete the local password policy and OU
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success for value 0 or 24; CONSTRAINT_VIOLATION for -1, 30, or a
+ (invalid cases marked xfail)
+ 4. passwordInHistory matches value when creation succeeds
+ 5. Success
+ """
+ inst = topo.standalone
+ inst.config.replace('nsslapd-pwpolicy-local', 'on')
+
+ ous = OrganizationalUnits(inst, DEFAULT_SUFFIX)
+ ou = ous.create(properties={'ou': f'pwpinhist{value}'})
+
+ pwp = PwPolicyManager(inst)
+
+ try:
+ if result == ldap.SUCCESS:
+ policy_entry = pwp.create_subtree_policy(ou.dn, {'passwordInHistory': value})
+ assert policy_entry.get_attr_val_utf8('passwordInHistory') == value
+ else:
+ with pytest.raises(result):
+ pwp.create_subtree_policy(ou.dn, {'passwordInHistory': value})
+ except ldap.LDAPError:
+ raise
+ finally:
+ try:
+ pwp.delete_local_policy(ou.dn)
+ except ValueError:
+ container = nsContainer(inst, f'cn=nsPwPolicyContainer,{ou.dn}')
+ try:
+ if container.exists():
+ container.delete()
+ except ldap.LDAPError:
+ pass
+ try:
+ ou.delete()
+ except ldap.LDAPError:
+ pass
+
+
def test_get_pwpolicy_cn_with_quotes(topology_m1, policy_qoutes_setup):
"""Test that that we can get pwpolicy when
cn attr includes quotes
--
2.55.0

View File

@ -0,0 +1,319 @@
From e2f152496052de949ffb727c354b00b4956d9aed Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Mon, 27 Jul 2026 09:58:51 +0100
Subject: [PATCH] Issue 7284 - Creating local password policy succeeds with
incorrect passwordInHistory value (#7662)
Description:
Creating a local password policy accepts invalid passwordrInHistory values.
Updating an existing policy via dsconf localpwp set correctly rejects the
same values with Constraint violation.
Fine grained password policy validation ran only on the modify path. Local
policy create uses the add path, which wasnt updated, so invalid values were
accepted.
Fix:
Add fine grained password policy attribute validation to the ADD path,
sharing the same checks used by MODIFY.
Fixes: https://github.com/389ds/389-ds-base/issues/7284
Reviewed by: @mreynolds389 (Thank you)
---
.../suites/password/password_policy_test.py | 6 +-
ldap/servers/slapd/add.c | 8 ++
ldap/servers/slapd/modify.c | 70 +---------
ldap/servers/slapd/pw.c | 123 ++++++++++++++++++
ldap/servers/slapd/pw.h | 2 +
5 files changed, 142 insertions(+), 67 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/password_policy_test.py b/dirsrvtests/tests/suites/password/password_policy_test.py
index 709e30b1c..4c4d2f6e4 100644
--- a/dirsrvtests/tests/suites/password/password_policy_test.py
+++ b/dirsrvtests/tests/suites/password/password_policy_test.py
@@ -1566,9 +1566,9 @@ def test_additional_corner_cases(topo, policy_setup, _fixture_for_additional_cas
@pytest.mark.parametrize('value,result',
[('0', ldap.SUCCESS),
('24', ldap.SUCCESS),
- pytest.param('-1', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284')),
- pytest.param('30', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284')),
- pytest.param('a', ldap.CONSTRAINT_VIOLATION, marks=pytest.mark.xfail(reason='https://github.com/389ds/389-ds-base/issues/7284'))])
+ ('-1', ldap.CONSTRAINT_VIOLATION),
+ ('30', ldap.CONSTRAINT_VIOLATION),
+ ('a', ldap.CONSTRAINT_VIOLATION)])
def test_create_local_pwp_with_passwordInHistory(topo, value, result):
"""Verify local password policy passwordInHistory accepts only values 0-24
diff --git a/ldap/servers/slapd/add.c b/ldap/servers/slapd/add.c
index 2f0d57ffa..209048333 100644
--- a/ldap/servers/slapd/add.c
+++ b/ldap/servers/slapd/add.c
@@ -730,6 +730,14 @@ op_shared_add(Slapi_PBlock *pb)
}
/* expand objectClass values to reflect the inheritance hierarchy */
slapi_schema_expand_objectclasses(e);
+
+ /* Validate password policy attrs */
+ if (!internal_op) {
+ if ((err = check_pw_policy_attrs(e, NULL, errorbuf, sizeof(errorbuf))) != LDAP_SUCCESS) {
+ send_ldap_result(pb, err, NULL, errorbuf, 0, NULL);
+ goto done;
+ }
+ }
}
/*
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
index be93e0cd6..152eb5e9f 100644
--- a/ldap/servers/slapd/modify.c
+++ b/ldap/servers/slapd/modify.c
@@ -66,34 +66,6 @@ mod_op_image(int op)
}
#endif
-/* an AttrCheckFunc function should return an LDAP result code (LDAP_SUCCESS if all goes well). */
-typedef int (*AttrCheckFunc)(const char *attr_name, char *value, long minval, long maxval, char *errorbuf, size_t ebuflen);
-
-static struct attr_value_check
-{
- const char *attr_name; /* the name of the attribute */
- AttrCheckFunc checkfunc;
- long minval;
- long maxval;
-} AttrValueCheckList[] = {
- {CONFIG_PW_SYNTAX_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_CHANGE_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_LOCKOUT_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_MUSTCHANGE_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_EXP_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_UNLOCK_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_HISTORY_ATTRIBUTE, attr_check_onoff, 0, 0},
- {CONFIG_PW_MINAGE_ATTRIBUTE, check_pw_duration_value, -1, -1},
- {CONFIG_PW_WARNING_ATTRIBUTE, check_pw_duration_value, 0, -1},
- {CONFIG_PW_MINLENGTH_ATTRIBUTE, attr_check_minmax, 2, 512},
- {CONFIG_PW_MAXFAILURE_ATTRIBUTE, attr_check_minmax, 1, 32767},
- {CONFIG_PW_INHISTORY_ATTRIBUTE, attr_check_minmax, 0, 24},
- {CONFIG_PW_LOCKDURATION_ATTRIBUTE, check_pw_duration_value, -1, -1},
- {CONFIG_PW_RESETFAILURECOUNT_ATTRIBUTE, check_pw_resetfailurecount_value, -1, -1},
- {CONFIG_PW_GRACELIMIT_ATTRIBUTE, attr_check_minmax, 0, -1},
- {CONFIG_PW_STORAGESCHEME_ATTRIBUTE, check_pw_storagescheme_value, -1, -1},
- {CONFIG_PW_MAXAGE_ATTRIBUTE, check_pw_duration_value, -1, -1}};
-
/* This function is called to process operation that come over external connections */
void
do_modify(Slapi_PBlock *pb)
@@ -662,7 +634,6 @@ op_shared_modify(Slapi_PBlock *pb, int pw_change, char *old_pw)
int err;
LDAPMod *lc_mod = NULL;
struct slapdplugin *p = NULL;
- int numattr;
char *proxydn = NULL;
int proxy_err = LDAP_SUCCESS;
char *errtext = NULL;
@@ -772,42 +743,13 @@ op_shared_modify(Slapi_PBlock *pb, int pw_change, char *old_pw)
slapi_pblock_set(pb, SLAPI_BACKEND, be);
- /* The following section checks the valid values of fine-grained
- * password policy attributes.
- * 1. First, it checks if the entry has "passwordpolicy" objectclass.
- * 2. If yes, then if the mods contain any passwdpolicy specific attributes.
- * 3. If yes, then it invokes corrosponding checking function.
- */
+ /* Validate password policy attrs */
if (!repl_op && !internal_op && normdn && slapi_search_get_entry(&entry_pb, sdn, NULL, &e, NULL) == LDAP_SUCCESS) {
- Slapi_Value target;
- slapi_value_init(&target);
- slapi_value_set_string(&target, "passwordpolicy");
- if ((slapi_entry_attr_has_syntax_value(e, "objectclass", &target)) == 1) {
- numattr = sizeof(AttrValueCheckList) / sizeof(AttrValueCheckList[0]);
- while (tmpmods && *tmpmods) {
- if ((*tmpmods)->mod_bvalues != NULL &&
- !SLAPI_IS_MOD_DELETE((*tmpmods)->mod_op)) {
- for (size_t i = 0; i < numattr; i++) {
- if (slapi_attr_type_cmp((*tmpmods)->mod_type,
- AttrValueCheckList[i].attr_name, SLAPI_TYPE_CMP_SUBTYPE) == 0) {
- /* The below function call is good for
- * single-valued attrs only
- */
- if ((err = AttrValueCheckList[i].checkfunc(AttrValueCheckList[i].attr_name,
- (*tmpmods)->mod_bvalues[0]->bv_val, AttrValueCheckList[i].minval,
- AttrValueCheckList[i].maxval, errorbuf, sizeof(errorbuf))) != LDAP_SUCCESS) {
- /* return error */
- send_ldap_result(pb, err, NULL, errorbuf, 0, NULL);
- goto free_and_return;
- }
- }
- }
- }
- tmpmods++;
- } /* end of (while */
- } /* end of if (found */
- value_done(&target);
- } /* end of if (!repl_op */
+ if ((err = check_pw_policy_attrs(e, tmpmods, errorbuf, sizeof(errorbuf))) != LDAP_SUCCESS) {
+ send_ldap_result(pb, err, NULL, errorbuf, 0, NULL);
+ goto free_and_return;
+ }
+ }
/* can get lastmod only after backend is selected */
slapi_pblock_get(pb, SLAPI_BE_LASTMOD, &lastmod);
diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c
index e81f10920..07e1e5fe7 100644
--- a/ldap/servers/slapd/pw.c
+++ b/ldap/servers/slapd/pw.c
@@ -79,6 +79,7 @@
*/
#include <stdio.h>
+#include <stdbool.h>
#include <string.h>
#include <sys/types.h>
#include <sechash.h>
@@ -2691,6 +2692,128 @@ check_pw_storagescheme_value(const char *attr_name __attribute__((unused)), char
return retVal;
}
+
+ /* pwpolicy_attr_check_fn function should return an LDAP result code (LDAP_SUCCESS if all goes well), shared by ADD and MODIFY */
+typedef int (*pwpolicy_attr_check_fn)(const char *attr_name, char *value, long minval, long maxval, char *errorbuf, size_t ebuflen);
+
+static const struct pwpolicy_attr_value_check
+{
+ const char *attr_name;
+ pwpolicy_attr_check_fn checkfunc;
+ long minval;
+ long maxval;
+} pwpolicy_attr_value_checklist[] = {
+ {CONFIG_PW_SYNTAX_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_CHANGE_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_LOCKOUT_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_MUSTCHANGE_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_EXP_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_UNLOCK_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_HISTORY_ATTRIBUTE, attr_check_onoff, 0, 0},
+ {CONFIG_PW_MINAGE_ATTRIBUTE, check_pw_duration_value, -1, -1},
+ {CONFIG_PW_WARNING_ATTRIBUTE, check_pw_duration_value, 0, -1},
+ {CONFIG_PW_MINLENGTH_ATTRIBUTE, attr_check_minmax, 2, 512},
+ {CONFIG_PW_MAXFAILURE_ATTRIBUTE, attr_check_minmax, 1, 32767},
+ {CONFIG_PW_INHISTORY_ATTRIBUTE, attr_check_minmax, 0, 24},
+ {CONFIG_PW_LOCKDURATION_ATTRIBUTE, check_pw_duration_value, -1, -1},
+ {CONFIG_PW_RESETFAILURECOUNT_ATTRIBUTE, check_pw_resetfailurecount_value, -1, -1},
+ {CONFIG_PW_GRACELIMIT_ATTRIBUTE, attr_check_minmax, 0, -1},
+ {CONFIG_PW_STORAGESCHEME_ATTRIBUTE, check_pw_storagescheme_value, -1, -1},
+ {CONFIG_PW_MAXAGE_ATTRIBUTE, check_pw_duration_value, -1, -1}};
+
+#define PWPOLICY_ATTR_CHECK_COUNT \
+ (sizeof(pwpolicy_attr_value_checklist) / sizeof(pwpolicy_attr_value_checklist[0]))
+
+/* Local password policies only. */
+static bool
+entry_is_pwpolicy(Slapi_Entry *e)
+{
+ Slapi_Value target;
+ bool is_pwp;
+
+ if (e == NULL) {
+ return false;
+ }
+
+ slapi_value_init(&target);
+ slapi_value_set_string(&target, "passwordpolicy");
+ is_pwp = (slapi_entry_attr_has_syntax_value(e, "objectclass", &target) == 1);
+ value_done(&target);
+ return is_pwp;
+}
+
+/* Validate a single attr against the checklist */
+static int
+check_pwpolicy_attr_value(const char *attr_type, char *value, char *errorbuf, size_t ebuflen)
+{
+ size_t i;
+
+ if (attr_type == NULL || value == NULL) {
+ return LDAP_SUCCESS;
+ }
+
+ for (i = 0; i < PWPOLICY_ATTR_CHECK_COUNT; i++) {
+ const struct pwpolicy_attr_value_check *c = &pwpolicy_attr_value_checklist[i];
+
+ if (slapi_attr_type_cmp(attr_type, c->attr_name, SLAPI_TYPE_CMP_SUBTYPE) == 0) {
+ return c->checkfunc(c->attr_name, value, c->minval, c->maxval, errorbuf, ebuflen);
+ }
+ }
+
+ return LDAP_SUCCESS;
+}
+
+/* Passwordpolicy attr validation for ADD and MODIFY */
+int
+check_pw_policy_attrs(Slapi_Entry *e, LDAPMod **mods, char *errorbuf, size_t ebuflen)
+{
+ if (!entry_is_pwpolicy(e)) {
+ return LDAP_SUCCESS;
+ }
+
+ /* Modify */
+ if (mods != NULL) {
+ for (; *mods != NULL; mods++) {
+ int err;
+
+ if ((*mods)->mod_bvalues == NULL || SLAPI_IS_MOD_DELETE((*mods)->mod_op)) {
+ continue;
+ }
+ err = check_pwpolicy_attr_value((*mods)->mod_type,
+ (*mods)->mod_bvalues[0]->bv_val,
+ errorbuf, ebuflen);
+ if (err != LDAP_SUCCESS) {
+ return err;
+ }
+ }
+ return LDAP_SUCCESS;
+ }
+
+ /* Add */
+ {
+ Slapi_Attr *attr = NULL;
+ char *type = NULL;
+
+ for (slapi_entry_first_attr(e, &attr); attr;
+ slapi_entry_next_attr(e, attr, &attr)) {
+ Slapi_Value *val = NULL;
+ int err;
+
+ slapi_attr_get_type(attr, &type);
+ if (slapi_attr_first_value(attr, &val) == -1 || val == NULL) {
+ continue;
+ }
+ err = check_pwpolicy_attr_value(type, (char *)slapi_value_get_string(val),
+ errorbuf, ebuflen);
+ if (err != LDAP_SUCCESS) {
+ return err;
+ }
+ }
+ }
+
+ return LDAP_SUCCESS;
+}
+
/* Before bind operation, check if the bind_target_entry has not overpass TPR limits
* returns:
* 0: TPR limits not enforced or reached
diff --git a/ldap/servers/slapd/pw.h b/ldap/servers/slapd/pw.h
index b2fd7c784..a39978805 100644
--- a/ldap/servers/slapd/pw.h
+++ b/ldap/servers/slapd/pw.h
@@ -40,6 +40,8 @@ void delete_passwdPolicy(struct passwordpolicyarray **pwpolicy);
int check_pw_duration_value(const char *attr_name, char *value, long minval, long maxval, char *errorbuf, size_t ebuflen);
int check_pw_resetfailurecount_value(const char *attr_name, char *value, long minval, long maxval, char *errorbuf, size_t ebuflen);
int check_pw_storagescheme_value(const char *attr_name, char *value, long minval, long maxval, char *errorbuf, size_t ebuflen);
+/* Passwordpolicy attr validation for ADD and MODIFY */
+int check_pw_policy_attrs(Slapi_Entry *e, LDAPMod **mods, char *errorbuf, size_t ebuflen);
int pw_is_pwp_admin(Slapi_PBlock *pb, struct passwordpolicyarray *pwp, int rootdn_flag);
#define PWP_ADMIN_OR_ROOTDN 0
--
2.55.0

View File

@ -0,0 +1,94 @@
From 3a461582c0e587b33196a3fd880cabde6a23ff9d Mon Sep 17 00:00:00 2001
From: Masahiro Matsuya <mmatsuya@redhat.com>
Date: Wed, 19 Aug 2026 17:31:10 +0900
Subject: [PATCH] Issue 7707 - lib389: set nsDS5ReplicaBindDNGroup before
ensure_agreement() in join_supplier/hub/consumer (#7708)
Bug Description:
join_supplier(), join_hub(), and join_consumer() set nsDS5ReplicaBindDNGroup
on the consumer after calling ensure_agreement(). The supplier's replication
thread starts immediately when the agreement is created and may send a
startReplication request before the bind DN group is configured, causing
check_replica_auth() to reject it with LDAP_INSUFFICIENT_ACCESS and entering
a permanent 'requires administrator action' state with no retry.
Fix Description:
Move nsDS5ReplicaBindDNGroup before ensure_agreement() in all three call sites
and add an explanatory comment to prevent future regressions.
Fixes: https://github.com/389ds/389-ds-base/issues/7707
Author: Masahiro Matsuya <mmatsuya@redhat.com>
Reviewed by: @tbordaz, @mreynolds389
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
src/lib389/lib389/replica.py | 24 +++++++++++++++---------
1 file changed, 15 insertions(+), 9 deletions(-)
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index 78d6eb4eb..a3bea4a4b 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -2195,14 +2195,16 @@ class ReplicationManager(object):
# to allow the tot_init to occur.
self._bootstrap_replica(from_r, to_r, to_instance)
+ # Set bind DN group BEFORE creating agreements: the supplier's replication
+ # thread starts immediately on agreement creation and will fail auth permanently
+ # if nsDS5ReplicaBindDNGroup is not yet configured on the consumer.
+ to_r.set('nsDS5ReplicaBindDNGroup', repl_dn)
+
# Now put in an agreement from to -> from
# both ends.
self.ensure_agreement(from_instance, to_instance)
self.ensure_agreement(to_instance, from_instance, init=True)
- # Now fix our replica credentials from -> to
- to_r.set('nsDS5ReplicaBindDNGroup', repl_dn)
-
# Now finally test it ...
self.test_replication(from_instance, to_instance)
self.test_replication(to_instance, from_instance)
@@ -2252,13 +2254,15 @@ class ReplicationManager(object):
# to allow the tot_init to occur.
self._bootstrap_replica(from_r, to_r, to_instance)
+ # Set bind DN group BEFORE creating agreements: the supplier's replication
+ # thread starts immediately on agreement creation and will fail auth permanently
+ # if nsDS5ReplicaBindDNGroup is not yet configured on the consumer.
+ to_r.set('nsDS5ReplicaBindDNGroup', repl_dn)
+
# Now put in an agreement from to -> from
# both ends.
self.ensure_agreement(from_instance, to_instance)
- # Now fix our replica credentials from -> to
- to_r.set('nsDS5ReplicaBindDNGroup', repl_dn)
-
# Now finally test it ...
self.test_replication(from_instance, to_instance)
# Done!
@@ -2306,13 +2310,15 @@ class ReplicationManager(object):
# to allow the tot_init to occur.
self._bootstrap_replica(from_r, to_r, to_instance)
+ # Set bind DN group BEFORE creating agreements: the supplier's replication
+ # thread starts immediately on agreement creation and will fail auth permanently
+ # if nsDS5ReplicaBindDNGroup is not yet configured on the consumer.
+ to_r.set('nsDS5ReplicaBindDNGroup', repl_group.dn)
+
# Now put in an agreement from to -> from
# both ends.
self.ensure_agreement(from_instance, to_instance)
- # Now fix our replica credentials from -> to
- to_r.set('nsDS5ReplicaBindDNGroup', repl_group.dn)
-
# Now finally test it ...
# If from_instance replica isn't read-write (hub, probably), we will test it later
if from_r.get_attr_val_int('nsDS5ReplicaType') == 3:
--
2.55.0

View File

@ -47,7 +47,7 @@ ExcludeArch: i686
Summary: 389 Directory Server (base)
Name: 389-ds-base
Version: 2.9.0
Release: 2%{?dist}
Release: 3%{?dist}
License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND Apache-2.0 AND MIT AND MPL-2.0 AND Zlib
URL: https://www.port389.org
Conflicts: selinux-policy-base < 3.9.8
@ -300,6 +300,16 @@ Patch: 0011-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patc
Patch: 0012-Issue-7593-Reject-invalid-SASL-packet-length-values-.patch
Patch: 0013-Issue-7593-Fix-testimony-docstring-for-SASL-overflow.patch
Patch: 0014-Issue-7558-Total-init-sends-the-suffix-entry-twice-7.patch
Patch: 0015-Issue-7200-repl-agmt-create-doesn-t-set-some-paramet.patch
Patch: 0016-Issue-7705-With-memberOfEntryScope-set-deferred-memb.patch
Patch: 0017-Issue-7711-Fix-typo-in-accountpolicy-login-history-s.patch
Patch: 0018-Issue-7658-Heap-Buffer-Overflow-in-sasl_io_recv-via-.patch
Patch: 0019-Issue-7284-Creating-local-password-policy-succeeds-w.patch
Patch: 0020-Issue-7284-CI-Fix-test_grace_limit_section-after-pwp.patch
Patch: 0021-Issue-7284-Automated-test-for-creating-local-passwor.patch
Patch: 0022-Issue-7284-Creating-local-password-policy-succeeds-w.patch
Patch: 0023-Issue-7707-lib389-set-nsDS5ReplicaBindDNGroup-before.patch
%description
389 Directory Server is an LDAPv3 compliant server. The base package includes
@ -749,6 +759,16 @@ exit 0
%endif
%changelog
* Tue Aug 18 2026 Simon Pichugin <spichugi@redhat.com> - 2.9.0-3
- Bump version to 2.9.0-3
- Resolves: RHEL-243052 - IPA - segfault with libjemalloc.so.2 [rhel-9]
- Resolves: RHEL-221410 - large IdM host group, missing memberof host group, authentication failures, high traffic and contention
- Resolves: RHEL-243050 - repl-agmt create doesn't set some parameters [rhel-9]
- Resolves: RHEL-243053 - 389-ds accountpolicy.py attribute name typo in help message [rhel-9]
- Resolves: RHEL-244870 - Local password policies can be created with unallowed values [rhel-9]
- Resolves: RHEL-238566 - lib389: join_supplier() sets nsDS5ReplicaBindDNGroup after ensure_agreement(), causing replication auth race [rhel-9]
- Resolves: RHEL-182153 - CVE-2026-11610 389-ds-base: 389-ds-base: Heap buffer overflow in sasl_io_recv() via padded SASL UNBIND [rhel-9.9]
* Fri Jul 17 2026 Simon Pichugin <spichugi@redhat.com> - 2.9.0-2
- Bump version to 2.9.0-2
- Resolves: RHEL-88942 - LDAP healthcheck and ignoring entrydn index and associated config