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:
parent
b1018138a1
commit
b065a40300
171
0018-Issue-7200-repl-agmt-create-doesn-t-set-some-paramet.patch
Normal file
171
0018-Issue-7200-repl-agmt-create-doesn-t-set-some-paramet.patch
Normal 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
|
||||
|
||||
2874
0019-Issue-7490-Enable-USDT-probes-by-default-in-RPM-7491.patch
Normal file
2874
0019-Issue-7490-Enable-USDT-probes-by-default-in-RPM-7491.patch
Normal file
File diff suppressed because it is too large
Load Diff
1163
0020-Issue-7468-RFE-HIBP-password-breach-validation-7492.patch
Normal file
1163
0020-Issue-7468-RFE-HIBP-password-breach-validation-7492.patch
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
192
0022-Issue-7705-With-memberOfEntryScope-set-deferred-memb.patch
Normal file
192
0022-Issue-7705-With-memberOfEntryScope-set-deferred-memb.patch
Normal 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
|
||||
|
||||
645
0023-Issue-7658-Heap-Buffer-Overflow-in-sasl_io_recv-via-.patch
Normal file
645
0023-Issue-7658-Heap-Buffer-Overflow-in-sasl_io_recv-via-.patch
Normal 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
|
||||
|
||||
319
0024-Issue-7284-Creating-local-password-policy-succeeds-w.patch
Normal file
319
0024-Issue-7284-Creating-local-password-policy-succeeds-w.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user