// Moderate (5)
CVE-2026-11791

Use-after-free in schema reload via attr_syntax_swap_ht()

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

Summary

A use-after-free vulnerability in the 389 Directory Server schema reload mechanism allows an administrator-triggered schema reload to crash the LDAP server when concurrent LDAP query traffic hits a narrow race window. The attr_syntax_swap_ht() function in attrsyntax.c frees all attribute syntax info nodes unconditionally via attr_syntax_free(), bypassing the refcount-based deferred deletion pattern used by attr_syntax_delete_no_lock() in the same file. Concurrent query threads that have already obtained a reference to an asyntaxinfo node and released the read lock hold dangling pointers to freed memory. When such a thread subsequently accesses the freed node or calls attr_syntax_return() on it, the result is a use-after-free or double-free that crashes the server with SIGSEGV.

The race window is narrow (nanoseconds to low microseconds in normal operation), and the schema reload side requires administrative privileges. The bug is real and GDB-reproducible, but difficult to trigger naturally. The primary risk is to server availability under concurrent load during schema reload operations.

Affected Versions

The attr_syntax_swap_ht() function has existed since the dynamic schema reload feature was introduced. The unconditional free has been present for the entire lifetime of this function. All versions of 389-ds-base with schema reload support are affected.

PackageVersion rangeStatusVerification
389-ds-base (upstream)All versions with schema reload through 3.2.0-dev (current main)Vulnerable (confirmed)Source review: attr_syntax_swap_ht() at attrsyntax.c:1639-1665attr_syntax_free() called unconditionally on all nodes, no refcount check. Confirmed at commit b019653.
389-ds-base (Fedora 42)3.1.4-6.fc42.x86_64Vulnerable (confirmed)GDB-controlled reproduction: UAF with MALLOC_PERTURB_=170 fill, double-free SIGSEGV
389-ds-base (RHEL 7)1.3.xVulnerable (code present, untested)attr_syntax_swap_ht() present in all 1.3.x. CVE-2025-14905 fix patched only schema.c, not attrsyntax.c
389-ds-base (RHEL 8)1.4.xVulnerable (code present, untested)CVE-2025-14905 fix patched only schema.c, not attrsyntax.c
389-ds-base (RHEL 9)2.0.x through 2.6.xVulnerable (code present, untested)CVE-2025-14905 fix patched only schema.c, not attrsyntax.c
389-ds-base (RHEL 10)3.xVulnerable (code present, untested)CVE-2025-14905 fix patched only schema.c, not attrsyntax.c
389-ds-base (CentOS Stream 10)3.1.xVulnerable (code present, untested)CentOS Stream tracks RHEL

Affected Products

ProductVersion389-ds-base versionStatus
389 Directory Server (upstream)All versions with schema reload through 3.2.0-devAll with schema reloadAffected
Red Hat Directory Server 1010.x1.3.xAffected
Red Hat Directory Server 1111.x1.4.xAffected
Red Hat Directory Server 1212.0-12.72.0.x-2.7.xAffected
Red Hat Directory Server 1313.0-13.13.xAffected
RHEL 7 (IdM/FreeIPA)7.0-7.91.3.xAffected
RHEL 8 (IdM/FreeIPA)8.0-8.101.4.xAffected
RHEL 9 (IdM/FreeIPA)9.0-9.62.0.x-2.6.xAffected
RHEL 10 (IdM/FreeIPA)10.0-10.13.xAffected
FedoraAll versions with 389-ds-base through 42All with schema reloadAffected
CentOS / CentOS Stream7, 8, 9, 10Matches RHELAffected

The attr_syntax_swap_ht() function’s unconditional free via attr_syntax_free() has never been modified since introduction. The refcount-safe pattern exists in attr_syntax_delete_no_lock() in the same file but was never applied to swap_ht. Any product shipping 389-ds-base with dynamic schema reload ships this bug. The CVE-2025-14905 fix (commit 2e424110) modified only schema_attr_enum_callback() in schema.c and did not touch attrsyntax.c.

Architecture Validation

The vulnerability is a pure logic bug (race condition with incorrect memory management) with no dependence on architecture.

ArchitectureTestedResult
x86_64YESConfirmed: GDB-controlled UAF reproduction with MALLOC_PERTURB_=170. Freed memory shows 0xAA fill at offset 16+. Double-free crash (SIGSEGV in attr_syntax_return_locking_optional).

The root cause is a race condition where freed memory is accessed via a dangling pointer. The race window, memory allocation behavior, and crash mechanics are identical across architectures. GDB-controlled reproduction (using breakpoints to widen the race window) will work on any architecture.

Root Cause

File: ldap/servers/slapd/attrsyntax.c Function: attr_syntax_swap_ht() (lines 1639-1665) Called from: schema.c:4954 (slapi_reload_schema_files()) with write lock held

The schema reload path in slapi_reload_schema_files() performs three steps under the write lock:

  1. attr_syntax_delete_all_for_schemareload(SLAPI_ATTR_FLAG_KEEP) (schema.c:4953) — iterates oid2asi and calls attr_syntax_delete_no_lock() on unflagged entries. Nodes with asi_refcnt > 0 are correctly marked for deferred deletion (asi_marked_for_delete = PR_TRUE) and remain in the global_at linked list.

  2. attr_syntax_swap_ht() (schema.c:4954) — walks the global_at linked list and calls attr_syntax_free() on every node, including those that step 1 correctly preserved because their refcount was > 0.

  3. attr_syntax_unlock_write() (schema.c:4955) — releases the write lock.

The bug is in step 2: attr_syntax_swap_ht() defeats the mark-and-sweep pattern that attr_syntax_delete_no_lock() correctly implements. The deferred deletion of refcounted nodes (step 1) is rendered useless because step 2 frees them unconditionally.

Contributing factor: refcount leak in slapi_attr_is_dn_syntax_type()

slapi_attr_is_dn_syntax_type() (lines 1186-1201) calls attr_syntax_get_by_name() (which increments asi_refcnt) but never calls attr_syntax_return(). This leaks the refcount, meaning asi_refcnt is permanently > 0 for any attribute processed through this path. This has two effects:

  1. More nodes retain asi_refcnt > 0 when attr_syntax_delete_all_for_schemareload runs, causing step 1 to mark them for deferred deletion rather than freeing them
  2. These marked-but-not-freed nodes remain in the global_at linked list for swap_ht to free unsafely in step 2

The refcount leak increases the probability of the UAF being triggered because more nodes have outstanding references when swap_ht frees them.

Proposed Fix

Replace the unconditional attr_syntax_free() in attr_syntax_swap_ht() with a mark-and-sweep pattern matching attr_syntax_delete_no_lock():

/* BEFORE (vulnerable): */
void
attr_syntax_swap_ht()
{
    struct asyntaxinfo *next;

    PL_HashTableDestroy(name2asi);
    PL_HashTableDestroy(oid2asi);

    while (global_at) {
        next = global_at->asi_next;
        attr_syntax_free(global_at);     /* frees unconditionally */
        global_at = next;
    }

    name2asi = name2asi_tmp;
    name2asi_tmp = NULL;
    oid2asi = oid2asi_tmp;
    oid2asi_tmp = NULL;
    global_at = global_at_tmp;
    global_at_tmp = NULL;
}

/* AFTER (fixed): */
void
attr_syntax_swap_ht()
{
    struct asyntaxinfo *next;

    PL_HashTableDestroy(name2asi);
    PL_HashTableDestroy(oid2asi);

    /* Mark old nodes for deletion; free only if refcount == 0 */
    while (global_at) {
        next = global_at->asi_next;
        global_at->asi_marked_for_delete = 1;
        if (slapi_atomic_load_64(&global_at->asi_refcnt, __ATOMIC_ACQUIRE) == 0) {
            attr_syntax_free(global_at);
        }
        /* else: node will be freed when last reference is returned
         * via attr_syntax_return() */
        global_at = next;
    }

    name2asi = name2asi_tmp;
    name2asi_tmp = NULL;
    oid2asi = oid2asi_tmp;
    oid2asi_tmp = NULL;
    global_at = global_at_tmp;
    global_at_tmp = NULL;
}

This also requires a change to attr_syntax_return_locking_optional() to check asi_marked_for_delete and call attr_syntax_free() when the last reference is returned:

/* In attr_syntax_return_locking_optional(), after decrementing refcnt: */
if (slapi_atomic_load_64(&a->asi_refcnt, __ATOMIC_ACQUIRE) == 0 &&
    a->asi_marked_for_delete) {
    attr_syntax_free(a);
}

Additionally, the slapi_attr_is_dn_syntax_type() refcount leak should be fixed:

/* BEFORE (leaks refcount): */
PRBool
slapi_attr_is_dn_syntax_type(const char *type)
{
    struct asyntaxinfo *asi = attr_syntax_get_by_name(type, 0);
    if (asi && (asi->asi_syntax == SYNID_DISTINGUISHED_NAME)) {
        return PR_TRUE;
    }
    return PR_FALSE;
}

/* AFTER (fixed): */
PRBool
slapi_attr_is_dn_syntax_type(const char *type)
{
    struct asyntaxinfo *asi = attr_syntax_get_by_name(type, 0);
    PRBool result = PR_FALSE;
    if (asi && (asi->asi_syntax == SYNID_DISTINGUISHED_NAME)) {
        result = PR_TRUE;
    }
    if (asi) {
        attr_syntax_return(asi);
    }
    return result;
}

Proof of Concept

PoC source code: CVE-2026-11791 on GitHub

Two PoC tools are provided in the poc/ directory:

Automated race trigger (poc/011-attrsyntax-uaf-schema-reload.py)

A multi-threaded Python script that generates concurrent LDAP search traffic while repeatedly triggering schema reloads to race against query threads. Each search forces the server to look up attribute syntax via attr_syntax_find(), creating the race window. Schema reloads are triggered by adding/deleting temporary attribute types in cn=schema,cn=config.

Usage:

python3 poc/011-attrsyntax-uaf-schema-reload.py --host HOST --port PORT \
    --bind-dn "cn=Directory Manager" --password SECRET \
    --iterations 500 --search-threads 8

This is a race condition PoC — it may require hundreds or thousands of iterations to trigger. Running the target under ASan or with MALLOC_CHECK_=3 increases detection probability.

TSan race detector (poc/011-tsan-race-test.c)

A C program that links against libslapd.so and directly exercises the race between attr_syntax_get_by_name (reader) and slapi_reload_schema_files (reloader) using TSan instrumentation.

Build:

gcc -fsanitize=thread -g -o tsan-011 poc/011-tsan-race-test.c \
    -L/usr/lib64/dirsrv -lslapd -lnspr4 -lpthread \
    -Wl,-rpath,/usr/lib64/dirsrv

GDB-Controlled Reproduction

The most reliable reproduction uses GDB breakpoints to deterministically control thread scheduling, widening the race window from nanoseconds to arbitrary duration.

System tested: Fedora 42, 389-ds-base-3.1.4-6.fc42.x86_64, glibc 2.41, MALLOC_PERTURB_=170

Prerequisites:

  • A 389 Directory Server instance
  • Administrative privileges (to trigger schema reload)
  • GDB (to control thread timing and widen the race window)
  • Concurrent LDAP query traffic

Steps:

  1. Start ns-slapd under GDB with MALLOC_PERTURB_=170:

    MALLOC_PERTURB_=170 gdb --args /usr/sbin/ns-slapd -D /etc/dirsrv/slapd-test
  2. Set breakpoints:

    break attr_syntax_swap_ht
    break attr_syntax_get_by_name_locking_optional
  3. Generate LDAP query traffic (separate terminal):

    while true; do
        ldapsearch -x -H ldap://localhost:389 -b "dc=test,dc=com" \
            "(objectclass=*)" cn sn 2>/dev/null
    done
  4. Trigger schema reload (separate terminal):

    dsconf -D "cn=Directory Manager" -w password ldap://localhost:389 \
        schema reload
  5. In GDB, when Thread A hits attr_syntax_get_by_name_locking_optional:

    • Let Thread A proceed past the read lock acquisition, hash lookup, and refcount increment
    • Let Thread A release the read lock
    • Hold Thread A before it accesses any asi fields
  6. Switch to Thread B (schema reload) and let it proceed through attr_syntax_swap_ht:

    • Thread B acquires write lock
    • Thread B calls attr_syntax_free() on all nodes in the global list
    • Thread B releases write lock
  7. Resume Thread A:

    • Thread A accesses asi->asi_name (or any field) — use-after-free
    • Memory at offset 16+ shows 0xAA fill (glibc MALLOC_PERTURB_ confirmation)
    • Thread A eventually calls attr_syntax_return_locking_optional(asi) — operates on freed memory
    • SIGSEGV — double-free crash

Race Window Diagram

Thread A (LDAP query)                Thread B (schema reload)
---------------------                --------------------------
AS_LOCK_READ(name2asi_lock)
  asi = PL_HashTableLookup(...)
  atomic_incr(asi->asi_refcnt)
AS_UNLOCK_READ(name2asi_lock)
                                     attr_syntax_write_lock()
                                       attr_syntax_delete_all_for_schemareload():
                                         // checks refcnt > 0, sets marked_for_delete
                                         // but nodes stay in global_at linked list
                                       attr_syntax_swap_ht():
                                         PL_HashTableDestroy(name2asi)
                                         PL_HashTableDestroy(oid2asi)
                                         while (global_at):
                                           attr_syntax_free(global_at)  <-- FREES node
                                           // ignores asi_refcnt > 0
                                           // ignores asi_marked_for_delete
                                     attr_syntax_unlock_write()

  // Thread A still holds asi*
  asi->asi_name                      <-- USE-AFTER-FREE (0xAA fill)
  ...
  attr_syntax_return(asi)
    atomic_decr(asi->asi_refcnt)     <-- writes to freed memory
    if (refcnt == 0 && marked_for_delete)
      attr_syntax_free(asi)          <-- DOUBLE-FREE -> SIGSEGV

Double-Free Consequence Chain

The double-free is the more severe consequence. When the reader thread (Thread A) eventually calls attr_syntax_return() on the freed asyntaxinfo node:

  1. attr_syntax_return_locking_optional() (lines 368-401) acquires the write lock
  2. Atomically decrements asi_refcnt — but asi_refcnt is in freed memory, so this writes to a freed (possibly reallocated) heap region
  3. If the decremented value appears to be 0 and asi_marked_for_delete appears true (both fields are in freed memory and may contain arbitrary values), it calls attr_syntax_free(asi) — a double-free
  4. The double-free corrupts glibc heap metadata (free list corruption)
  5. The next malloc() or free() call by any thread in the process triggers malloc_printerr or SIGSEGV

LD_PRELOAD Race Widening Reproduction

An LD_PRELOAD wrapper for attr_syntax_get_by_name was built that adds a 5ms usleep after the real function returns, widening the nanosecond race window to milliseconds. The wrapper uses a file-trigger (/tmp/ARM_RACE) to arm only after server startup. Tested on the production binary (389-ds-base-1.4.3.39, ns-slapd unmodified) with MALLOC_PERTURB_=170.

With 5 concurrent search threads + 100 schema reload tasks, searches showed numResponses: 0 during reloads (server briefly unresponsive during swap), confirming concurrent access to the schema data structures.

dmesg GPF Crash Evidence

During TSan stress testing on a TSan-instrumented build (389-ds-base 1.4.3.39, compiled with -fsanitize=thread -g -O1), dmesg captured:

traps: ns-slapd[1328710] general protection fault ip:7fe0875ac96a sp:7fe0533284e8 error:0 in libc-2.28.so

This is a general protection fault in glibc’s memory management — consistent with accessing freed memory (the UAF from attr_syntax_swap_ht). The crash occurred during concurrent schema reload tasks + LDAP searches.

Stress Test (Natural Race)

# 380 schema reload tasks with 12 concurrent LDAP query threads, 80 seconds
for i in $(seq 1 380); do
    dsconf -D "cn=Directory Manager" -w password ldap://localhost:389 \
        schema reload &
done
# 12 threads of continuous LDAP queries
for i in $(seq 1 12); do
    while true; do
        ldapsearch -x -H ldap://localhost:389 -b "dc=test,dc=com" \
            "(objectclass=*)" cn sn 2>/dev/null
    done &
done
wait

Result: No natural crash in 80 seconds. This documents the narrow window honestly — the race is real (GDB-proven) but the window between read lock release and write lock acquisition by the reload task is typically nanoseconds to low microseconds.

Exploitation Analysis

  • DoS: CONFIRMED — GPF crash (dmesg evidence) + GDB-controlled UAF + MALLOC_PERTURB_ evidence all demonstrate that freed memory is accessed and the server can crash.
  • Info leak: NOT POSSIBLE — strdup truncates at null bytes, and LDAP protocol returns structured responses; there is no path to leak raw heap contents to a remote client.
  • RCE: THEORETICAL — type confusion via reused chunk + crafted schema could allow controlled writes, but requires admin privileges + ASLR bypass. No PoC exists.

Impact

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

ComponentValueJustification
AV (Attack Vector)NetworkConcurrent LDAP clients (ports 389/636) are the victims. The admin triggers the reload, and normal LDAP traffic provides the racing threads.
AC (Attack Complexity)HighRace condition with a narrow window. The reader must have obtained a reference + released the read lock in the window before the schema reload acquires the write lock. Most callers use the asi pointer very briefly (check a flag, copy a string, call attr_syntax_return()). GDB is required to hit the race deterministically. Stress testing (380 reloads + 12 threads x 80s) did not trigger a natural crash.
PR (Privileges Required)HighSchema reload requires administrative privileges (dsconf schema reload, cn=schema,cn=config modification, or schema replication).
UI (User Interaction)NoneNo user interaction required. Normal concurrent LDAP traffic is sufficient for the victim side.
S (Scope)UnchangedImpact is limited to the LDAP server process (slapd/ns-slapd).
C (Confidentiality)NoneNo information disclosure observed. The freed memory is filled with MALLOC_PERTURB_ pattern, not attacker-readable data.
I (Integrity)LowIf the freed memory is reallocated before the stale reference is used, the thread may corrupt an unrelated heap allocation via write-through-dangling-pointer. This is a theoretical integrity impact — not demonstrated beyond heap metadata corruption.
A (Availability)HighServer crash from double-free (SIGSEGV confirmed via GDB). The crash is in the LDAP worker thread, which terminates the entire ns-slapd process.

Severity Rationale

The severity is appropriately calibrated at Moderate. The bug is real (GDB-proven UAF and double-free crash), but practical exploitation is limited by:

  • PR:H: The attacker must be an admin to trigger schema reload. An admin who can trigger schema reload already has near-total control of the directory server.
  • AC:H: The race window is extremely narrow. Stress testing did not produce a natural crash.
  • Self-DoS: The primary impact is DoS, which is already within the admin’s power. The “attack” is largely a self-DoS scenario under race conditions.
  • Victim impact: The impact on concurrent LDAP clients (who experience a server crash) is the factor that justifies Moderate rather than Low.

Blast Radius

The blast radius is limited. This finding requires administrative privileges to trigger the schema reload and depends on a narrow race window with concurrent LDAP traffic.

Scenario: An administrator performs a schema reload (routine maintenance operation) while the server is under normal LDAP query load. If the race window is hit, the server crashes. All connected clients lose their sessions. Services dependent on the LDAP directory (FreeIPA/IdM, Dogtag PKI, SSSD, Kerberos KDC) experience transient failures until the server restarts.

The primary risk is accidental triggering during routine schema maintenance in production environments with high concurrent query load, not deliberate exploitation.

Comparable CVEs

CVESoftwareBug classCVSSComparison
CVE-2025-14905389-ds-baseHeap buffer overflow in schema_attr_enum_callbackNot yet scoredDifferent bug class (heap overflow vs UAF), different function, different file (schema.c vs attrsyntax.c). Both affect schema handling.
CVE-2024-3657389-ds-baseDoS via unauthenticated LDAP packet7.5Lower privilege requirement (PR:N) but simpler impact (DoS only). CVE-2026-11791 has higher privilege requirement (PR:H) and narrower race window (AC:H).

Workaround

The following mitigations reduce exposure:

  1. Avoid schema reload during peak query load. Schedule schema reload operations during maintenance windows with reduced LDAP traffic. This minimizes the probability of concurrent readers holding asyntaxinfo references during the reload.

  2. Minimize schema reload frequency. Schema reloads are rare administrative operations. Avoid unnecessary reloads. In replication topologies, schema changes propagate automatically — manual reloads should be infrequent.

  3. Monitor for schema reload crashes. Enable error logging and monitor for unexpected ns-slapd restarts during or immediately after schema reload operations. If crashes correlate with schema reloads, this bug is a likely cause.

  4. Restrict schema modification access. Limit the number of accounts with write access to cn=schema,cn=config. Use LDAP ACIs to restrict schema modification to dedicated administrative accounts.

Exploitation in the Wild

No evidence of exploitation in the wild was found.

SourceQuery / MethodResult
NVD / CVE databaseSearch for attr_syntax_swap_ht, schema reload UAF, use-after-free in 389-ds-base attrsyntax.cNo matching CVE. CVE-2025-14905 is a different bug (heap overflow in schema_attr_enum_callback).
GitHub 389-ds-base issuesSearch for attr_syntax_swap_ht, attr_syntax_free, schema reload race, use-after-freeNo security reports for this specific race condition.
Red Hat BugzillaSearch for attr_syntax_swap_ht, schema reload crash, attrsyntax use-after-freeNo Bugzilla entry for this race condition.
Pagure 389-ds-base ticketsSearch for schema reload crash, heap corruptionTicket 48492 (“heap corruption at schema replication”) is related but describes a different code path (schema replication vs. schema reload) and has no CVE assigned.
oss-security mailing listSearch for 389-ds-base schema reload vulnerabilitiesNo matching reports.

Timeline

DateEvent
2026-04-14Discovered during 389-ds-base security assessment
2026-04-15Reported to vendor

References

  • Red Hat CVE page
  • NVD
  • CVE-2025-14905: 389-ds-base heap buffer overflow in schema_attr_enum_callback (schema.c). Different vulnerability, different function, different file. The fix for CVE-2025-14905 does not address attrsyntax.c.
  • Pagure Ticket 48492: “Heap corruption at schema replication” — related symptom (heap corruption during schema operations) but different code path and no CVE assigned.
  • Commit b019653: Current upstream HEAD — confirmed vulnerable.
  • Commit 2e424110: Fix for CVE-2025-14905 in schema.c. Confirms attrsyntax.c was not modified.
  • CWE-416: Use After Free
  • CWE-362: Concurrent Execution Using Shared Resource with Improper Synchronization

Credits

Discovered by Ian Murphy through source code review and static analysis during a security assessment of 389-ds-base. Manual verification via GDB confirmed the use-after-free (freed memory with MALLOC_PERTURB_ fill) and the double-free crash (SIGSEGV in attr_syntax_return_locking_optional). The slapi_attr_is_dn_syntax_type() refcount leak was identified during review as a contributing factor.