diff --git a/0037-Issue-6940-dsconf-monitor-server-fails-with-ldapi-du.patch b/0037-Issue-6940-dsconf-monitor-server-fails-with-ldapi-du.patch new file mode 100644 index 0000000..32d54ff --- /dev/null +++ b/0037-Issue-6940-dsconf-monitor-server-fails-with-ldapi-du.patch @@ -0,0 +1,268 @@ +From 7423f0a0b90bac39a23b5ce54a1c61439d0ebcb6 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Tue, 19 Aug 2025 16:10:09 -0700 +Subject: [PATCH] Issue 6940 - dsconf monitor server fails with ldapi:// due to + absent server ID (#6941) + +Description: The dsconf monitor server command fails when using ldapi:// +protocol because the server ID is not set, preventing PID retrieval from +defaults.inf. This causes the Web console to fail displaying the "Server +Version" field and potentially other CLI/WebUI issues. + +The fix attempts to derive the server ID from the LDAPI socket path when +not explicitly provided. This covers the common case where the socket name +contains the instance name (e.g., slapd-instance.socket). +If that's not possible, it also attempts to derive the server ID from the +nsslapd-instancedir configuration attribute. The derived server ID +is validated against actual system instances to ensure it exists. +Note that socket names can vary and nsslapd-instancedir can be changed. +This is a best-effort approach for the common naming pattern. + +Also fixes the LDAPI socket path extraction which was incorrectly using +offset 9 instead of 8 for ldapi:// URIs. + +The monitor command now handles missing PIDs gracefully, returning zero +values for process-specific stats instead of failing completely. + +Fixes: https://github.com/389ds/389-ds-base/issues/6940 + +Reviewed by: @vashirov, @mreynolds389 (Thanks!!) +--- + src/lib389/lib389/__init__.py | 93 +++++++++++++++++++++++++++--- + src/lib389/lib389/cli_base/dsrc.py | 4 +- + src/lib389/lib389/monitor.py | 50 ++++++++++++---- + 3 files changed, 124 insertions(+), 23 deletions(-) + +diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py +index 0ddfca8ae..23a20739f 100644 +--- a/src/lib389/lib389/__init__.py ++++ b/src/lib389/lib389/__init__.py +@@ -17,7 +17,7 @@ + + import sys + import os +-from urllib.parse import urlparse ++from urllib.parse import urlparse, unquote + import stat + import pwd + import grp +@@ -67,7 +67,8 @@ from lib389.utils import ( + get_default_db_lib, + selinux_present, + selinux_label_port, +- get_user_is_root) ++ get_user_is_root, ++ get_instance_list) + from lib389.paths import Paths + from lib389.nss_ssl import NssSsl + from lib389.tasks import BackupTask, RestoreTask, Task +@@ -249,6 +250,57 @@ class DirSrv(SimpleLDAPObject, object): + self.dbdir = self.ds_paths.db_dir + self.changelogdir = os.path.join(os.path.dirname(self.dbdir), DEFAULT_CHANGELOG_DB) + ++ def _extract_serverid_from_string(self, text): ++ """Extract serverid from a string containing 'slapd-' pattern. ++ Returns the serverid or None if not found or validation fails. ++ Only attempts derivation if serverid is currently None. ++ """ ++ if getattr(self, 'serverid', None) is not None: ++ return None ++ if not text: ++ return None ++ ++ # Use regex to extract serverid from "slapd-" or "slapd-.socket" ++ match = re.search(r'slapd-([A-Za-z0-9._-]+?)(?:\.socket)?(?:$|/)', text) ++ if not match: ++ return None ++ candidate = match.group(1) ++ ++ self.serverid = candidate ++ try: ++ insts = get_instance_list() ++ except Exception: ++ self.serverid = None ++ return None ++ if f'slapd-{candidate}' in insts or candidate in insts: ++ return candidate ++ # restore original and report failure ++ self.serverid = None ++ return None ++ ++ def _derive_serverid_from_ldapi(self): ++ """Attempt to derive serverid from an LDAPI socket path or URI and ++ verify it exists on the system. Returns the serverid or None. ++ """ ++ socket_path = None ++ if hasattr(self, 'ldapi_socket') and self.ldapi_socket: ++ socket_path = unquote(self.ldapi_socket) ++ elif hasattr(self, 'ldapuri') and isinstance(self.ldapuri, str) and self.ldapuri.startswith('ldapi://'): ++ socket_path = unquote(self.ldapuri[len('ldapi://'):]) ++ ++ return self._extract_serverid_from_string(socket_path) ++ ++ def _derive_serverid_from_instancedir(self): ++ """Extract serverid from nsslapd-instancedir path like '/usr/lib64/dirsrv/slapd-'""" ++ try: ++ from lib389.config import Config ++ config = Config(self) ++ instancedir = config.get_attr_val_utf8_l("nsslapd-instancedir") ++ except Exception: ++ return None ++ ++ return self._extract_serverid_from_string(instancedir) ++ + def rebind(self): + """Reconnect to the DS + +@@ -528,6 +580,15 @@ class DirSrv(SimpleLDAPObject, object): + self.ldapi_autobind = args.get(SER_LDAPI_AUTOBIND, 'off') + self.isLocal = True + self.log.debug("Allocate %s with %s", self.__class__, self.ldapi_socket) ++ elif self.ldapuri is not None and isinstance(self.ldapuri, str) and self.ldapuri.startswith('ldapi://'): ++ # Try to learn serverid from ldapi uri ++ try: ++ self.ldapi_enabled = 'on' ++ self.ldapi_socket = unquote(self.ldapuri[len('ldapi://'):]) ++ self.ldapi_autobind = args.get(SER_LDAPI_AUTOBIND, 'off') ++ self.isLocal = True ++ except Exception: ++ pass + # Settings from args of server attributes + self.strict_hostname = args.get(SER_STRICT_HOSTNAME_CHECKING, False) + if self.strict_hostname is True: +@@ -548,9 +609,16 @@ class DirSrv(SimpleLDAPObject, object): + + self.log.debug("Allocate %s with %s:%s", self.__class__, self.host, (self.sslport or self.port)) + +- if SER_SERVERID_PROP in args: +- self.ds_paths = Paths(serverid=args[SER_SERVERID_PROP], instance=self, local=self.isLocal) ++ # Try to determine serverid if not provided ++ if SER_SERVERID_PROP in args and args.get(SER_SERVERID_PROP) is not None: + self.serverid = args.get(SER_SERVERID_PROP, None) ++ elif getattr(self, 'serverid', None) is None and self.isLocal: ++ sid = self._derive_serverid_from_ldapi() ++ if sid: ++ self.serverid = sid ++ ++ if getattr(self, 'serverid', None): ++ self.ds_paths = Paths(serverid=self.serverid, instance=self, local=self.isLocal) + else: + self.ds_paths = Paths(instance=self, local=self.isLocal) + +@@ -989,6 +1057,17 @@ class DirSrv(SimpleLDAPObject, object): + self.__initPart2() + self.state = DIRSRV_STATE_ONLINE + # Now that we're online, some of our methods may try to query the version online. ++ ++ # After transitioning online, attempt to derive serverid if still unknown. ++ # If we find it, refresh ds_paths and rerun __initPart2 ++ if getattr(self, 'serverid', None) is None and self.isLocal: ++ sid = self._derive_serverid_from_instancedir() ++ if sid: ++ self.serverid = sid ++ # Reinitialize paths with the new serverid ++ self.ds_paths = Paths(serverid=self.serverid, instance=self, local=self.isLocal) ++ if not connOnly: ++ self.__initPart2() + self.__add_brookers__() + + def close(self): +@@ -3537,8 +3616,4 @@ class DirSrv(SimpleLDAPObject, object): + """ + Get the pid of the running server + """ +- pid = pid_from_file(self.pid_file()) +- if pid == 0 or pid is None: +- return 0 +- else: +- return pid ++ return pid_from_file(self.pid_file()) +diff --git a/src/lib389/lib389/cli_base/dsrc.py b/src/lib389/lib389/cli_base/dsrc.py +index 84567b990..498228ce0 100644 +--- a/src/lib389/lib389/cli_base/dsrc.py ++++ b/src/lib389/lib389/cli_base/dsrc.py +@@ -56,7 +56,7 @@ def dsrc_arg_concat(args, dsrc_inst): + new_dsrc_inst['args'][SER_ROOT_DN] = new_dsrc_inst['binddn'] + if new_dsrc_inst['uri'][0:8] == 'ldapi://': + new_dsrc_inst['args'][SER_LDAPI_ENABLED] = "on" +- new_dsrc_inst['args'][SER_LDAPI_SOCKET] = new_dsrc_inst['uri'][9:] ++ new_dsrc_inst['args'][SER_LDAPI_SOCKET] = new_dsrc_inst['uri'][8:] + new_dsrc_inst['args'][SER_LDAPI_AUTOBIND] = "on" + + # Make new +@@ -170,7 +170,7 @@ def dsrc_to_ldap(path, instance_name, log): + dsrc_inst['args'][SER_ROOT_DN] = dsrc_inst['binddn'] + if dsrc_inst['uri'][0:8] == 'ldapi://': + dsrc_inst['args'][SER_LDAPI_ENABLED] = "on" +- dsrc_inst['args'][SER_LDAPI_SOCKET] = dsrc_inst['uri'][9:] ++ dsrc_inst['args'][SER_LDAPI_SOCKET] = dsrc_inst['uri'][8:] + dsrc_inst['args'][SER_LDAPI_AUTOBIND] = "on" + + # Return the dict. +diff --git a/src/lib389/lib389/monitor.py b/src/lib389/lib389/monitor.py +index 27b99a7e3..bf3e1df76 100644 +--- a/src/lib389/lib389/monitor.py ++++ b/src/lib389/lib389/monitor.py +@@ -92,21 +92,47 @@ class Monitor(DSLdapObject): + Get CPU and memory stats + """ + stats = {} +- pid = self._instance.get_pid() ++ try: ++ pid = self._instance.get_pid() ++ except Exception: ++ pid = None + total_mem = psutil.virtual_memory()[0] +- p = psutil.Process(pid) +- memory_stats = p.memory_full_info() + +- # Get memory & CPU stats ++ # Always include total system memory + stats['total_mem'] = [str(total_mem)] +- stats['rss'] = [str(memory_stats[0])] +- stats['vms'] = [str(memory_stats[1])] +- stats['swap'] = [str(memory_stats[9])] +- stats['mem_rss_percent'] = [str(round(p.memory_percent("rss")))] +- stats['mem_vms_percent'] = [str(round(p.memory_percent("vms")))] +- stats['mem_swap_percent'] = [str(round(p.memory_percent("swap")))] +- stats['total_threads'] = [str(p.num_threads())] +- stats['cpu_usage'] = [str(round(p.cpu_percent(interval=0.1)))] ++ ++ # Process-specific stats - only if process is running (pid is not None) ++ if pid is not None: ++ try: ++ p = psutil.Process(pid) ++ memory_stats = p.memory_full_info() ++ ++ # Get memory & CPU stats ++ stats['rss'] = [str(memory_stats[0])] ++ stats['vms'] = [str(memory_stats[1])] ++ stats['swap'] = [str(memory_stats[9])] ++ stats['mem_rss_percent'] = [str(round(p.memory_percent("rss")))] ++ stats['mem_vms_percent'] = [str(round(p.memory_percent("vms")))] ++ stats['mem_swap_percent'] = [str(round(p.memory_percent("swap")))] ++ stats['total_threads'] = [str(p.num_threads())] ++ stats['cpu_usage'] = [str(round(p.cpu_percent(interval=0.1)))] ++ except (psutil.NoSuchProcess, psutil.AccessDenied): ++ # Process exists in PID file but is not accessible or doesn't exist ++ pid = None ++ ++ # If no valid PID, provide zero values for process stats ++ if pid is None: ++ stats['rss'] = ['0'] ++ stats['vms'] = ['0'] ++ stats['swap'] = ['0'] ++ stats['mem_rss_percent'] = ['0'] ++ stats['mem_vms_percent'] = ['0'] ++ stats['mem_swap_percent'] = ['0'] ++ stats['total_threads'] = ['0'] ++ stats['cpu_usage'] = ['0'] ++ stats['server_status'] = ['PID unavailable'] ++ else: ++ stats['server_status'] = ['Server running'] + + # Connections to DS + if self._instance.port == "0": +-- +2.49.0 + diff --git a/0038-Issue-6936-Make-user-subtree-policy-creation-idempot.patch b/0038-Issue-6936-Make-user-subtree-policy-creation-idempot.patch new file mode 100644 index 0000000..c946749 --- /dev/null +++ b/0038-Issue-6936-Make-user-subtree-policy-creation-idempot.patch @@ -0,0 +1,569 @@ +From 594333d1a6a8bba4d485b8227c4474e4ca2aa6a4 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Tue, 19 Aug 2025 14:30:15 -0700 +Subject: [PATCH] Issue 6936 - Make user/subtree policy creation idempotent + (#6937) + +Description: Correct the CLI mapping typo to use 'nsslapd-pwpolicy-local', +rework subtree policy detection to validate CoS templates and add user-policy detection. +Make user/subtree policy creation idempotent via ensure_state, and improve deletion +logic to distinguish subtree vs user policies and fail if none exist. + +Add a test suite (pwp_history_local_override_test.py) exercising global-only and local-only +history enforcement, local overriding global counts, immediate effect of dsconf updates, +and fallback to global after removing a user policy, ensuring reliable behavior +and preventing regressions. + +Fixes: https://github.com/389ds/389-ds-base/issues/6936 + +Reviewed by: @mreynolds389 (Thanks!) +--- + .../pwp_history_local_override_test.py | 351 ++++++++++++++++++ + src/lib389/lib389/cli_conf/pwpolicy.py | 4 +- + src/lib389/lib389/pwpolicy.py | 107 ++++-- + 3 files changed, 424 insertions(+), 38 deletions(-) + create mode 100644 dirsrvtests/tests/suites/password/pwp_history_local_override_test.py + +diff --git a/dirsrvtests/tests/suites/password/pwp_history_local_override_test.py b/dirsrvtests/tests/suites/password/pwp_history_local_override_test.py +new file mode 100644 +index 000000000..6d72725fa +--- /dev/null ++++ b/dirsrvtests/tests/suites/password/pwp_history_local_override_test.py +@@ -0,0 +1,351 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++import os ++import time ++import ldap ++import pytest ++import subprocess ++import logging ++ ++from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD, DN_CONFIG ++from lib389.topologies import topology_st ++from lib389.idm.user import UserAccounts ++from lib389.idm.domain import Domain ++from lib389.pwpolicy import PwPolicyManager ++ ++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__) ++ ++OU_DN = f"ou=People,{DEFAULT_SUFFIX}" ++USER_ACI = '(targetattr="userpassword || passwordHistory")(version 3.0; acl "pwp test"; allow (all) userdn="ldap:///self";)' ++ ++ ++@pytest.fixture(autouse=True, scope="function") ++def restore_global_policy(topology_st, request): ++ """Snapshot and restore global password policy around each test in this file.""" ++ inst = topology_st.standalone ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ attrs = [ ++ 'nsslapd-pwpolicy-local', ++ 'nsslapd-pwpolicy-inherit-global', ++ 'passwordHistory', ++ 'passwordInHistory', ++ 'passwordChange', ++ ] ++ ++ entry = inst.getEntry(DN_CONFIG, ldap.SCOPE_BASE, '(objectClass=*)', attrs) ++ saved = {attr: entry.getValue(attr) for attr in attrs} ++ ++ def fin(): ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ for attr, value in saved.items(): ++ inst.config.replace(attr, value) ++ ++ request.addfinalizer(fin) ++ ++ ++@pytest.fixture(scope="function") ++def setup_entries(topology_st, request): ++ """Create test OU and user, and install an ACI for self password changes.""" ++ ++ inst = topology_st.standalone ++ ++ suffix = Domain(inst, DEFAULT_SUFFIX) ++ suffix.add('aci', USER_ACI) ++ ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ try: ++ user = users.create_test_user(uid=1) ++ except ldap.ALREADY_EXISTS: ++ user = users.get("test_user_1") ++ ++ def fin(): ++ pwp = PwPolicyManager(inst) ++ try: ++ pwp.delete_local_policy(OU_DN) ++ except Exception as e: ++ if "No password policy" in str(e): ++ pass ++ else: ++ raise e ++ try: ++ pwp.delete_local_policy(user.dn) ++ except Exception as e: ++ if "No password policy" in str(e): ++ pass ++ else: ++ raise e ++ suffix.remove('aci', USER_ACI) ++ request.addfinalizer(fin) ++ ++ return user ++ ++ ++def set_user_password(inst, user, new_password, bind_as_user_password=None, expect_violation=False): ++ if bind_as_user_password is not None: ++ user.rebind(bind_as_user_password) ++ try: ++ user.reset_password(new_password) ++ if expect_violation: ++ pytest.fail("Password change unexpectedly succeeded") ++ except ldap.CONSTRAINT_VIOLATION: ++ if not expect_violation: ++ pytest.fail("Password change unexpectedly rejected with CONSTRAINT_VIOLATION") ++ finally: ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ time.sleep(1) ++ ++ ++def set_global_history(inst, enabled: bool, count: int, inherit_global: str = 'on'): ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ inst.config.replace('nsslapd-pwpolicy-local', 'on') ++ inst.config.replace('nsslapd-pwpolicy-inherit-global', inherit_global) ++ inst.config.replace('passwordHistory', 'on' if enabled else 'off') ++ inst.config.replace('passwordInHistory', str(count)) ++ inst.config.replace('passwordChange', 'on') ++ time.sleep(1) ++ ++ ++def ensure_local_subtree_policy(inst, count: int, track_update_time: str = 'on'): ++ pwp = PwPolicyManager(inst) ++ pwp.create_subtree_policy(OU_DN, { ++ 'passwordChange': 'on', ++ 'passwordHistory': 'on', ++ 'passwordInHistory': str(count), ++ 'passwordTrackUpdateTime': track_update_time, ++ }) ++ time.sleep(1) ++ ++ ++def set_local_history_via_cli(inst, count: int): ++ sbin_dir = inst.get_sbin_dir() ++ inst_name = inst.serverid ++ cmd = [f"{sbin_dir}/dsconf", inst_name, "localpwp", "set", f"--pwdhistorycount={count}", OU_DN] ++ rc = subprocess.call(cmd) ++ assert rc == 0, f"dsconf command failed rc={rc}: {' '.join(cmd)}" ++ time.sleep(1) ++ ++ ++def test_global_history_only_enforced(topology_st, setup_entries): ++ """Global-only history enforcement with count 2 ++ ++ :id: 3d8cf35b-4a33-4587-9814-ebe18b7a1f92 ++ :setup: Standalone instance, test OU and user, ACI for self password changes ++ :steps: ++ 1. Remove local policies ++ 2. Set global policy: passwordHistory=on, passwordInHistory=2 ++ 3. Set password to Alpha1, then change to Alpha2 and Alpha3 as the user ++ 4. Attempt to change to Alpha1 and Alpha2 ++ 5. Attempt to change to Alpha4 ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Changes to Welcome1 and Welcome2 are rejected with CONSTRAINT_VIOLATION ++ 5. Change to Welcome4 is accepted ++ """ ++ inst = topology_st.standalone ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ set_global_history(inst, enabled=True, count=2) ++ ++ user = setup_entries ++ user.reset_password('Alpha1') ++ set_user_password(inst, user, 'Alpha2', bind_as_user_password='Alpha1') ++ set_user_password(inst, user, 'Alpha3', bind_as_user_password='Alpha2') ++ ++ # Within last 2 ++ set_user_password(inst, user, 'Alpha2', bind_as_user_password='Alpha3', expect_violation=True) ++ set_user_password(inst, user, 'Alpha1', bind_as_user_password='Alpha3', expect_violation=True) ++ ++ # New password should be allowed ++ set_user_password(inst, user, 'Alpha4', bind_as_user_password='Alpha3', expect_violation=False) ++ ++ ++def test_local_overrides_global_history(topology_st, setup_entries): ++ """Local subtree policy (history=3) overrides global (history=1) ++ ++ :id: 97c22f56-5ea6-40c1-8d8c-1cece3bf46fd ++ :setup: Standalone instance, test OU and user ++ :steps: ++ 1. Set global policy passwordInHistory=1 ++ 2. Create local subtree policy on the OU with passwordInHistory=3 ++ 3. Set password to Bravo1, then change to Bravo2 and Bravo3 as the user ++ 4. Attempt to change to Bravo1 ++ 5. Attempt to change to Bravo5 ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Change to Welcome1 is rejected (local policy wins) ++ 5. Change to Welcome5 is accepted ++ """ ++ inst = topology_st.standalone ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ set_global_history(inst, enabled=True, count=1, inherit_global='on') ++ ++ ensure_local_subtree_policy(inst, count=3) ++ ++ user = setup_entries ++ user.reset_password('Bravo1') ++ set_user_password(inst, user, 'Bravo2', bind_as_user_password='Bravo1') ++ set_user_password(inst, user, 'Bravo3', bind_as_user_password='Bravo2') ++ ++ # Third prior should be rejected under local policy count=3 ++ set_user_password(inst, user, 'Bravo1', bind_as_user_password='Bravo3', expect_violation=True) ++ ++ # New password allowed ++ set_user_password(inst, user, 'Bravo5', bind_as_user_password='Bravo3', expect_violation=False) ++ ++ ++def test_change_local_history_via_cli_affects_enforcement(topology_st, setup_entries): ++ """Changing local policy via CLI is enforced immediately ++ ++ :id: 5a6d0d14-4009-4bad-86e1-cde5000c43dc ++ :setup: Standalone instance, test OU and user, dsconf available ++ :steps: ++ 1. Ensure local subtree policy passwordInHistory=3 ++ 2. Set password to Charlie1, then change to Charlie2 and Charlie3 as the user ++ 3. Attempt to change to Charlie1 (within last 3) ++ 4. Run: dsconf localpwp set --pwdhistorycount=1 "ou=product testing," ++ 5. Attempt to change to Charlie1 again ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Change to Welcome1 is rejected ++ 4. CLI command succeeds ++ 5. Change to Welcome1 now succeeds (only last 1 is disallowed) ++ """ ++ inst = topology_st.standalone ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ ensure_local_subtree_policy(inst, count=3) ++ ++ user = setup_entries ++ user.reset_password('Charlie1') ++ set_user_password(inst, user, 'Charlie2', bind_as_user_password='Charlie1', expect_violation=False) ++ set_user_password(inst, user, 'Charlie3', bind_as_user_password='Charlie2', expect_violation=False) ++ ++ # With count=3, Welcome1 is within history ++ set_user_password(inst, user, 'Charlie1', bind_as_user_password='Charlie3', expect_violation=True) ++ ++ # Reduce local count to 1 via CLI to exercise CLI mapping and updated code ++ set_local_history_via_cli(inst, count=1) ++ ++ # Now Welcome1 should be allowed ++ set_user_password(inst, user, 'Charlie1', bind_as_user_password='Charlie3', expect_violation=False) ++ ++ ++def test_history_local_only_enforced(topology_st, setup_entries): ++ """Local-only history enforcement with count 3 ++ ++ :id: af6ff34d-ac94-4108-a7b6-2b589c960154 ++ :setup: Standalone instance, test OU and user ++ :steps: ++ 1. Disable global password history (passwordHistory=off, passwordInHistory=0, inherit off) ++ 2. Ensure local subtree policy with passwordInHistory=3 ++ 3. Set password to Delta1, then change to Delta2 and Delta3 as the user ++ 4. Attempt to change to Delta1 ++ 5. Attempt to change to Delta5 ++ 6. Change once more to Delta6, then change to Delta1 ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Change to Welcome1 is rejected (within last 3) ++ 5. Change to Welcome5 is accepted ++ 6. Welcome1 is now older than the last 3 and is accepted ++ """ ++ inst = topology_st.standalone ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ set_global_history(inst, enabled=False, count=0, inherit_global='off') ++ ++ ensure_local_subtree_policy(inst, count=3) ++ ++ user = setup_entries ++ user.reset_password('Delta1') ++ set_user_password(inst, user, 'Delta2', bind_as_user_password='Delta1') ++ set_user_password(inst, user, 'Delta3', bind_as_user_password='Delta2') ++ ++ # Within last 2 ++ set_user_password(inst, user, 'Delta1', bind_as_user_password='Delta3', expect_violation=True) ++ ++ # New password allowed ++ set_user_password(inst, user, 'Delta5', bind_as_user_password='Delta3', expect_violation=False) ++ ++ # Now Welcome1 is older than last 2 after one more change ++ set_user_password(inst, user, 'Delta6', bind_as_user_password='Delta5', expect_violation=False) ++ set_user_password(inst, user, 'Delta1', bind_as_user_password='Delta6', expect_violation=False) ++ ++ ++def test_user_policy_detection_and_enforcement(topology_st, setup_entries): ++ """User local policy is detected and enforced; removal falls back to global policy ++ ++ :id: 2213126a-1f47-468c-8337-0d2ee5d2d585 ++ :setup: Standalone instance, test OU and user ++ :steps: ++ 1. Set global policy passwordInHistory=1 ++ 2. Create a user local password policy on the user with passwordInHistory=3 ++ 3. Verify is_user_policy(USER_DN) is True ++ 4. Set password to Echo1, then change to Echo2 and Echo3 as the user ++ 5. Attempt to change to Echo1 (within last 3) ++ 6. Delete the user local policy ++ 7. Verify is_user_policy(USER_DN) is False ++ 8. Attempt to change to Echo1 again (now only last 1 disallowed by global) ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. is_user_policy returns True ++ 4. Success ++ 5. Change to Welcome1 is rejected ++ 6. Success ++ 7. is_user_policy returns False ++ 8. Change to Welcome1 succeeds (two back is allowed by global=1) ++ """ ++ inst = topology_st.standalone ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ set_global_history(inst, enabled=True, count=1, inherit_global='on') ++ ++ pwp = PwPolicyManager(inst) ++ user = setup_entries ++ pwp.create_user_policy(user.dn, { ++ 'passwordChange': 'on', ++ 'passwordHistory': 'on', ++ 'passwordInHistory': '3', ++ }) ++ ++ assert pwp.is_user_policy(user.dn) is True ++ ++ user.reset_password('Echo1') ++ set_user_password(inst, user, 'Echo2', bind_as_user_password='Echo1', expect_violation=False) ++ set_user_password(inst, user, 'Echo3', bind_as_user_password='Echo2', expect_violation=False) ++ set_user_password(inst, user, 'Echo1', bind_as_user_password='Echo3', expect_violation=True) ++ ++ pwp.delete_local_policy(user.dn) ++ assert pwp.is_user_policy(user.dn) is False ++ ++ # With only global=1, Echo1 (two back) is allowed ++ set_user_password(inst, user, 'Echo1', bind_as_user_password='Echo3', expect_violation=False) ++ ++ ++if __name__ == '__main__': ++ # Run isolated ++ # -s for DEBUG mode ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main("-s %s" % CURRENT_FILE) +diff --git a/src/lib389/lib389/cli_conf/pwpolicy.py b/src/lib389/lib389/cli_conf/pwpolicy.py +index 2d4ba9b21..a3e59a90c 100644 +--- a/src/lib389/lib389/cli_conf/pwpolicy.py ++++ b/src/lib389/lib389/cli_conf/pwpolicy.py +@@ -1,5 +1,5 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2023 Red Hat, Inc. ++# Copyright (C) 2025 Red Hat, Inc. + # All rights reserved. + # + # License: GPL (version 3 or any later version). +@@ -43,7 +43,7 @@ def _get_pw_policy(inst, targetdn, log, use_json=None): + targetdn = 'cn=config' + policydn = targetdn + basedn = targetdn +- attr_list.extend(['passwordisglobalpolicy', 'nsslapd-pwpolicy_local']) ++ attr_list.extend(['passwordisglobalpolicy', 'nsslapd-pwpolicy-local']) + all_attrs = inst.config.get_attrs_vals_utf8(attr_list) + attrs = {k: v for k, v in all_attrs.items() if len(v) > 0} + else: +diff --git a/src/lib389/lib389/pwpolicy.py b/src/lib389/lib389/pwpolicy.py +index 6a47a44fe..539c230a9 100644 +--- a/src/lib389/lib389/pwpolicy.py ++++ b/src/lib389/lib389/pwpolicy.py +@@ -1,5 +1,5 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2018 Red Hat, Inc. ++# Copyright (C) 2025 Red Hat, Inc. + # All rights reserved. + # + # License: GPL (version 3 or any later version). +@@ -7,6 +7,7 @@ + # --- END COPYRIGHT BLOCK --- + + import ldap ++from ldap import filter as ldap_filter + from lib389._mapped_object import DSLdapObject, DSLdapObjects + from lib389.backend import Backends + from lib389.config import Config +@@ -74,19 +75,56 @@ class PwPolicyManager(object): + } + + def is_subtree_policy(self, dn): +- """Check if the entry has a subtree password policy. If we can find a +- template entry it is subtree policy ++ """Check if a subtree password policy exists for a given entry DN. + +- :param dn: Entry DN with PwPolicy set up ++ A subtree policy is indicated by the presence of any CoS template ++ (under `cn=nsPwPolicyContainer,`) that has a `pwdpolicysubentry` ++ attribute pointing to an existing entry with objectClass `passwordpolicy`. ++ ++ :param dn: Entry DN to check for subtree policy + :type dn: str + +- :returns: True if the entry has a subtree policy, False otherwise ++ :returns: True if a subtree policy exists, False otherwise ++ :rtype: bool + """ +- cos_templates = CosTemplates(self._instance, 'cn=nsPwPolicyContainer,{}'.format(dn)) + try: +- cos_templates.get('cn=nsPwTemplateEntry,%s' % dn) +- return True +- except: ++ container_basedn = 'cn=nsPwPolicyContainer,{}'.format(dn) ++ templates = CosTemplates(self._instance, container_basedn).list() ++ for tmpl in templates: ++ pwp_dn = tmpl.get_attr_val_utf8('pwdpolicysubentry') ++ if not pwp_dn: ++ continue ++ # Validate that the referenced entry exists and is a passwordpolicy ++ pwp_entry = PwPolicyEntry(self._instance, pwp_dn) ++ if pwp_entry.exists() and pwp_entry.present('objectClass', 'passwordpolicy'): ++ return True ++ except ldap.LDAPError: ++ pass ++ return False ++ ++ def is_user_policy(self, dn): ++ """Check if the entry has a user password policy. ++ ++ A user policy is indicated by the target entry having a ++ `pwdpolicysubentry` attribute that points to an existing ++ entry with objectClass `passwordpolicy`. ++ ++ :param dn: Entry DN to check ++ :type dn: str ++ ++ :returns: True if the entry has a user policy, False otherwise ++ :rtype: bool ++ """ ++ try: ++ entry = Account(self._instance, dn) ++ if not entry.exists(): ++ return False ++ pwp_dn = entry.get_attr_val_utf8('pwdpolicysubentry') ++ if not pwp_dn: ++ return False ++ pwp_entry = PwPolicyEntry(self._instance, pwp_dn) ++ return pwp_entry.exists() and pwp_entry.present('objectClass', 'passwordpolicy') ++ except ldap.LDAPError: + return False + + def create_user_policy(self, dn, properties): +@@ -114,10 +152,10 @@ class PwPolicyManager(object): + pwp_containers = nsContainers(self._instance, basedn=parentdn) + pwp_container = pwp_containers.ensure_state(properties={'cn': 'nsPwPolicyContainer'}) + +- # Create policy entry ++ # Create or update the policy entry + properties['cn'] = 'cn=nsPwPolicyEntry_user,%s' % dn + pwp_entries = PwPolicyEntries(self._instance, pwp_container.dn) +- pwp_entry = pwp_entries.create(properties=properties) ++ pwp_entry = pwp_entries.ensure_state(properties=properties) + try: + # Add policy to the entry + user_entry.replace('pwdpolicysubentry', pwp_entry.dn) +@@ -152,32 +190,27 @@ class PwPolicyManager(object): + pwp_containers = nsContainers(self._instance, basedn=dn) + pwp_container = pwp_containers.ensure_state(properties={'cn': 'nsPwPolicyContainer'}) + +- # Create policy entry +- pwp_entry = None ++ # Create or update the policy entry + properties['cn'] = 'cn=nsPwPolicyEntry_subtree,%s' % dn + pwp_entries = PwPolicyEntries(self._instance, pwp_container.dn) +- pwp_entry = pwp_entries.create(properties=properties) +- try: +- # The CoS template entry (nsPwTemplateEntry) that has the pwdpolicysubentry +- # value pointing to the above (nsPwPolicyEntry) entry +- cos_template = None +- cos_templates = CosTemplates(self._instance, pwp_container.dn) +- cos_template = cos_templates.create(properties={'cosPriority': '1', +- 'pwdpolicysubentry': pwp_entry.dn, +- 'cn': 'cn=nsPwTemplateEntry,%s' % dn}) +- +- # The CoS specification entry at the subtree level +- cos_pointer_defs = CosPointerDefinitions(self._instance, dn) +- cos_pointer_defs.create(properties={'cosAttribute': 'pwdpolicysubentry default operational-default', +- 'cosTemplateDn': cos_template.dn, +- 'cn': 'nsPwPolicy_CoS'}) +- except ldap.LDAPError as e: +- # Something went wrong, remove what we have done +- if pwp_entry is not None: +- pwp_entry.delete() +- if cos_template is not None: +- cos_template.delete() +- raise e ++ pwp_entry = pwp_entries.ensure_state(properties=properties) ++ ++ # Ensure the CoS template entry (nsPwTemplateEntry) that points to the ++ # password policy entry ++ cos_templates = CosTemplates(self._instance, pwp_container.dn) ++ cos_template = cos_templates.ensure_state(properties={ ++ 'cosPriority': '1', ++ 'pwdpolicysubentry': pwp_entry.dn, ++ 'cn': 'cn=nsPwTemplateEntry,%s' % dn ++ }) ++ ++ # Ensure the CoS specification entry at the subtree level ++ cos_pointer_defs = CosPointerDefinitions(self._instance, dn) ++ cos_pointer_defs.ensure_state(properties={ ++ 'cosAttribute': 'pwdpolicysubentry default operational-default', ++ 'cosTemplateDn': cos_template.dn, ++ 'cn': 'nsPwPolicy_CoS' ++ }) + + # make sure that local policies are enabled + self.set_global_policy({'nsslapd-pwpolicy-local': 'on'}) +@@ -244,10 +277,12 @@ class PwPolicyManager(object): + if self.is_subtree_policy(entry.dn): + parentdn = dn + subtree = True +- else: ++ elif self.is_user_policy(entry.dn): + dn_comps = ldap.dn.explode_dn(dn) + dn_comps.pop(0) + parentdn = ",".join(dn_comps) ++ else: ++ raise ValueError('The target entry dn does not have a password policy') + + # Starting deleting the policy, ignore the parts that might already have been removed + pwp_container = nsContainer(self._instance, 'cn=nsPwPolicyContainer,%s' % parentdn) +-- +2.49.0 + diff --git a/0039-Issue-6919-numSubordinates-tombstoneNumSubordinates-.patch b/0039-Issue-6919-numSubordinates-tombstoneNumSubordinates-.patch new file mode 100644 index 0000000..10ac381 --- /dev/null +++ b/0039-Issue-6919-numSubordinates-tombstoneNumSubordinates-.patch @@ -0,0 +1,1460 @@ +From 9aa30236c7db41280af48d9cf74168e4d05c2c5c Mon Sep 17 00:00:00 2001 +From: progier389 +Date: Thu, 21 Aug 2025 17:30:00 +0200 +Subject: [PATCH] =?UTF-8?q?Issue=206919=20-=20numSubordinates/tombstoneNum?= + =?UTF-8?q?Subordinates=20are=20inconsisten=E2=80=A6=20(#6920)?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +* Issue 6919 - numSubordinates/tombstoneNumSubordinates are inconsistent after import + +Problem was that the number of tombstone was not propperly computed. +With bdb: tombstoneNumSubordinates was not computed: +With lmdb: numSubordinates was also including the tombstones +Fixed the numSubordinates/tombstoneNumSubordinates computation during import by: +walking the entryrdn C keys (because parentid does not contains tombstone on bdb) +checking if the children entry is a tombstone by looking in objectclass index +and increasing numSubordinates/tombstoneNumSubordinates subcount accordingly +performed some code cleanup. +- removed the job->mother containing the hashtable of non leaf entry ids +- moved the function that replace the numSubordinates/tombstoneNumSubordinates +attribute in an entry back in export.c (rather than duplicating it in +db_import.c and db_import.c) +- changed a PR_ASSERT that is no more true. + +Notes: +Not using the parentid index because it does not contains the tombstone on bdb +(although it does on lmdb) +The new subcount computation algorythm was not possible when the code was origionally written +because it requires entrytrdn index and having alls the keys (i.e: no ALLIDs in the indexes) +That was why hash table of ids and idlist was used. ( I removed that code that code generate a serious +overhead if there is a large number of non leaf entries (typically if users entries have children) + +Issue: #6919 + +Reviewed by: @mreynolds389 (Thanks!) +--- + .../numsubordinates_replication_test.py | 124 ++++- + ldap/servers/plugins/replication/cl5_api.c | 2 +- + .../slapd/back-ldbm/db-bdb/bdb_import.c | 492 ++++++++---------- + .../back-ldbm/db-bdb/bdb_import_threads.c | 13 - + .../slapd/back-ldbm/db-bdb/bdb_layer.h | 3 +- + .../slapd/back-ldbm/db-mdb/mdb_import.c | 264 ++++++---- + .../back-ldbm/db-mdb/mdb_import_threads.c | 1 + + .../slapd/back-ldbm/db-mdb/mdb_instance.c | 2 +- + .../slapd/back-ldbm/db-mdb/mdb_layer.h | 2 - + ldap/servers/slapd/back-ldbm/import.c | 59 +++ + ldap/servers/slapd/back-ldbm/import.h | 2 +- + ldap/servers/slapd/back-ldbm/ldif2ldbm.c | 43 -- + ldap/servers/slapd/control.c | 2 +- + src/lib389/lib389/__init__.py | 2 +- + src/lib389/lib389/_mapped_object.py | 2 +- + 15 files changed, 572 insertions(+), 441 deletions(-) + +diff --git a/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py +index 9ba10657d..2624b2144 100644 +--- a/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py ++++ b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py +@@ -9,12 +9,15 @@ + import os + import logging + import pytest ++import re + from lib389._constants import DEFAULT_SUFFIX +-from lib389.replica import ReplicationManager + from lib389.idm.organizationalunit import OrganizationalUnits + from lib389.idm.user import UserAccounts +-from lib389.topologies import topology_i2 as topo_i2 +- ++from lib389.replica import ReplicationManager ++from lib389.tasks import * ++from lib389.tombstone import Tombstones ++from lib389.topologies import topology_i2 as topo_i2, topology_m2 as topo_m2 ++from lib389.utils import get_default_db_lib + + pytestmark = pytest.mark.tier1 + +@@ -26,6 +29,61 @@ else: + log = logging.getLogger(__name__) + + ++def get_test_name(): ++ full_testname = os.getenv('PYTEST_CURRENT_TEST') ++ res = re.match('.*::([^ ]+) .*', full_testname) ++ assert res ++ return res.group(1) ++ ++ ++@pytest.fixture(scope="function") ++def with_container(topo_m2, request): ++ # Creates a organizational unit container with proper cleanup ++ testname = get_test_name() ++ ou = f'test_container_{testname}' ++ S1 = topo_m2.ms["supplier1"] ++ S2 = topo_m2.ms["supplier2"] ++ repl = ReplicationManager(DEFAULT_SUFFIX) ++ ++ log.info(f"Create container ou={ou},{DEFAULT_SUFFIX} on {S1.serverid}") ++ ous1 = OrganizationalUnits(S1, DEFAULT_SUFFIX) ++ container = ous1.create(properties={ ++ 'ou': ou, ++ 'description': f'Test container for {testname} test' ++ }) ++ ++ def fin(): ++ container.delete(recursive=True) ++ repl.wait_for_replication(S1, S2) ++ ++ if not DEBUGGING: ++ request.addfinalizer(fin) ++ repl.wait_for_replication(S1, S2) ++ ++ return container ++ ++ ++def verify_value_against_entries(container, attr, entries, msg): ++ # Check that container attr value match the number of entries ++ num = container.get_attr_val_int(attr) ++ num_entries = len(entries) ++ dns = [ e.dn for e in entries ] ++ log.debug(f"[{msg}] {attr}: entries: {entries}") ++ log.info(f"[{msg}] container is {container}") ++ log.info(f"[{msg}] {attr}: {num} (Expecting: {num_entries})") ++ assert num == num_entries, ( ++ f"{attr} attribute has wrong value: {num} {msg}, was expecting: {num_entries}", ++ f"entries are {dns}" ) ++ ++ ++def verify_subordinates(inst, container, msg): ++ log.info(f"Verify numSubordinates and tombstoneNumSubordinates {msg}") ++ tombstones = Tombstones(inst, container.dn).list() ++ entries = container.search(scope='one') ++ verify_value_against_entries(container, 'numSubordinates', entries, msg) ++ verify_value_against_entries(container, 'tombstoneNumSubordinates', tombstones, msg) ++ ++ + def test_numsubordinates_tombstone_replication_mismatch(topo_i2): + """Test that numSubordinates values match between replicas after tombstone creation + +@@ -136,9 +194,67 @@ def test_numsubordinates_tombstone_replication_mismatch(topo_i2): + f"instance2 has {tombstone_numsubordinates_instance2}. " + ) + ++def test_numsubordinates_tombstone_after_import(topo_m2, with_container): ++ """Test that numSubordinates values are the expected one after an import ++ ++ :id: 67bec454-6bb3-11f0-b9ae-c85309d5c3e3 ++ :setup: Two suppliers instances with an ou container ++ :steps: ++ 1. Create a container (organizational unit) on the first instance ++ 2. Create a user object in that container ++ 3. Delete the user object (this creates a tombstone) ++ 4. Set up replication between the two instances ++ 5. Wait for replication to complete ++ 6. Check numSubordinates on both instances ++ 7. Check tombstoneNumSubordinates on both instances ++ 8. Verify that numSubordinates values match on both instances ++ :expectedresults: ++ 1. Container should be created successfully ++ 2. User object should be created successfully ++ 3. User object should be deleted successfully ++ 4. Replication should be set up successfully ++ 5. Replication should complete successfully ++ 6. numSubordinates should be accessible on both instances ++ 7. tombstoneNumSubordinates should be accessible on both instances ++ 8. numSubordinates values should match on both instances ++ """ ++ ++ S1 = topo_m2.ms["supplier1"] ++ S2 = topo_m2.ms["supplier2"] ++ container = with_container ++ repl = ReplicationManager(DEFAULT_SUFFIX) ++ tasks = Tasks(S1) ++ ++ log.info("Create some user objects in that container") ++ users1 = UserAccounts(S1, DEFAULT_SUFFIX, rdn=f"ou={container.rdn}") ++ users = {} ++ for uid in range(1001,1010): ++ users[uid] = users1.create_test_user(uid=uid) ++ log.info(f"Created user: {users[uid].dn}") ++ ++ for uid in range(1002,1007,2): ++ users[uid].delete() ++ log.info(f"Removing user: {users[uid].dn}") ++ repl.wait_for_replication(S1, S2) ++ ++ ldif_file = f"{S1.get_ldif_dir()}/export.ldif" ++ log.info(f"Export into {ldif_file}") ++ args = {EXPORT_REPL_INFO: True, ++ TASK_WAIT: True} ++ tasks.exportLDIF(DEFAULT_SUFFIX, None, ldif_file, args) ++ ++ verify_subordinates(S1, container, "before importing") ++ ++ # import the ldif file ++ log.info(f"Import from {ldif_file}") ++ args = {TASK_WAIT: True} ++ tasks.importLDIF(DEFAULT_SUFFIX, None, ldif_file, args) ++ ++ verify_subordinates(S1, container, "after importing") ++ + + if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode + CURRENT_FILE = os.path.realpath(__file__) +- pytest.main("-s %s" % CURRENT_FILE) +\ No newline at end of file ++ pytest.main("-s %s" % CURRENT_FILE) +diff --git a/ldap/servers/plugins/replication/cl5_api.c b/ldap/servers/plugins/replication/cl5_api.c +index 1d62aa020..a5e43c87d 100644 +--- a/ldap/servers/plugins/replication/cl5_api.c ++++ b/ldap/servers/plugins/replication/cl5_api.c +@@ -3211,7 +3211,7 @@ _cl5EnumConsumerRUV(const ruv_enum_data *element, void *arg) + RUV *ruv; + CSN *csn = NULL; + +- PR_ASSERT(element && element->csn && arg); ++ PR_ASSERT(element && arg); + + ruv = (RUV *)arg; + +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +index 3f7392059..46c80ec3d 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +@@ -30,6 +30,18 @@ static int bdb_ancestorid_create_index(backend *be, ImportJob *job); + static int bdb_ancestorid_default_create_index(backend *be, ImportJob *job); + static int bdb_ancestorid_new_idl_create_index(backend *be, ImportJob *job); + ++/* Helper struct used to compute numsubordinates */ ++ ++typedef struct { ++ backend *be; ++ DB_TXN *txn; ++ const char *attrname; ++ struct attrinfo *ai; ++ dbi_db_t *db; ++ DBC *dbc; ++} subcount_cursor_info_t; ++ ++ + /* Start of definitions for a simple cache using a hash table */ + + typedef struct id2idl +@@ -56,6 +68,69 @@ static int bdb_parentid(backend *be, DB_TXN *txn, ID id, ID *ppid); + static int bdb_check_cache(id2idl_hash *ht); + static IDList *bdb_idl_union_allids(backend *be, struct attrinfo *ai, IDList *a, IDList *b); + ++ ++/********** Code to debug numsubordinates/tombstonenumsubordinates computation **********/ ++ ++#ifdef DEBUG_SUBCOUNT ++#define DEBUG_SUBCOUNT_MSG(msg, ...) { debug_subcount(__FUNCTION__, __LINE__, (msg), __VA_ARGS__); } ++#define DUMP_SUBCOUNT_KEY(msg, key, ret) { debug_subcount(__FUNCTION__, __LINE__, "ret=%d size=%u ulen=%u doff=%u dlen=%u", \ ++ ret, (key).size, (key).ulen, (key).doff, (key).dlen); \ ++ if (ret == 0) hexadump(msg, (key).data, 0, (key).size); \ ++ else if (ret == DB_BUFFER_SMALL) \ ++ hexadump(msg, (key).data, 0, (key).ulen); } ++ ++static void ++debug_subcount(const char *funcname, int line, char *msg, ...) ++{ ++ va_list ap; ++ char buff[1024]; ++ va_start(ap, msg); ++ PR_vsnprintf(buff, (sizeof buff), msg, ap); ++ va_end(ap); ++ slapi_log_err(SLAPI_LOG_INFO, (char*)funcname, "DEBUG SUBCOUNT [%d] %s\n", line, buff); ++} ++ ++/* ++ * Dump a memory buffer in hexa and ascii in error log ++ * ++ * addr - The memory buffer address. ++ * len - The memory buffer lenght. ++ */ ++static void ++hexadump(char *msg, const void *addr, size_t offset, size_t len) ++{ ++#define HEXADUMP_TAB 4 ++/* 4 characters per bytes: 2 hexa digits, 1 space and the ascii */ ++#define HEXADUMP_BUF_SIZE (4*16+HEXADUMP_TAB) ++ char hexdigit[] = "0123456789ABCDEF"; ++ ++ const unsigned char *pt = addr; ++ char buff[HEXADUMP_BUF_SIZE+1]; ++ memset (buff, ' ', HEXADUMP_BUF_SIZE); ++ buff[HEXADUMP_BUF_SIZE] = '\0'; ++ while (len > 0) { ++ int dpl; ++ for (dpl = 0; dpl < 16 && len>0; dpl++, len--) { ++ buff[3*dpl] = hexdigit[((*pt) >> 4) & 0xf]; ++ buff[3*dpl+1] = hexdigit[(*pt) & 0xf]; ++ buff[3*16+HEXADUMP_TAB+dpl] = (*pt>=0x20 && *pt<0x7f) ? *pt : '.'; ++ pt++; ++ } ++ for (;dpl < 16; dpl++) { ++ buff[3*dpl] = ' '; ++ buff[3*dpl+1] = ' '; ++ buff[3*16+HEXADUMP_TAB+dpl] = ' '; ++ } ++ slapi_log_err(SLAPI_LOG_INFO, msg, "[0x%08lx] %s\n", offset, buff); ++ offset += 16; ++ } ++} ++#else ++#define DEBUG_SUBCOUNT_MSG(msg, ...) ++#define DUMP_SUBCOUNT_KEY(msg, key, ret) ++#endif ++ ++ + /********** routines to manipulate the entry fifo **********/ + + /* this is pretty bogus -- could be a HUGE amount of memory */ +@@ -994,173 +1069,85 @@ out: + + return ret; + } +-/* Update subordinate count in a hint list, given the parent's ID */ +-int +-bdb_import_subcount_mother_init(import_subcount_stuff *mothers, ID parent_id, size_t count) +-{ +- PR_ASSERT(NULL == PL_HashTableLookup(mothers->hashtable, (void *)((uintptr_t)parent_id))); +- PL_HashTableAdd(mothers->hashtable, (void *)((uintptr_t)parent_id), (void *)count); +- return 0; +-} + +-/* Look for a subordinate count in a hint list, given the parent's ID */ +-static int +-bdb_import_subcount_mothers_lookup(import_subcount_stuff *mothers, +- ID parent_id, +- size_t *count) ++static void ++bdb_close_subcount_cursor(subcount_cursor_info_t *info) + { +- size_t stored_count = 0; +- +- *count = 0; +- /* Lookup hash table for ID */ +- stored_count = (size_t)PL_HashTableLookup(mothers->hashtable, +- (void *)((uintptr_t)parent_id)); +- /* If present, return the count found */ +- if (0 != stored_count) { +- *count = stored_count; +- return 0; ++ if (info->dbc) { ++ int ret = info->dbc->c_close(info->dbc); ++ if (ret) { ++ char errfunc[60]; ++ snprintf(errfunc, (sizeof errfunc), "%s[%s]", __FUNCTION__, info->attrname); ++ ldbm_nasty(errfunc, sourcefile, 73, ret); ++ } ++ info->dbc = NULL; ++ } ++ if (info->db) { ++ dblayer_release_index_file(info->be, info->ai, info->db); ++ info->db = NULL; ++ info->ai = NULL; + } +- return -1; +-} +- +-/* Update subordinate count in a hint list, given the parent's ID */ +-int +-bdb_import_subcount_mother_count(import_subcount_stuff *mothers, ID parent_id) +-{ +- size_t stored_count = 0; +- +- /* Lookup the hash table for the target ID */ +- stored_count = (size_t)PL_HashTableLookup(mothers->hashtable, +- (void *)((uintptr_t)parent_id)); +- PR_ASSERT(0 != stored_count); +- /* Increment the count */ +- stored_count++; +- PL_HashTableAdd(mothers->hashtable, (void *)((uintptr_t)parent_id), (void *)stored_count); +- return 0; + } + + static int +-bdb_import_update_entry_subcount(backend *be, ID parentid, size_t sub_count, int isencrypted) ++bdb_open_subcount_cursor(backend *be, const char *attrname, DB_TXN *txn, subcount_cursor_info_t *info) + { +- ldbm_instance *inst = (ldbm_instance *)be->be_instance_info; ++ char errfunc[60]; ++ DB *db = NULL; + int ret = 0; +- modify_context mc = {0}; +- char value_buffer[22] = {0}; /* enough digits for 2^64 children */ +- struct backentry *e = NULL; +- int isreplace = 0; +- char *numsub_str = numsubordinates; +- +- /* Get hold of the parent */ +- e = id2entry(be, parentid, NULL, &ret); +- if ((NULL == e) || (0 != ret)) { +- ldbm_nasty("bdb_import_update_entry_subcount", sourcefile, 5, ret); +- return (0 == ret) ? -1 : ret; +- } +- /* Lock it (not really required since we're single-threaded here, but +- * let's do it so we can reuse the modify routines) */ +- cache_lock_entry(&inst->inst_cache, e); +- modify_init(&mc, e); +- mc.attr_encrypt = isencrypted; +- sprintf(value_buffer, "%lu", (long unsigned int)sub_count); +- /* If it is a tombstone entry, add tombstonesubordinates instead of +- * numsubordinates. */ +- if (slapi_entry_flag_is_set(e->ep_entry, SLAPI_ENTRY_FLAG_TOMBSTONE)) { +- numsub_str = LDBM_TOMBSTONE_NUMSUBORDINATES_STR; +- } +- /* attr numsubordinates/tombstonenumsubordinates could already exist in +- * the entry, let's check whether it's already there or not */ +- isreplace = (attrlist_find(e->ep_entry->e_attrs, numsub_str) != NULL); +- { +- int op = isreplace ? LDAP_MOD_REPLACE : LDAP_MOD_ADD; +- Slapi_Mods *smods = slapi_mods_new(); + +- slapi_mods_add(smods, op | LDAP_MOD_BVALUES, numsub_str, +- strlen(value_buffer), value_buffer); +- ret = modify_apply_mods(&mc, smods); /* smods passed in */ +- } +- if (0 == ret || LDAP_TYPE_OR_VALUE_EXISTS == ret) { +- /* This will correctly index subordinatecount: */ +- ret = modify_update_all(be, NULL, &mc, NULL); +- if (0 == ret) { +- modify_switch_entries(&mc, be); +- } ++ snprintf(errfunc, (sizeof errfunc), "%s[%s]", __FUNCTION__, attrname); ++ info->attrname = attrname; ++ info->txn = txn; ++ info->be = be; ++ ++ /* Lets get the attrinfo */ ++ ainfo_get(be, (char*)attrname, &info->ai); ++ PR_ASSERT(info->ai); ++ /* Lets get the db instance */ ++ if ((ret = dblayer_get_index_file(be, info->ai, &info->db, 0)) != 0) { ++ if (ret == DBI_RC_NOTFOUND) { ++ bdb_close_subcount_cursor(info); ++ return 0; ++ } ++ ldbm_nasty(errfunc, sourcefile, 70, ret); ++ bdb_close_subcount_cursor(info); ++ return ret; + } +- /* entry is unlocked and returned to the cache in modify_term */ +- modify_term(&mc, be); +- return ret; +-} +-struct _import_subcount_trawl_info +-{ +- struct _import_subcount_trawl_info *next; +- ID id; +- size_t sub_count; +-}; +-typedef struct _import_subcount_trawl_info import_subcount_trawl_info; +- +-static void +-bdb_import_subcount_trawl_add(import_subcount_trawl_info **list, ID id) +-{ +- import_subcount_trawl_info *new_info = CALLOC(import_subcount_trawl_info); + +- new_info->next = *list; +- new_info->id = id; +- *list = new_info; ++ /* Lets get the cursor */ ++ db = (DB*)(info->db); ++ if ((ret = db->cursor(db, info->txn, &info->dbc, 0)) != 0) { ++ ldbm_nasty(errfunc, sourcefile, 71, ret); ++ bdb_close_subcount_cursor(info); ++ ret = bdb_map_error(__FUNCTION__, ret); ++ } ++ return 0; + } + +-static int +-bdb_import_subcount_trawl(backend *be, +- import_subcount_trawl_info *trawl_list, +- int isencrypted) ++static bool ++bdb_subcount_is_tombstone(subcount_cursor_info_t *info, DBT *id) + { +- ldbm_instance *inst = (ldbm_instance *)be->be_instance_info; +- ID id = 1; +- int ret = 0; +- import_subcount_trawl_info *current = NULL; +- char value_buffer[20]; /* enough digits for 2^64 children */ +- +- /* OK, we do */ +- /* We open id2entry and iterate through it */ +- /* Foreach entry, we check to see if its parentID matches any of the +- * values in the trawl list . If so, we bump the sub count for that +- * parent in the list. ++ /* ++ * Check if record =nstombstone ==> id exists in objectclass index + */ +- while (1) { +- struct backentry *e = NULL; +- +- /* Get the next entry */ +- e = id2entry(be, id, NULL, &ret); +- if ((NULL == e) || (0 != ret)) { +- if (DB_NOTFOUND == ret) { +- break; +- } else { +- ldbm_nasty("bdb_import_subcount_trawl", sourcefile, 8, ret); +- return ret; +- } +- } +- for (current = trawl_list; current != NULL; current = current->next) { +- sprintf(value_buffer, "%lu", (u_long)current->id); +- if (slapi_entry_attr_hasvalue(e->ep_entry, LDBM_PARENTID_STR, value_buffer)) { +- /* If this entry's parent ID matches one we're trawling for, +- * bump its count */ +- current->sub_count++; +- } +- } +- /* Free the entry */ +- CACHE_REMOVE(&inst->inst_cache, e); +- CACHE_RETURN(&inst->inst_cache, &e); +- id++; +- } +- /* Now update the parent entries from the list */ +- for (current = trawl_list; current != NULL; current = current->next) { +- /* Update the parent entry with the correctly counted subcount */ +- ret = bdb_import_update_entry_subcount(be, current->id, +- current->sub_count, isencrypted); +- if (0 != ret) { +- ldbm_nasty("bdb_import_subcount_trawl", sourcefile, 10, ret); +- break; +- } ++ DBT key = {0}; ++ DBC *dbc = info->dbc; ++ int ret; ++ key.flags = DB_DBT_USERMEM; ++ key.data = "=nstombstone" ; ++ key.size = key.ulen = 13; ++ ret = dbc->c_get(dbc, &key, id, DB_GET_BOTH); ++ ++ switch (ret) { ++ case 0: ++ return true; ++ case DB_NOTFOUND: ++ return false; ++ default: ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 72, ret); ++ return false; + } +- return ret; + } + + /* +@@ -1172,65 +1159,69 @@ bdb_import_subcount_trawl(backend *be, + static int + bdb_update_subordinatecounts(backend *be, ImportJob *job, DB_TXN *txn) + { +- import_subcount_stuff *mothers = job->mothers; +- int isencrypted = job->encrypt; ++ subcount_cursor_info_t c_objectclass = {0}; ++ subcount_cursor_info_t c_entryrdn = {0}; + int started_progress_logging = 0; ++ int isencrypted = job->encrypt; ++ DBT data = {0}; ++ DBT key = {0}; + int key_count = 0; ++ char tmp[11]; ++ char oldkey[11]; ++ ID data_data; ++ int ret2 = 0; + int ret = 0; +- DB *db = NULL; +- DBC *dbc = NULL; +- struct attrinfo *ai = NULL; +- DBT key = {0}; +- dbi_val_t dbikey = {0}; +- DBT data = {0}; +- import_subcount_trawl_info *trawl_list = NULL; +- +- /* Open the parentid index */ +- ainfo_get(be, LDBM_PARENTID_STR, &ai); + +- /* Open the parentid index file */ +- if ((ret = dblayer_get_index_file(be, ai, (dbi_db_t**)&db, DBOPEN_CREATE)) != 0) { +- ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 67, ret); +- return (ret); ++ /* Open cursor on the objectclass index */ ++ ret = bdb_open_subcount_cursor(be, SLAPI_ATTR_OBJECTCLASS, txn, &c_objectclass); ++ if (ret) { ++ if (ret != DBI_RC_NOTFOUND) { ++ /* No database ==> There is nothing to do. */ ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 61, ret); ++ } ++ return ret; + } +- /* Get a cursor so we can walk through the parentid */ +- ret = db->cursor(db, txn, &dbc, 0); +- if (ret != 0) { +- ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 68, ret); +- dblayer_release_index_file(be, ai, db); ++ /* Open entryrdn index */ ++ /* Open cursor on the entryrdn index */ ++ ret = bdb_open_subcount_cursor(be, LDBM_ENTRYRDN_STR, txn, &c_entryrdn); ++ if (ret) { ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 62, ret); ++ bdb_close_subcount_cursor(&c_objectclass); + return ret; + } + +- /* Walk along the index */ +- while (1) { ++ key.flags = DB_DBT_USERMEM; ++ key.ulen = sizeof tmp; ++ key.data = tmp; ++ /* Only the first 4 bytes of the data record interrest us */ ++ data.flags = DB_DBT_USERMEM | DB_DBT_PARTIAL; ++ data.ulen = sizeof data_data; ++ data.data = &data_data; ++ data.dlen = sizeof (ID); ++ data.doff = 0; ++ ++ /* Walk along C* keys (usually starting at C1) */ ++ strcpy(tmp, "C"); ++ key.size = 1; ++ ret = c_entryrdn.dbc->c_get(c_entryrdn.dbc, &key, &data, DB_SET_RANGE); ++ ++ while (ret == 0) { + size_t sub_count = 0; +- int found_count = 1; ++ size_t t_sub_count = 0; + ID parentid = 0; + +- /* Foreach key which is an equality key : */ +- data.flags = DB_DBT_MALLOC; +- key.flags = DB_DBT_MALLOC; +- ret = dbc->c_get(dbc, &key, &data, DB_NEXT_NODUP); +- if (NULL != data.data) { +- slapi_ch_free(&(data.data)); +- data.data = NULL; +- } +- if (0 != ret) { +- if (ret != DB_NOTFOUND) { +- ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 62, ret); +- } +- if (NULL != key.data) { +- slapi_ch_free(&(key.data)); +- key.data = NULL; +- } +- break; +- } ++ DUMP_SUBCOUNT_KEY("key:", key, ret); ++ DUMP_SUBCOUNT_KEY("data:", data, ret); + /* check if we need to abort */ + if (job->flags & FLAG_ABORT) { + import_log_notice(job, SLAPI_LOG_ERR, "bdb_update_subordinatecounts", + "numsubordinate generation aborted."); + break; + } ++ if (0 != ret) { ++ ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 63, ret); ++ break; ++ } + /* + * Do an update count + */ +@@ -1241,57 +1232,47 @@ bdb_update_subordinatecounts(backend *be, ImportJob *job, DB_TXN *txn) + key_count); + started_progress_logging = 1; + } ++ if (key.size == 0 || *(char *)key.data != 'C') { ++ /* No more children */ ++ break; ++ } + +- if (*(char *)key.data == EQ_PREFIX) { +- char *idptr = NULL; +- +- /* construct the parent's ID from the key */ +- /* Look for the ID in the hint list supplied by the caller */ +- /* If its there, we know the answer already */ +- idptr = (((char *)key.data) + 1); +- parentid = (ID)atol(idptr); +- PR_ASSERT(0 != parentid); +- ret = bdb_import_subcount_mothers_lookup(mothers, parentid, &sub_count); +- if (0 != ret) { +- IDList *idl = NULL; +- +- /* If it's not, we need to compute it ourselves: */ +- /* Load the IDL matching the key */ +- key.flags = DB_DBT_REALLOC; +- ret = NEW_IDL_NO_ALLID; +- bdb_dbt2dbival(&key, &dbikey, PR_FALSE); +- idl = idl_fetch(be, db, &dbikey, NULL, NULL, &ret); +- bdb_dbival2dbt(&dbikey, &key, PR_TRUE); +- dblayer_value_protect_data(be, &dbikey); +- if ((NULL == idl) || (0 != ret)) { +- ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 4, ret); +- dblayer_release_index_file(be, ai, db); +- return (0 == ret) ? -1 : ret; +- } +- /* The number of IDs in the IDL tells us the number of +- * subordinates for the entry */ +- /* Except, the number might be above the allidsthreshold, +- * in which case */ +- if (ALLIDS(idl)) { +- /* We add this ID to the list for which to trawl */ +- bdb_import_subcount_trawl_add(&trawl_list, parentid); +- found_count = 0; +- } else { +- /* We get the count from the IDL */ +- sub_count = idl->b_nids; +- } +- idl_free(&idl); ++ /* construct the parent's ID from the key */ ++ if (key.size >= sizeof tmp) { ++ ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 64, ret); ++ break; ++ } ++ /* Generate expected value for parentid */ ++ tmp[key.size] = 0; ++ parentid = (ID)atol(tmp+1); ++ PR_ASSERT(0 != parentid); ++ strcpy(oldkey,tmp); ++ /* Walk the entries having same key and check if they are tombstone */ ++ do { ++ /* Reorder data_data */ ++ ID old_data_data = data_data; ++ id_internal_to_stored(old_data_data, (char*)&data_data); ++ if (!bdb_subcount_is_tombstone(&c_objectclass, &data)) { ++ sub_count++; ++ } else { ++ t_sub_count++; + } +- /* Did we get the count ? */ +- if (found_count) { +- PR_ASSERT(0 != sub_count); +- /* If so, update the parent now */ +- bdb_import_update_entry_subcount(be, parentid, sub_count, isencrypted); ++ DUMP_SUBCOUNT_KEY("key:", key, ret); ++ DUMP_SUBCOUNT_KEY("data:", data, ret); ++ ret = c_entryrdn.dbc->c_get(c_entryrdn.dbc, &key, &data, DB_NEXT); ++ DUMP_SUBCOUNT_KEY("key:", key, ret); ++ DUMP_SUBCOUNT_KEY("data:", data, ret); ++ if (ret == 0 && key.size < sizeof tmp) { ++ tmp[key.size] = 0; ++ } else { ++ break; + } +- } +- if (NULL != key.data) { +- slapi_ch_free(&(key.data)); +- key.data = NULL; ++ } while (strcmp(key.data, oldkey) == 0); ++ ret2 = import_update_entry_subcount(be, parentid, sub_count, t_sub_count, isencrypted, (dbi_txn_t*)txn); ++ if (ret2) { ++ ret = ret2; ++ ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 65, ret); ++ break; + } + } + if (started_progress_logging) { +@@ -1301,22 +1282,15 @@ bdb_update_subordinatecounts(backend *be, ImportJob *job, DB_TXN *txn) + key_count); + job->numsubordinates = key_count; + } +- +- ret = dbc->c_close(dbc); +- if (0 != ret) { +- ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 6, ret); +- } +- dblayer_release_index_file(be, ai, db); +- +- /* Now see if we need to go trawling through id2entry for the info +- * we need */ +- if (NULL != trawl_list) { +- ret = bdb_import_subcount_trawl(be, trawl_list, isencrypted); +- if (0 != ret) { +- ldbm_nasty("bdb_update_subordinatecounts", sourcefile, 7, ret); +- } ++ if (ret == DB_NOTFOUND || ret == DB_BUFFER_SMALL) { ++ /* No more records or record is the suffix dn ++ * ==> there is no more children to look at ++ */ ++ ret = 0; + } +- return (ret); ++ bdb_close_subcount_cursor(&c_entryrdn); ++ bdb_close_subcount_cursor(&c_objectclass); ++ return ret; + } + + /* Function used to gather a list of indexed attrs */ +@@ -1453,10 +1427,6 @@ bdb_import_free_job(ImportJob *job) + slapi_ch_free((void **)&asabird); + } + job->index_list = NULL; +- if (NULL != job->mothers) { +- import_subcount_stuff_term(job->mothers); +- slapi_ch_free((void **)&job->mothers); +- } + + bdb_back_free_incl_excl(job->include_subtrees, job->exclude_subtrees); + charray_free(job->input_filenames); +@@ -2708,7 +2678,6 @@ bdb_back_ldif2db(Slapi_PBlock *pb) + } + job->starting_ID = 1; + job->first_ID = 1; +- job->mothers = CALLOC(import_subcount_stuff); + + /* how much space should we allocate to index buffering? */ + job->job_index_buffer_size = bdb_import_get_index_buffer_size(); +@@ -2719,7 +2688,6 @@ bdb_back_ldif2db(Slapi_PBlock *pb) + (job->inst->inst_li->li_import_cachesize / 10) + (1024 * 1024); + PR_Unlock(job->inst->inst_li->li_config_mutex); + } +- import_subcount_stuff_init(job->mothers); + + if (job->task != NULL) { + /* count files, use that to track "progress" in cn=tasks */ +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c +index 08543b888..11054d438 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c +@@ -2244,17 +2244,6 @@ bdb_foreman_do_parentid(ImportJob *job, FifoItem *fi, struct attrinfo *parentid_ + ret = index_addordel_values_ext_sv(be, LDBM_PARENTID_STR, svals, NULL, + entry->ep_id, BE_INDEX_ADD, + NULL, &idl_disposition, NULL); +- if (idl_disposition != IDL_INSERT_NORMAL) { +- char *attr_value = slapi_value_get_berval(svals[0])->bv_val; +- ID parent_id = atol(attr_value); +- +- if (idl_disposition == IDL_INSERT_NOW_ALLIDS) { +- bdb_import_subcount_mother_init(job->mothers, parent_id, +- idl_get_allidslimit(parentid_ai, 0) + 1); +- } else if (idl_disposition == IDL_INSERT_ALLIDS) { +- bdb_import_subcount_mother_count(job->mothers, parent_id); +- } +- } + if (ret != 0) { + import_log_notice(job, SLAPI_LOG_ERR, "bdb_foreman_do_parentid", + "Can't update parentid index (error %d)", ret); +@@ -2989,7 +2978,6 @@ bdb_bulk_import_start(Slapi_PBlock *pb) + job->starting_ID = 1; + job->first_ID = 1; + +- job->mothers = CALLOC(import_subcount_stuff); + /* how much space should we allocate to index buffering? */ + job->job_index_buffer_size = bdb_import_get_index_buffer_size(); + if (job->job_index_buffer_size == 0) { +@@ -2997,7 +2985,6 @@ bdb_bulk_import_start(Slapi_PBlock *pb) + job->job_index_buffer_size = (job->inst->inst_li->li_dbcachesize / 10) + + (1024 * 1024); + } +- import_subcount_stuff_init(job->mothers); + + pthread_mutex_init(&job->wire_lock, NULL); + pthread_cond_init(&job->wire_cv, NULL); +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h +index 0be6cab49..f66640d2e 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.h +@@ -210,8 +210,6 @@ void bdb_restore_file_update(struct ldbminfo *li, const char *directory); + int bdb_import_file_init(ldbm_instance *inst); + void bdb_import_file_update(ldbm_instance *inst); + int bdb_import_file_check(ldbm_instance *inst); +-int bdb_import_subcount_mother_init(import_subcount_stuff *mothers, ID parent_id, size_t count); +-int bdb_import_subcount_mother_count(import_subcount_stuff *mothers, ID parent_id); + void bdb_import_configure_index_buffer_size(size_t size); + size_t bdb_import_get_index_buffer_size(void); + int bdb_ldbm_back_wire_import(Slapi_PBlock *pb); +@@ -230,6 +228,7 @@ int bdb_public_in_import(ldbm_instance *inst); + int bdb_dblayer_cursor_iterate(dbi_cursor_t *cursor, + int (*action_cb)(dbi_val_t *key, dbi_val_t *data, void *ctx), + const dbi_val_t *startingkey, void *ctx); ++dbi_error_t bdb_map_error(const char *funcname, int err); + + + /* dbimpl helpers */ +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c +index e9c9e73f5..801c7eb20 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import.c +@@ -26,7 +26,16 @@ + + static char *sourcefile = "dbmdb_import.c"; + +-static int dbmdb_import_update_entry_subcount(backend *be, ID parentid, size_t sub_count, int isencrypted, back_txn *txn); ++/* Helper struct used to compute numsubordinates */ ++ ++typedef struct { ++ backend *be; ++ dbi_txn_t *txn; ++ const char *attrname; ++ struct attrinfo *ai; ++ dbi_db_t *db; ++ MDB_cursor *dbc; ++} subcount_cursor_info_t; + + /********** routines to manipulate the entry fifo **********/ + +@@ -126,57 +135,74 @@ dbmdb_import_task_abort(Slapi_Task *task) + + /********** helper functions for importing **********/ + ++static void ++dbmdb_close_subcount_cursor(subcount_cursor_info_t *info) ++{ ++ if (info->dbc) { ++ MDB_CURSOR_CLOSE(info->dbc); ++ info->dbc = NULL; ++ } ++ if (info->db) { ++ dblayer_release_index_file(info->be, info->ai, info->db); ++ info->db = NULL; ++ info->ai = NULL; ++ } ++} ++ + static int +-dbmdb_import_update_entry_subcount(backend *be, ID parentid, size_t sub_count, int isencrypted, back_txn *txn) ++dbmdb_open_subcount_cursor(backend *be, const char *attrname, dbi_txn_t *txn, subcount_cursor_info_t *info) + { +- ldbm_instance *inst = (ldbm_instance *)be->be_instance_info; ++ char errfunc[60]; + int ret = 0; +- modify_context mc = {0}; +- char value_buffer[22] = {0}; /* enough digits for 2^64 children */ +- struct backentry *e = NULL; +- int isreplace = 0; +- char *numsub_str = numsubordinates; +- +- /* Get hold of the parent */ +- e = id2entry(be, parentid, txn, &ret); +- if ((NULL == e) || (0 != ret)) { +- slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_update_entry_subcount", "failed to read entry with ID %d ret=%d\n", +- parentid, ret); +- ldbm_nasty("dbmdb_import_update_entry_subcount", sourcefile, 5, ret); +- return (0 == ret) ? -1 : ret; +- } +- /* Lock it (not really required since we're single-threaded here, but +- * let's do it so we can reuse the modify routines) */ +- cache_lock_entry(&inst->inst_cache, e); +- modify_init(&mc, e); +- mc.attr_encrypt = isencrypted; +- sprintf(value_buffer, "%lu", (long unsigned int)sub_count); +- /* If it is a tombstone entry, add tombstonesubordinates instead of +- * numsubordinates. */ +- if (slapi_entry_flag_is_set(e->ep_entry, SLAPI_ENTRY_FLAG_TOMBSTONE)) { +- numsub_str = LDBM_TOMBSTONE_NUMSUBORDINATES_STR; +- } +- /* attr numsubordinates/tombstonenumsubordinates could already exist in +- * the entry, let's check whether it's already there or not */ +- isreplace = (attrlist_find(e->ep_entry->e_attrs, numsub_str) != NULL); +- { +- int op = isreplace ? LDAP_MOD_REPLACE : LDAP_MOD_ADD; +- Slapi_Mods *smods = slapi_mods_new(); + +- slapi_mods_add(smods, op | LDAP_MOD_BVALUES, numsub_str, +- strlen(value_buffer), value_buffer); +- ret = modify_apply_mods(&mc, smods); /* smods passed in */ +- } +- if (0 == ret || LDAP_TYPE_OR_VALUE_EXISTS == ret) { +- /* This will correctly index subordinatecount: */ +- ret = modify_update_all(be, NULL, &mc, txn); +- if (0 == ret) { +- modify_switch_entries(&mc, be); ++ snprintf(errfunc, (sizeof errfunc), "%s[%s]", __FUNCTION__, attrname); ++ info->attrname = attrname; ++ info->txn = txn; ++ info->be = be; ++ ++ /* Lets get the attrinfo */ ++ ainfo_get(be, (char*)attrname, &info->ai); ++ PR_ASSERT(info->ai); ++ /* Lets get the db instance */ ++ if ((ret = dblayer_get_index_file(be, info->ai, &info->db, 0)) != 0) { ++ if (ret == DBI_RC_NOTFOUND) { ++ dbmdb_close_subcount_cursor(info); ++ return 0; + } ++ ldbm_nasty(errfunc, sourcefile, 70, ret); ++ dbmdb_close_subcount_cursor(info); ++ return ret; ++ } ++ ++ /* Lets get the cursor */ ++ if ((ret = MDB_CURSOR_OPEN(TXN(info->txn), DB(info->db), &info->dbc)) != 0) { ++ ldbm_nasty(errfunc, sourcefile, 71, ret); ++ dbmdb_close_subcount_cursor(info); ++ ret = dbmdb_map_error(__FUNCTION__, ret); ++ } ++ return 0; ++} ++ ++static bool ++dbmdb_subcount_is_tombstone(subcount_cursor_info_t *info, MDB_val *id) ++{ ++ /* ++ * Check if record =nstombstone ==> id exists in objectclass index ++ */ ++ MDB_val key = {0}; ++ int ret; ++ key.mv_data = "=nstombstone" ; ++ key.mv_size = 13; ++ ret = MDB_CURSOR_GET(info->dbc, &key, id, MDB_GET_BOTH); ++ switch (ret) { ++ case 0: ++ return true; ++ case MDB_NOTFOUND: ++ return false; ++ default: ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 72, ret); ++ return false; + } +- /* entry is unlocked and returned to the cache in modify_term */ +- modify_term(&mc, be); +- return ret; + } + + /* +@@ -188,47 +214,56 @@ dbmdb_import_update_entry_subcount(backend *be, ID parentid, size_t sub_count, i + static int + dbmdb_update_subordinatecounts(backend *be, ImportJob *job, dbi_txn_t *txn) + { +- int isencrypted = job->encrypt; ++ subcount_cursor_info_t c_objectclass = {0}; ++ subcount_cursor_info_t c_entryrdn = {0}; + int started_progress_logging = 0; ++ int isencrypted = job->encrypt; ++ MDB_val data = {0}; ++ MDB_val key = {0}; ++ back_txn btxn = {0}; + int key_count = 0; ++ char tmp[11]; ++ int ret2 = 0; + int ret = 0; +- dbmdb_dbi_t*db = NULL; +- MDB_cursor *dbc = NULL; +- struct attrinfo *ai = NULL; +- MDB_val key = {0}; +- MDB_val data = {0}; +- dbmdb_cursor_t cursor = {0}; +- struct ldbminfo *li = (struct ldbminfo*)be->be_database->plg_private; +- back_txn btxn = {0}; +- +- /* Open the parentid index */ +- ainfo_get(be, LDBM_PARENTID_STR, &ai); + +- /* Open the parentid index file */ +- if ((ret = dblayer_get_index_file(be, ai, (dbi_db_t**)&db, DBOPEN_CREATE)) != 0) { +- ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 67, ret); +- return (ret); +- } +- /* Get a cursor with r/w txn so we can walk through the parentid */ +- ret = dbmdb_open_cursor(&cursor, MDB_CONFIG(li), db, 0); +- if (ret != 0) { +- ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 68, ret); +- dblayer_release_index_file(be, ai, db); +- return ret; ++ PR_ASSERT(txn == NULL); /* Apparently always called with null txn */ ++ /* Need txn / should be rw to update id2entry */ ++ ret = START_TXN(&txn, NULL, 0); ++ if (ret) { ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 60, ret); ++ return dbmdb_map_error(__FUNCTION__, ret); + } +- dbc = cursor.cur; +- txn = cursor.txn; + btxn.back_txn_txn = txn; +- ret = MDB_CURSOR_GET(dbc, &key, &data, MDB_FIRST); ++ /* Open cursor on the objectclass index */ ++ ret = dbmdb_open_subcount_cursor(be, SLAPI_ATTR_OBJECTCLASS, txn, &c_objectclass); ++ if (ret) { ++ if (ret != DBI_RC_NOTFOUND) { ++ /* No database ==> There is nothing to do. */ ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 61, ret); ++ } ++ return END_TXN(&txn, ret); ++ } ++ /* Open cursor on the entryrdn index */ ++ ret = dbmdb_open_subcount_cursor(be, LDBM_ENTRYRDN_STR, txn, &c_entryrdn); ++ if (ret) { ++ ldbm_nasty((char*)__FUNCTION__, sourcefile, 62, ret); ++ dbmdb_close_subcount_cursor(&c_objectclass); ++ return END_TXN(&txn, ret); ++ } + +- /* Walk along the index */ +- while (ret != MDB_NOTFOUND) { ++ /* Walk along C* keys (usually starting at C1) */ ++ key.mv_data = "C"; ++ key.mv_size = 1; ++ ret = MDB_CURSOR_GET(c_entryrdn.dbc, &key, &data, MDB_SET_RANGE); ++ while (ret == 0) { + size_t sub_count = 0; ++ size_t t_sub_count = 0; ++ MDB_val oldkey = key; + ID parentid = 0; + + if (0 != ret) { + key.mv_data=NULL; +- ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 62, ret); ++ ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 63, ret); + break; + } + /* check if we need to abort */ +@@ -247,33 +282,50 @@ dbmdb_update_subordinatecounts(backend *be, ImportJob *job, dbi_txn_t *txn) + key_count); + started_progress_logging = 1; + } ++ if (!key.mv_data || *(char *)key.mv_data != 'C') { ++ /* No more children */ ++ break; ++ } + +- if (*(char *)key.mv_data == EQ_PREFIX) { +- char tmp[11]; +- +- /* construct the parent's ID from the key */ +- if (key.mv_size >= sizeof tmp) { ++ /* construct the parent's ID from the key */ ++ if (key.mv_size >= sizeof tmp) { ++ ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 64, ret); ++ ret = DBI_RC_INVALID; ++ break; ++ } ++ /* Generate expected value for parentid */ ++ memcpy(tmp, key.mv_data, key.mv_size); ++ tmp[key.mv_size] = 0; ++ parentid = (ID)atol(tmp+1); ++ PR_ASSERT(0 != parentid); ++ oldkey = key; ++ /* Walk the entries having same key and check if they are tombstone */ ++ do { ++ /* Reorder data */ ++ ID old_data, new_data; ++ if (data.mv_size < sizeof old_data) { + ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 66, ret); ++ ret = DBI_RC_INVALID; + break; + } +- memcpy(tmp, key.mv_data, key.mv_size); +- tmp[key.mv_size] = 0; +- parentid = (ID)atol(tmp+1); +- PR_ASSERT(0 != parentid); +- /* Get number of records having the same key */ +- ret = mdb_cursor_count(dbc, &sub_count); +- if (ret) { +- ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 63, ret); +- break; +- } +- PR_ASSERT(0 != sub_count); +- ret = dbmdb_import_update_entry_subcount(be, parentid, sub_count, isencrypted, &btxn); +- if (ret) { +- ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 64, ret); +- break; ++ memcpy(&old_data, data.mv_data, sizeof old_data); ++ id_internal_to_stored(old_data, (char*)&new_data); ++ data.mv_data = &new_data; ++ data.mv_size = sizeof new_data; ++ if (!dbmdb_subcount_is_tombstone(&c_objectclass, &data)) { ++ sub_count++; ++ } else { ++ t_sub_count++; + } ++ ret = MDB_CURSOR_GET(c_entryrdn.dbc, &key, &data, MDB_NEXT); ++ } while (ret == 0 && key.mv_size == oldkey.mv_size && ++ memcmp(key.mv_data, oldkey.mv_data, key.mv_size) == 0); ++ ret2 = import_update_entry_subcount(be, parentid, sub_count, t_sub_count, isencrypted, &btxn); ++ if (ret2) { ++ ret = ret2; ++ ldbm_nasty("dbmdb_update_subordinatecounts", sourcefile, 65, ret); ++ break; + } +- ret = MDB_CURSOR_GET(dbc, &key, &data, MDB_NEXT_NODUP); + } + if (started_progress_logging) { + /* Finish what we started... */ +@@ -285,11 +337,13 @@ dbmdb_update_subordinatecounts(backend *be, ImportJob *job, dbi_txn_t *txn) + if (ret == MDB_NOTFOUND) { + ret = 0; + } +- +- dbmdb_close_cursor(&cursor, ret); +- dblayer_release_index_file(be, ai, db); +- +- return (ret); ++ dbmdb_close_subcount_cursor(&c_entryrdn); ++ dbmdb_close_subcount_cursor(&c_objectclass); ++ if (txn) { ++ return END_TXN(&txn, ret); ++ } else { ++ return ret; ++ } + } + + /* Function used to gather a list of indexed attrs */ +@@ -364,10 +418,6 @@ dbmdb_import_free_job(ImportJob *job) + slapi_ch_free((void **)&asabird); + } + job->index_list = NULL; +- if (NULL != job->mothers) { +- import_subcount_stuff_term(job->mothers); +- slapi_ch_free((void **)&job->mothers); +- } + + dbmdb_back_free_incl_excl(job->include_subtrees, job->exclude_subtrees); + +@@ -1245,7 +1295,6 @@ dbmdb_run_ldif2db(Slapi_PBlock *pb) + } + job->starting_ID = 1; + job->first_ID = 1; +- job->mothers = CALLOC(import_subcount_stuff); + + /* how much space should we allocate to index buffering? */ + job->job_index_buffer_size = dbmdb_import_get_index_buffer_size(); +@@ -1256,7 +1305,6 @@ dbmdb_run_ldif2db(Slapi_PBlock *pb) + (job->inst->inst_li->li_import_cachesize / 10) + (1024 * 1024); + PR_Unlock(job->inst->inst_li->li_config_mutex); + } +- import_subcount_stuff_init(job->mothers); + + if (job->task != NULL) { + /* count files, use that to track "progress" in cn=tasks */ +@@ -1383,7 +1431,6 @@ dbmdb_bulk_import_start(Slapi_PBlock *pb) + job->starting_ID = 1; + job->first_ID = 1; + +- job->mothers = CALLOC(import_subcount_stuff); + /* how much space should we allocate to index buffering? */ + job->job_index_buffer_size = dbmdb_import_get_index_buffer_size(); + if (job->job_index_buffer_size == 0) { +@@ -1391,7 +1438,6 @@ dbmdb_bulk_import_start(Slapi_PBlock *pb) + job->job_index_buffer_size = (job->inst->inst_li->li_dbcachesize / 10) + + (1024 * 1024); + } +- import_subcount_stuff_init(job->mothers); + dbmdb_import_init_writer(job, IM_BULKIMPORT); + + pthread_mutex_init(&job->wire_lock, NULL); +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c +index aa10d3704..f44b831aa 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c +@@ -2900,6 +2900,7 @@ dbmdb_add_op_attrs(ImportJob *job, struct backentry *ep, ID pid) + /* Get rid of attributes you're not allowed to specify yourself */ + slapi_entry_delete_values(ep->ep_entry, hassubordinates, NULL); + slapi_entry_delete_values(ep->ep_entry, numsubordinates, NULL); ++ slapi_entry_delete_values(ep->ep_entry, tombstone_numsubordinates, NULL); + + /* Upgrade DN format only */ + /* Set current parentid to e_aux_attrs to remove it from the index file. */ +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_instance.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_instance.c +index a794430e2..fbf8a9a47 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_instance.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_instance.c +@@ -1357,7 +1357,7 @@ int dbmdb_open_cursor(dbmdb_cursor_t *dbicur, dbmdb_ctx_t *ctx, dbmdb_dbi_t *dbi + dbicur->dbi = dbi; + if (ctx->readonly) + flags |= MDB_RDONLY; +- rc = START_TXN(&dbicur->txn, NULL, 0); ++ rc = START_TXN(&dbicur->txn, NULL, ((flags&MDB_RDONLY) ? TXNFL_RDONLY : 0)); + if (rc) { + return rc; + } +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h +index fdc4a9288..c5a72e21c 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h +@@ -394,8 +394,6 @@ void dbmdb_restore_file_update(struct ldbminfo *li, const char *directory); + int dbmdb_import_file_init(ldbm_instance *inst); + void dbmdb_import_file_update(ldbm_instance *inst); + int dbmdb_import_file_check(ldbm_instance *inst); +-int dbmdb_import_subcount_mother_init(import_subcount_stuff *mothers, ID parent_id, size_t count); +-int dbmdb_import_subcount_mother_count(import_subcount_stuff *mothers, ID parent_id); + void dbmdb_import_configure_index_buffer_size(size_t size); + size_t dbmdb_import_get_index_buffer_size(void); + int dbmdb_ldbm_back_wire_import(Slapi_PBlock *pb); +diff --git a/ldap/servers/slapd/back-ldbm/import.c b/ldap/servers/slapd/back-ldbm/import.c +index 5a03bb533..f9a20051a 100644 +--- a/ldap/servers/slapd/back-ldbm/import.c ++++ b/ldap/servers/slapd/back-ldbm/import.c +@@ -241,3 +241,62 @@ wait_for_ref_count(Slapi_Counter *inst_ref_count) + /* Done waiting, return the current ref count */ + return slapi_counter_get_value(inst_ref_count); + } ++ ++/********** helper functions for importing **********/ ++ ++int ++import_update_entry_subcount(backend *be, ID parentid, size_t sub_count, size_t t_sub_count, int isencrypted, back_txn *txn) ++{ ++ ldbm_instance *inst = (ldbm_instance *)be->be_instance_info; ++ int ret = 0; ++ modify_context mc = {0}; ++ char value_buffer[22] = {0}; /* enough digits for 2^64 children */ ++ char t_value_buffer[22] = {0}; /* enough digits for 2^64 children */ ++ struct backentry *e = NULL; ++ char *numsub_str = numsubordinates; ++ Slapi_Mods *smods = NULL; ++ static char *sourcefile = "import.c"; ++ ++ /* Get hold of the parent */ ++ e = id2entry(be, parentid, txn, &ret); ++ if ((NULL == e) || (0 != ret)) { ++ slapi_log_err(SLAPI_LOG_ERR, "import_update_entry_subcount", "failed to read entry with ID %d ret=%d\n", ++ parentid, ret); ++ ldbm_nasty("import_update_entry_subcount", sourcefile, 5, ret); ++ return (0 == ret) ? -1 : ret; ++ } ++ /* Lock it (not really required since we're single-threaded here, but ++ * let's do it so we can reuse the modify routines) */ ++ cache_lock_entry(&inst->inst_cache, e); ++ modify_init(&mc, e); ++ mc.attr_encrypt = isencrypted; ++ sprintf(value_buffer, "%lu", (long unsigned int)sub_count); ++ sprintf(t_value_buffer, "%lu", (long unsigned int)t_sub_count); ++ smods = slapi_mods_new(); ++ if (sub_count) { ++ slapi_mods_add(smods, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES, numsub_str, ++ strlen(value_buffer), value_buffer); ++ } else { ++ /* Make sure that the attribute is deleted */ ++ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES, numsub_str, NULL); ++ } ++ if (t_sub_count) { ++ slapi_mods_add(smods, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES, LDBM_TOMBSTONE_NUMSUBORDINATES_STR, ++ strlen(t_value_buffer), t_value_buffer); ++ } else { ++ /* Make sure that the attribute is deleted */ ++ slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES, LDBM_TOMBSTONE_NUMSUBORDINATES_STR, NULL); ++ } ++ ret = modify_apply_mods(&mc, smods); /* smods passed in */ ++ if (0 == ret) { ++ /* This will correctly index subordinatecount: */ ++ ret = modify_update_all(be, NULL, &mc, txn); ++ if (0 == ret) { ++ modify_switch_entries(&mc, be); ++ } ++ } ++ /* entry is unlocked and returned to the cache in modify_term */ ++ modify_term(&mc, be); ++ return ret; ++} ++ +diff --git a/ldap/servers/slapd/back-ldbm/import.h b/ldap/servers/slapd/back-ldbm/import.h +index b3f6b7493..e066f4195 100644 +--- a/ldap/servers/slapd/back-ldbm/import.h ++++ b/ldap/servers/slapd/back-ldbm/import.h +@@ -117,7 +117,6 @@ typedef struct _ImportJob + * another pass */ + int uuid_gen_type; /* kind of uuid to generate */ + char *uuid_namespace; /* namespace for name-generated uuid */ +- import_subcount_stuff *mothers; + double average_progress_rate; + double recent_progress_rate; + double cache_hit_ratio; +@@ -209,6 +208,7 @@ struct _import_worker_info + /* import.c */ + void import_log_notice(ImportJob *job, int log_level, char *subsystem, char *format, ...); + int import_main_offline(void *arg); ++int import_update_entry_subcount(backend *be, ID parentid, size_t sub_count, size_t t_sub_count, int isencrypted, back_txn *txn); + + /* ldif2ldbm.c */ + void reset_progress(void); +diff --git a/ldap/servers/slapd/back-ldbm/ldif2ldbm.c b/ldap/servers/slapd/back-ldbm/ldif2ldbm.c +index 403ce6ae8..8b0386489 100644 +--- a/ldap/servers/slapd/back-ldbm/ldif2ldbm.c ++++ b/ldap/servers/slapd/back-ldbm/ldif2ldbm.c +@@ -54,49 +54,6 @@ typedef struct _export_args + /* static functions */ + + +-/********** common routines for classic/deluxe import code **********/ +- +-static PRIntn +-import_subcount_hash_compare_keys(const void *v1, const void *v2) +-{ +- return (((ID)((uintptr_t)v1) == (ID)((uintptr_t)v2)) ? 1 : 0); +-} +- +-static PRIntn +-import_subcount_hash_compare_values(const void *v1, const void *v2) +-{ +- return (((size_t)v1 == (size_t)v2) ? 1 : 0); +-} +- +-static PLHashNumber +-import_subcount_hash_fn(const void *id) +-{ +- return (PLHashNumber)((uintptr_t)id); +-} +- +-void +-import_subcount_stuff_init(import_subcount_stuff *stuff) +-{ +- stuff->hashtable = PL_NewHashTable(IMPORT_SUBCOUNT_HASHTABLE_SIZE, +- import_subcount_hash_fn, import_subcount_hash_compare_keys, +- import_subcount_hash_compare_values, NULL, NULL); +-} +- +-void +-import_subcount_stuff_term(import_subcount_stuff *stuff) +-{ +- if (stuff != NULL && stuff->hashtable != NULL) { +- PL_HashTableDestroy(stuff->hashtable); +- } +-} +- +- +- +-/********** functions for maintaining the subordinate count **********/ +- +- +- +- + /********** ldif2db entry point **********/ + + /* +diff --git a/ldap/servers/slapd/control.c b/ldap/servers/slapd/control.c +index f8744901a..7aeeba885 100644 +--- a/ldap/servers/slapd/control.c ++++ b/ldap/servers/slapd/control.c +@@ -172,7 +172,7 @@ create_sessiontracking_ctrl(const char *session_tracking_id, LDAPControl **sessi + { + BerElement *ctrlber = NULL; + char *undefined_sid = "undefined sid"; +- char *sid; ++ const char *sid; + int rc = 0; + int tag; + LDAPControl *ctrl = NULL; +diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py +index 23a20739f..d57a91929 100644 +--- a/src/lib389/lib389/__init__.py ++++ b/src/lib389/lib389/__init__.py +@@ -1803,7 +1803,7 @@ class DirSrv(SimpleLDAPObject, object): + one entry. + @param - entry dn + @param - search scope, in ldap.SCOPE_BASE (default), +- ldap.SCOPE_SUB, ldap.SCOPE_ONE ++ ldap.SCOPE_SUB, ldap.SCOPE_ONELEVEL + @param filterstr - filterstr, default '(objectClass=*)' from + SimpleLDAPObject + @param attrlist - list of attributes to retrieve. eg ['cn', 'uid'] +diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py +index 1f9f1556f..37277296d 100644 +--- a/src/lib389/lib389/_mapped_object.py ++++ b/src/lib389/lib389/_mapped_object.py +@@ -200,7 +200,7 @@ class DSLdapObject(DSLogging, DSLint): + if scope == 'base': + search_scope = ldap.SCOPE_BASE + elif scope == 'one': +- search_scope = ldap.SCOPE_ONE ++ search_scope = ldap.SCOPE_ONELEVEL + elif scope == 'subtree': + search_scope = ldap.SCOPE_SUBTREE + return _search_ext_s(self._instance,self._dn, search_scope, filter, +-- +2.49.0 + diff --git a/0040-Issue-6910-Fix-latest-coverity-issues.patch b/0040-Issue-6910-Fix-latest-coverity-issues.patch new file mode 100644 index 0000000..c1b2abc --- /dev/null +++ b/0040-Issue-6910-Fix-latest-coverity-issues.patch @@ -0,0 +1,574 @@ +From 5d2dc7f78f0a834e46d5665f0c12024da5ddda9e Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Mon, 28 Jul 2025 17:12:33 -0400 +Subject: [PATCH] Issue 6910 - Fix latest coverity issues + +Description: + +Fix various coverity/ASAN warnings: + +- CID 1618837: Out-of-bounds read (OVERRUN) - bdb_bdbreader_glue.c +- CID 1618831: Resource leak (RESOURCE_LEAK) - bdb_layer.c +- CID 1612606: Resource leak (RESOURCE_LEAK) - log.c +- CID 1611461: Uninitialized pointer read (UNINIT) - repl5_agmt.c +- CID 1568589: Dereference before null check (REVERSE_INULL) - repl5_agmt.c +- CID 1590353: Logically dead code (DEADCODE) - repl5_agmt.c +- CID 1611460: Logically dead code (DEADCODE) - control.c +- CID 1610568: Dereference after null check (FORWARD_NULL) - modify.c +- CID 1591259: Out-of-bounds read (OVERRUN) - memberof.c +- CID 1550231: Unsigned compared against 0 (NO_EFFECT) - memberof_config.c +- CID 1548904: Overflowed constant (INTEGER_OVERFLOW) - ch_malloc.c +- CID 1548902: Overflowed constant (INTEGER_OVERFLOW) - dse.lc +- CID 1548900: Overflowed return value (INTEGER_OVERFLOW) - acct_util.c +- CID 1548898: Overflowed constant (INTEGER_OVERFLOW) - parents.c +- CID 1546849: Resource leak (RESOURCE_LEAK) - referint.c +- ASAN - Use after free - automember.c + +Relates: http://github.com/389ds/389-ds-base/issues/6910 + +Reviewed by: progier & spichugi(Thanks!) +--- + ldap/servers/plugins/acctpolicy/acct_util.c | 6 ++- + ldap/servers/plugins/automember/automember.c | 9 ++-- + ldap/servers/plugins/memberof/memberof.c | 15 +++++-- + .../plugins/memberof/memberof_config.c | 11 +++-- + ldap/servers/plugins/referint/referint.c | 4 +- + ldap/servers/plugins/replication/repl5_agmt.c | 41 ++++++++----------- + .../slapd/back-ldbm/db-bdb/bdb_import.c | 5 ++- + .../back-ldbm/db-bdb/bdb_instance_config.c | 3 +- + .../slapd/back-ldbm/db-bdb/bdb_layer.c | 13 ++++-- + ldap/servers/slapd/back-ldbm/parents.c | 4 +- + ldap/servers/slapd/ch_malloc.c | 4 +- + ldap/servers/slapd/control.c | 5 +-- + ldap/servers/slapd/dse.c | 4 +- + ldap/servers/slapd/log.c | 5 ++- + ldap/servers/slapd/modify.c | 6 +-- + ldap/servers/slapd/passwd_extop.c | 2 +- + ldap/servers/slapd/unbind.c | 12 ++++-- + 17 files changed, 88 insertions(+), 61 deletions(-) + +diff --git a/ldap/servers/plugins/acctpolicy/acct_util.c b/ldap/servers/plugins/acctpolicy/acct_util.c +index b27eeaff1..7735d10e6 100644 +--- a/ldap/servers/plugins/acctpolicy/acct_util.c ++++ b/ldap/servers/plugins/acctpolicy/acct_util.c +@@ -17,7 +17,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + Contributors: + Hewlett-Packard Development Company, L.P. + +-Copyright (C) 2021 Red Hat, Inc. ++Copyright (C) 2025 Red Hat, Inc. + ******************************************************************************/ + + #include +@@ -248,6 +248,10 @@ gentimeToEpochtime(char *gentimestr) + + /* Turn tm object into local epoch time */ + epochtime = mktime(&t); ++ if (epochtime == (time_t) -1) { ++ /* mktime failed */ ++ return 0; ++ } + + /* Turn local epoch time into GMT epoch time */ + epochtime -= zone_offset; +diff --git a/ldap/servers/plugins/automember/automember.c b/ldap/servers/plugins/automember/automember.c +index f900db7f2..9eade495e 100644 +--- a/ldap/servers/plugins/automember/automember.c ++++ b/ldap/servers/plugins/automember/automember.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2022 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -1756,9 +1756,10 @@ automember_update_member_value(Slapi_Entry *member_e, const char *group_dn, char + + mod_pb = slapi_pblock_new(); + /* Do a single mod with error overrides for DEL/ADD */ +- result = slapi_single_modify_internal_override(mod_pb, slapi_sdn_new_dn_byval(group_dn), mods, +- automember_get_plugin_id(), 0); +- ++ Slapi_DN *sdn = slapi_sdn_new_normdn_byref(group_dn); ++ result = slapi_single_modify_internal_override(mod_pb, sdn, mods, ++ automember_get_plugin_id(), 0); ++ slapi_sdn_free(&sdn); + if(add){ + if (result != LDAP_SUCCESS) { + slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM, +diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c +index 073d8d938..cfda977f0 100644 +--- a/ldap/servers/plugins/memberof/memberof.c ++++ b/ldap/servers/plugins/memberof/memberof.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2021 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -1655,6 +1655,7 @@ memberof_call_foreach_dn(Slapi_PBlock *pb __attribute__((unused)), Slapi_DN *sdn + /* We already did the search for this backend, don't + * do it again when we fall through */ + do_suffix_search = PR_FALSE; ++ slapi_pblock_init(search_pb); + } + } + } else if (!all_backends) { +@@ -3763,6 +3764,10 @@ memberof_replace_list(Slapi_PBlock *pb, MemberOfConfig *config, Slapi_DN *group_ + + pre_index++; + } else { ++ if (pre_index >= pre_total || post_index >= post_total) { ++ /* Don't overrun pre_array/post_array */ ++ break; ++ } + /* decide what to do */ + int cmp = memberof_compare( + config, +@@ -4453,10 +4458,12 @@ memberof_add_memberof_attr(LDAPMod **mods, const char *dn, char *add_oc) + + while (1) { + slapi_pblock_init(mod_pb); +- ++ Slapi_DN *sdn = slapi_sdn_new_normdn_byref(dn); + /* Internal mod with error overrides for DEL/ADD */ +- rc = slapi_single_modify_internal_override(mod_pb, slapi_sdn_new_normdn_byref(dn), single_mod, +- memberof_get_plugin_id(), SLAPI_OP_FLAG_BYPASS_REFERRALS); ++ rc = slapi_single_modify_internal_override(mod_pb, sdn, single_mod, ++ memberof_get_plugin_id(), ++ SLAPI_OP_FLAG_BYPASS_REFERRALS); ++ slapi_sdn_free(&sdn); + if (rc == LDAP_OBJECT_CLASS_VIOLATION) { + if (!add_oc || added_oc) { + /* +diff --git a/ldap/servers/plugins/memberof/memberof_config.c b/ldap/servers/plugins/memberof/memberof_config.c +index 1e83ba6e0..e4da351d9 100644 +--- a/ldap/servers/plugins/memberof/memberof_config.c ++++ b/ldap/servers/plugins/memberof/memberof_config.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2021 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -570,21 +570,24 @@ memberof_apply_config(Slapi_PBlock *pb __attribute__((unused)), + if (num_groupattrs > 1) { + size_t bytes_out = 0; + size_t filter_str_len = groupattr_name_len + (num_groupattrs * 4) + 4; ++ int32_t rc = 0; + + /* Allocate enough space for the filter */ + filter_str = slapi_ch_malloc(filter_str_len); + + /* Add beginning of filter. */ +- bytes_out = snprintf(filter_str, filter_str_len - bytes_out, "(|"); +- if (bytes_out<0) { ++ rc = snprintf(filter_str, filter_str_len - bytes_out, "(|"); ++ if (rc < 0) { + slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM, "snprintf unexpectly failed in memberof_apply_config.\n"); + *returncode = LDAP_UNWILLING_TO_PERFORM; + goto done; ++ } else { ++ bytes_out = rc; + } + + /* Add filter section for each groupattr. */ + for (size_t i=0; theConfig.groupattrs && theConfig.groupattrs[i]; i++) { +- size_t bytes_read = snprintf(filter_str + bytes_out, filter_str_len - bytes_out, "(%s=*)", theConfig.groupattrs[i]); ++ int32_t bytes_read = snprintf(filter_str + bytes_out, filter_str_len - bytes_out, "(%s=*)", theConfig.groupattrs[i]); + if (bytes_read<0) { + slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM, "snprintf unexpectly failed in memberof_apply_config.\n"); + *returncode = LDAP_UNWILLING_TO_PERFORM; +diff --git a/ldap/servers/plugins/referint/referint.c b/ldap/servers/plugins/referint/referint.c +index 5d7f9e5dd..5746b913f 100644 +--- a/ldap/servers/plugins/referint/referint.c ++++ b/ldap/servers/plugins/referint/referint.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2021 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -1499,6 +1499,8 @@ referint_thread_func(void *arg __attribute__((unused))) + slapi_sdn_free(&sdn); + continue; + } ++ ++ slapi_sdn_free(&tmpsuperior); + if (!strcasecmp(ptoken, "NULL")) { + tmpsuperior = NULL; + } else { +diff --git a/ldap/servers/plugins/replication/repl5_agmt.c b/ldap/servers/plugins/replication/repl5_agmt.c +index 0a81167b7..eed97578e 100644 +--- a/ldap/servers/plugins/replication/repl5_agmt.c ++++ b/ldap/servers/plugins/replication/repl5_agmt.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2021 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -202,7 +202,7 @@ agmt_init_session_id(Repl_Agmt *ra) + char *host = NULL; /* e.g. localhost.domain */ + char port[10]; /* e.g. 389 */ + char sport[10]; /* e.g. 636 */ +- char *hash_in; ++ char *hash_in = NULL; + int32_t max_str_sid = SESSION_ID_STR_SZ - 4; + + if (ra == NULL) { +@@ -2718,31 +2718,26 @@ agmt_update_init_status(Repl_Agmt *ra) + mod_idx++; + } + +- if (nb_mods) { +- /* it is ok to release the lock here because we are done with the agreement data. +- we have to do it before issuing the modify operation because it causes +- agmtlist_notify_all to be called which uses the same lock - hence the deadlock */ +- PR_Unlock(ra->lock); +- +- pb = slapi_pblock_new(); +- mods[nb_mods] = NULL; ++ /* it is ok to release the lock here because we are done with the agreement data. ++ we have to do it before issuing the modify operation because it causes ++ agmtlist_notify_all to be called which uses the same lock - hence the deadlock */ ++ PR_Unlock(ra->lock); + +- slapi_modify_internal_set_pb_ext(pb, ra->dn, mods, NULL, NULL, +- repl_get_plugin_identity(PLUGIN_MULTISUPPLIER_REPLICATION), 0); +- slapi_modify_internal_pb(pb); ++ pb = slapi_pblock_new(); ++ mods[nb_mods] = NULL; + +- slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &rc); +- if (rc != LDAP_SUCCESS && rc != LDAP_NO_SUCH_ATTRIBUTE) { +- slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "agmt_update_consumer_ruv - " +- "%s: agmt_update_consumer_ruv: " +- "failed to update consumer's RUV; LDAP error - %d\n", +- ra->long_name, rc); +- } ++ slapi_modify_internal_set_pb_ext(pb, ra->dn, mods, NULL, NULL, ++ repl_get_plugin_identity(PLUGIN_MULTISUPPLIER_REPLICATION), 0); ++ slapi_modify_internal_pb(pb); + +- slapi_pblock_destroy(pb); +- } else { +- PR_Unlock(ra->lock); ++ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &rc); ++ if (rc != LDAP_SUCCESS && rc != LDAP_NO_SUCH_ATTRIBUTE) { ++ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "agmt_update_consumer_ruv - " ++ "%s: agmt_update_consumer_ruv: failed to update consumer's RUV; LDAP error - %d\n", ++ ra->long_name, rc); + } ++ ++ slapi_pblock_destroy(pb); + slapi_ch_free((void **)&mods); + slapi_mod_done(&smod_start_time); + slapi_mod_done(&smod_end_time); +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +index 46c80ec3d..0127bf2f9 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2020 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -947,6 +947,7 @@ bdb_ancestorid_new_idl_create_index(backend *be, ImportJob *job) + EQ_PREFIX, (u_long)id); + key.size++; /* include the null terminator */ + ret = NEW_IDL_NO_ALLID; ++ idl_free(&children); + children = idl_fetch(be, db_pid, &key, txn, ai_pid, &ret); + if (ret != 0) { + ldbm_nasty("bdb_ancestorid_new_idl_create_index", sourcefile, 13070, ret); +@@ -957,6 +958,7 @@ bdb_ancestorid_new_idl_create_index(backend *be, ImportJob *job) + if (job->flags & FLAG_ABORT) { + import_log_notice(job, SLAPI_LOG_ERR, "bdb_ancestorid_new_idl_create_index", + "ancestorid creation aborted."); ++ idl_free(&children); + ret = -1; + break; + } +@@ -1290,6 +1292,7 @@ bdb_update_subordinatecounts(backend *be, ImportJob *job, DB_TXN *txn) + } + bdb_close_subcount_cursor(&c_entryrdn); + bdb_close_subcount_cursor(&c_objectclass); ++ + return ret; + } + +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_instance_config.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_instance_config.c +index bb515a23f..44a624fde 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_instance_config.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_instance_config.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2020 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -261,6 +261,7 @@ bdb_instance_cleanup(struct ldbm_instance *inst) + if (inst_dirp && *inst_dir) { + return_value = env->remove(env, inst_dirp, 0); + } else { ++ slapi_ch_free((void **)&env); + return_value = -1; + } + if (return_value == EBUSY) { +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +index 53f1cde69..b1e44a919 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2023 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -2027,9 +2027,13 @@ bdb_pre_close(struct ldbminfo *li) + conf = (bdb_config *)li->li_dblayer_config; + bdb_db_env *pEnv = (bdb_db_env *)priv->dblayer_env; + ++ if (pEnv == NULL) { ++ return; ++ } ++ + pthread_mutex_lock(&pEnv->bdb_thread_count_lock); + +- if (conf->bdb_stop_threads || !pEnv) { ++ if (conf->bdb_stop_threads) { + /* already stopped. do nothing... */ + goto timeout_escape; + } +@@ -2203,6 +2207,7 @@ bdb_remove_env(struct ldbminfo *li) + } + if (NULL == li) { + slapi_log_err(SLAPI_LOG_ERR, "bdb_remove_env", "No ldbm info is given\n"); ++ slapi_ch_free((void **)&env); + return -1; + } + +@@ -2212,10 +2217,11 @@ bdb_remove_env(struct ldbminfo *li) + if (rc) { + slapi_log_err(SLAPI_LOG_ERR, + "bdb_remove_env", "Failed to remove DB environment files. " +- "Please remove %s/__db.00# (# is 1 through 6)\n", ++ "Please remove %s/__db.00# (# is 1 through 6)\n", + home_dir); + } + } ++ slapi_ch_free((void **)&env); + return rc; + } + +@@ -6341,6 +6347,7 @@ bdb_back_ctrl(Slapi_Backend *be, int cmd, void *info) + db->close(db, 0); + rc = bdb_db_remove_ex((bdb_db_env *)priv->dblayer_env, path, NULL, PR_TRUE); + inst->inst_changelog = NULL; ++ slapi_ch_free_string(&path); + slapi_ch_free_string(&instancedir); + } + } +diff --git a/ldap/servers/slapd/back-ldbm/parents.c b/ldap/servers/slapd/back-ldbm/parents.c +index 31107591e..52c665ca4 100644 +--- a/ldap/servers/slapd/back-ldbm/parents.c ++++ b/ldap/servers/slapd/back-ldbm/parents.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -123,7 +123,7 @@ parent_update_on_childchange(modify_context *mc, int op, size_t *new_sub_count) + /* Now compute the new value */ + if ((PARENTUPDATE_ADD == op) || (PARENTUPDATE_RESURECT == op)) { + current_sub_count++; +- } else { ++ } else if (current_sub_count > 0) { + current_sub_count--; + } + +diff --git a/ldap/servers/slapd/ch_malloc.c b/ldap/servers/slapd/ch_malloc.c +index cbab1d170..27ed546a5 100644 +--- a/ldap/servers/slapd/ch_malloc.c ++++ b/ldap/servers/slapd/ch_malloc.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -254,7 +254,7 @@ slapi_ch_bvecdup(struct berval **v) + ++i; + newberval = (struct berval **)slapi_ch_malloc((i + 1) * sizeof(struct berval *)); + newberval[i] = NULL; +- while (i-- > 0) { ++ while (i > 0 && i-- > 0) { + newberval[i] = slapi_ch_bvdup(v[i]); + } + } +diff --git a/ldap/servers/slapd/control.c b/ldap/servers/slapd/control.c +index 7aeeba885..d661dc6e1 100644 +--- a/ldap/servers/slapd/control.c ++++ b/ldap/servers/slapd/control.c +@@ -174,7 +174,6 @@ create_sessiontracking_ctrl(const char *session_tracking_id, LDAPControl **sessi + char *undefined_sid = "undefined sid"; + const char *sid; + int rc = 0; +- int tag; + LDAPControl *ctrl = NULL; + + if (session_tracking_id) { +@@ -183,9 +182,7 @@ create_sessiontracking_ctrl(const char *session_tracking_id, LDAPControl **sessi + sid = undefined_sid; + } + ctrlber = ber_alloc(); +- tag = ber_printf( ctrlber, "{nnno}", sid, strlen(sid)); +- if (rc == LBER_ERROR) { +- tag = -1; ++ if ((rc = ber_printf( ctrlber, "{nnno}", sid, strlen(sid)) == LBER_ERROR)) { + goto done; + } + slapi_build_control(LDAP_CONTROL_X_SESSION_TRACKING, ctrlber, 0, &ctrl); +diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c +index b788054db..bec3e32f4 100644 +--- a/ldap/servers/slapd/dse.c ++++ b/ldap/servers/slapd/dse.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -637,7 +637,7 @@ dse_updateNumSubordinates(Slapi_Entry *entry, int op) + /* Now compute the new value */ + if (SLAPI_OPERATION_ADD == op) { + current_sub_count++; +- } else { ++ } else if (current_sub_count > 0) { + current_sub_count--; + } + { +diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c +index 91ba23047..a9a5f3b3f 100644 +--- a/ldap/servers/slapd/log.c ++++ b/ldap/servers/slapd/log.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005-2024 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * Copyright (C) 2010 Hewlett-Packard Development Company, L.P. + * All rights reserved. + * +@@ -201,6 +201,7 @@ compress_log_file(char *log_name, int32_t mode) + + if ((source = fopen(log_name, "r")) == NULL) { + /* Failed to open log file */ ++ /* coverity[leaked_storage] gzclose does close FD */ + gzclose(outfile); + return -1; + } +@@ -211,11 +212,13 @@ compress_log_file(char *log_name, int32_t mode) + if (bytes_written == 0) + { + fclose(source); ++ /* coverity[leaked_storage] gzclose does close FD */ + gzclose(outfile); + return -1; + } + bytes_read = fread(buf, 1, LOG_CHUNK, source); + } ++ /* coverity[leaked_storage] gzclose does close FD */ + gzclose(outfile); + fclose(source); + PR_Delete(log_name); /* remove the old uncompressed log */ +diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c +index 0a351d46a..9e5bce80b 100644 +--- a/ldap/servers/slapd/modify.c ++++ b/ldap/servers/slapd/modify.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2009 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * Copyright (C) 2009, 2010 Hewlett-Packard Development Company, L.P. + * All rights reserved. + * +@@ -498,7 +498,7 @@ slapi_modify_internal_set_pb_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, LDAPMod + * + * Any other errors encountered during the operation will be returned as-is. + */ +-int ++int + slapi_single_modify_internal_override(Slapi_PBlock *pb, const Slapi_DN *sdn, LDAPMod **mod, Slapi_ComponentId *plugin_id, int op_flags) + { + int rc = 0; +@@ -512,7 +512,7 @@ slapi_single_modify_internal_override(Slapi_PBlock *pb, const Slapi_DN *sdn, LDA + !pb ? "pb " : "", + !sdn ? "sdn " : "", + !mod ? "mod " : "", +- !mod[0] ? "mod[0] " : ""); ++ !mod || !mod[0] ? "mod[0] " : ""); + + return LDAP_PARAM_ERROR; + } +diff --git a/ldap/servers/slapd/passwd_extop.c b/ldap/servers/slapd/passwd_extop.c +index 69bb3494c..5f05cf74e 100644 +--- a/ldap/servers/slapd/passwd_extop.c ++++ b/ldap/servers/slapd/passwd_extop.c +@@ -1,5 +1,5 @@ + /** BEGIN COPYRIGHT BLOCK +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +diff --git a/ldap/servers/slapd/unbind.c b/ldap/servers/slapd/unbind.c +index fa8cd649f..c4e7a5efd 100644 +--- a/ldap/servers/slapd/unbind.c ++++ b/ldap/servers/slapd/unbind.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -112,8 +112,12 @@ do_unbind(Slapi_PBlock *pb) + /* pass the unbind to all backends */ + be_unbindall(pb_conn, operation); + +-free_and_return:; ++free_and_return: + +- /* close the connection to the client */ +- disconnect_server(pb_conn, operation->o_connid, operation->o_opid, SLAPD_DISCONNECT_UNBIND, 0); ++ /* close the connection to the client after refreshing the operation */ ++ slapi_pblock_get(pb, SLAPI_OPERATION, &operation); ++ disconnect_server(pb_conn, ++ operation ? operation->o_connid : -1, ++ operation ? operation->o_opid : -1, ++ SLAPD_DISCONNECT_UNBIND, 0); + } +-- +2.49.0 + diff --git a/0041-Issue-6929-Compilation-failure-with-rust-1.89-on-Fed.patch b/0041-Issue-6929-Compilation-failure-with-rust-1.89-on-Fed.patch new file mode 100644 index 0000000..76e45a7 --- /dev/null +++ b/0041-Issue-6929-Compilation-failure-with-rust-1.89-on-Fed.patch @@ -0,0 +1,35 @@ +From ea62e862c8ca7e036f7d1e23ec3a27bffbc39bdf Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 11 Aug 2025 13:19:13 +0200 +Subject: [PATCH] Issue 6929 - Compilation failure with rust-1.89 on Fedora ELN + +Bug Description: +The `ValueArrayRefIter` struct has a lifetime parameter `'a`. +But in the `iter` method the return type doesn't specify the lifetime parameter. + +Fix Description: +Make the lifetime explicit. + +Fixes: https://github.com/389ds/389-ds-base/issues/6929 + +Reviewed by: @droideck (Thanks!) +--- + src/slapi_r_plugin/src/value.rs | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/slapi_r_plugin/src/value.rs b/src/slapi_r_plugin/src/value.rs +index 2fd35c808..fec74ac25 100644 +--- a/src/slapi_r_plugin/src/value.rs ++++ b/src/slapi_r_plugin/src/value.rs +@@ -61,7 +61,7 @@ impl ValueArrayRef { + ValueArrayRef { raw_slapi_val } + } + +- pub fn iter(&self) -> ValueArrayRefIter { ++ pub fn iter(&self) -> ValueArrayRefIter<'_> { + ValueArrayRefIter { + idx: 0, + va_ref: &self, +-- +2.49.0 + diff --git a/389-ds-base.spec b/389-ds-base.spec index 36c99a2..ac0ef81 100644 --- a/389-ds-base.spec +++ b/389-ds-base.spec @@ -333,6 +333,11 @@ Patch: 0033-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patc Patch: 0034-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch Patch: 0035-Issue-6875-Fix-dsidm-tests.patch Patch: 0036-Issue-6519-Add-basic-dsidm-account-tests.patch +Patch: 0037-Issue-6940-dsconf-monitor-server-fails-with-ldapi-du.patch +Patch: 0038-Issue-6936-Make-user-subtree-policy-creation-idempot.patch +Patch: 0039-Issue-6919-numSubordinates-tombstoneNumSubordinates-.patch +Patch: 0040-Issue-6910-Fix-latest-coverity-issues.patch +Patch: 0041-Issue-6929-Compilation-failure-with-rust-1.89-on-Fed.patch %description 389 Directory Server is an LDAPv3 compliant server. The base package includes diff --git a/rpminspect.yaml b/rpminspect.yaml index 873d73f..af18612 100644 --- a/rpminspect.yaml +++ b/rpminspect.yaml @@ -1,4 +1,9 @@ --- +abidiff: + ignore: + - /usr/lib64/dirsrv/plugins/*.so + - /usr/lib64/dirsrv/libsds.so* + - /usr/lib64/dirsrv/libslapd.so* specname: match: suffix runpath: