Bump version to 3.3.0-3

- Issue 6387 - Use make macro in the spec file
- Resolves: RHEL-80254 - [RFE] Provide a way to check for leaked passwords
- Resolves: RHEL-80257 - [RFE] Should be able to monitor the importing progress via the CLI
- Resolves: RHEL-149760 - IPA - segfault with libjemalloc.so.2
- Resolves: RHEL-153098 - [RFE] Enable USDT probes in production builds
- Resolves: RHEL-171355 - 389-ds accountpolicy.py attribute name typo in help message
- Resolves: RHEL-213991 - repl-agmt create doesn't set some parameters
- Resolves: RHEL-243048 - large IdM host group, missing memberof host group, authentication failures, high traffic and contention [rhel-10]
- Resolves: RHEL-129204 - Local password policies can be created with unallowed values
- Resolves: RHEL-238569 - lib389: join_supplier() sets nsDS5ReplicaBindDNGroup after ensure_agreement(), causing replication auth race [rhel-10]
- Resolves: RHEL-182157 - CVE-2026-11610 389-ds-base: 389-ds-base: Heap buffer overflow in sasl_io_recv() via padded SASL UNBIND [rhel-10.3]
This commit is contained in:
Simon Pichugin 2026-08-17 20:26:14 -07:00
parent b1018138a1
commit b065a40300
9 changed files with 5532 additions and 5 deletions

View File

@ -0,0 +1,171 @@
From d8ab66a76a1d1f0811e30d370e88cd761eef5e05 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
From 206bf042e75460ac9c0b72637c9c153a968bd134 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,192 @@
From c92d9d6bfeabfe1bde6b0c65cf765e504f535673 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 | 5 +-
2 files changed, 132 insertions(+), 2 deletions(-)
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 39cfbf72e..5e66637ff 100644
--- a/ldap/servers/plugins/memberof/memberof.c
+++ b/ldap/servers/plugins/memberof/memberof.c
@@ -1009,7 +1009,7 @@ deferred_mod_func(MemberofDeferredModTask *task)
Slapi_Mod *smod = 0;
LDAPMod **mods;
Slapi_DN *sdn;
- Slapi_Entry *pre_e = NULL;
+ Slapi_Entry *post_e = NULL;
int ret = SLAPI_PLUGIN_SUCCESS;
int config_copied = 0;
MemberOfConfig *mainConfig = 0;
@@ -1018,6 +1018,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 */
@@ -1040,7 +1041,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,645 @@
From 156179d5e698b151d85fa443af6b08b306ce6ebb 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 6c2cac084..efbc6d7dc 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,319 @@
From d7a4aa29bb284b38c94498e73586c6f8b194d81f 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 36f25eba1..93ebdd5cb 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 cbba0ecd4..e1ed5c3e3 100644
--- a/ldap/servers/slapd/add.c
+++ b/ldap/servers/slapd/add.c
@@ -810,6 +810,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 92a559e2c..2cba9256a 100644
--- a/ldap/servers/slapd/modify.c
+++ b/ldap/servers/slapd/modify.c
@@ -69,34 +69,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)
@@ -665,7 +637,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;
@@ -798,42 +769,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 19f50c2bf..cc511da00 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>
@@ -2719,6 +2720,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 fd3b936cf2df49bf8e0d3e7de185d13560ccab6b 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 cd4e0fee5..137b68996 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -2214,14 +2214,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)
@@ -2271,13 +2273,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!
@@ -2325,13 +2329,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

@ -44,6 +44,9 @@ ExcludeArch: i686
# Build cockpit plugin
%bcond cockpit 0
%bcond hibp 1
%bcond usdt 1
# fedora 15 and later uses tmpfiles.d
# otherwise, comment this out
%{!?with_tmpfiles_d: %global with_tmpfiles_d %{_sysconfdir}/tmpfiles.d}
@ -220,10 +223,16 @@ BuildRequires: libdb-devel
BuildRequires: net-snmp-devel
BuildRequires: bzip2-devel
BuildRequires: openssl-devel
%if %{with hibp}
BuildRequires: libcurl-devel
%endif
# the following is for the pam passthru auth plug-in
BuildRequires: pam-devel
BuildRequires: systemd-units
BuildRequires: systemd-devel
%if %{with usdt}
BuildRequires: systemtap-sdt-devel
%endif
BuildRequires: systemd-rpm-macros
%{?sysusers_requires_compat}
BuildRequires: cargo
@ -294,6 +303,12 @@ Requires: python3-file-magic
# Picks up our systemd deps.
%{?systemd_requires}
%if %{with usdt}
# Optional eBPF tracer for the shipped USDT bpftrace scripts.
# Use Suggests so it is not installed by default (debug-only, pulls in gcc).
Suggests: bpftrace
%endif
Obsoletes: %{name} <= 1.4.4
Source0: https://github.com/389ds/%{name}/releases/download/%{name}-%{version}/%{name}-%{version}.tar.bz2
@ -326,6 +341,14 @@ Patch: 0014-Issue-7406-Fix-ldap-agent-SNMP-stats-file-loading-76.patc
Patch: 0015-Issue-7633-RFE-Add-offline-diagnostics-for-thread-po.patch
Patch: 0016-Issue-7583-Compressed-logs-are-prematurely-deleted-7.patch
Patch: 0017-Issue-7573-Post-import-cache-autotuning-does-not-rec.patch
Patch: 0018-Issue-7200-repl-agmt-create-doesn-t-set-some-paramet.patch
Patch: 0019-Issue-7490-Enable-USDT-probes-by-default-in-RPM-7491.patch
Patch: 0020-Issue-7468-RFE-HIBP-password-breach-validation-7492.patch
Patch: 0021-Issue-7711-Fix-typo-in-accountpolicy-login-history-s.patch
Patch: 0022-Issue-7705-With-memberOfEntryScope-set-deferred-memb.patch
Patch: 0023-Issue-7658-Heap-Buffer-Overflow-in-sasl_io_recv-via-.patch
Patch: 0024-Issue-7284-Creating-local-password-policy-succeeds-w.patch
Patch: 0025-Issue-7707-lib389-set-nsDS5ReplicaBindDNGroup-before.patch
%description
389 Directory Server is an LDAPv3 compliant server. The base package includes
@ -505,6 +528,12 @@ RUST_FLAGS="--enable-rust --enable-rust-offline"
COCKPIT_FLAGS="--disable-cockpit"
%endif
%if %{with usdt}
USDT_FLAGS="--enable-usdt"
%else
USDT_FLAGS="--disable-usdt"
%endif
%if %{with bundle_jemalloc}
# Override page size, bz #1545539
# 4K
@ -529,7 +558,7 @@ pushd ../%{jemalloc_name}-%{jemalloc_ver}
--libdir=%{_libdir}/%{pkgname}/lib \
--bindir=%{_libdir}/%{pkgname}/bin \
--enable-prof %{lg_page} %{lg_hugepage}
make %{?_smp_mflags}
%make_build
popd
%endif
@ -556,15 +585,18 @@ autoreconf -fiv
--with-systemdsystemconfdir=%{_sysconfdir}/systemd/system \
--with-systemdgroupname=%{groupname} \
--libexecdir=%{_libexecdir}/%{pkgname} \
$ASAN_FLAGS $MSAN_FLAGS $TSAN_FLAGS $UBSAN_FLAGS $RUST_FLAGS $CLANG_FLAGS $COCKPIT_FLAGS \
$ASAN_FLAGS $MSAN_FLAGS $TSAN_FLAGS $UBSAN_FLAGS $RUST_FLAGS $CLANG_FLAGS $COCKPIT_FLAGS $USDT_FLAGS \
%if 0%{?fedora} >= 34 || 0%{?rhel} >= 9
--with-libldap-r=no \
%endif
%if %{with hibp}
--enable-hibp \
%endif
--enable-cmocka
# Avoid "Unknown key name 'XXX' in section 'Service', ignoring." warnings from systemd on older releases
%if 0%{?rhel} && 0%{?rhel} < 9
sed -r -i '/^(Protect(Home|Hostname|KernelLogs)|PrivateMounts)=/d' %{_builddir}/%{name}-%{version}/wrappers/*.service.in
sed -r -i '/^(Protect(Home|Hostname|KernelLogs)|PrivateMounts|NoExecPaths)=/d' %{_builddir}/%{name}-%{version}/wrappers/*.service.in
%endif
# lib389
@ -576,7 +608,7 @@ popd
# Generate symbolic info for debuggers
export XCFLAGS=$RPM_OPT_FLAGS
make %{?_smp_mflags}
%make_build
%install
@ -584,7 +616,7 @@ mkdir -p %{buildroot}%{_datadir}/gdb/auto-load%{_sbindir}
%if %{with cockpit}
mkdir -p %{buildroot}%{_datadir}/cockpit
%endif
make DESTDIR="$RPM_BUILD_ROOT" install
%make_install
%if %{with cockpit}
find %{buildroot}%{_datadir}/cockpit/389-console -type d | sed -e "s@%{buildroot}@@" | sed -e 's/^/\%dir /' > cockpit.list