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.
| Package | Version range | Status | Verification |
|---|---|---|---|
| 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-1665 — attr_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_64 | Vulnerable (confirmed) | GDB-controlled reproduction: UAF with MALLOC_PERTURB_=170 fill, double-free SIGSEGV |
| 389-ds-base (RHEL 7) | 1.3.x | Vulnerable (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.x | Vulnerable (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.x | Vulnerable (code present, untested) | CVE-2025-14905 fix patched only schema.c, not attrsyntax.c |
| 389-ds-base (RHEL 10) | 3.x | Vulnerable (code present, untested) | CVE-2025-14905 fix patched only schema.c, not attrsyntax.c |
| 389-ds-base (CentOS Stream 10) | 3.1.x | Vulnerable (code present, untested) | CentOS Stream tracks RHEL |
Affected Products
| Product | Version | 389-ds-base version | Status |
|---|---|---|---|
| 389 Directory Server (upstream) | All versions with schema reload through 3.2.0-dev | All with schema reload | Affected |
| Red Hat Directory Server 10 | 10.x | 1.3.x | Affected |
| Red Hat Directory Server 11 | 11.x | 1.4.x | Affected |
| Red Hat Directory Server 12 | 12.0-12.7 | 2.0.x-2.7.x | Affected |
| Red Hat Directory Server 13 | 13.0-13.1 | 3.x | Affected |
| RHEL 7 (IdM/FreeIPA) | 7.0-7.9 | 1.3.x | Affected |
| RHEL 8 (IdM/FreeIPA) | 8.0-8.10 | 1.4.x | Affected |
| RHEL 9 (IdM/FreeIPA) | 9.0-9.6 | 2.0.x-2.6.x | Affected |
| RHEL 10 (IdM/FreeIPA) | 10.0-10.1 | 3.x | Affected |
| Fedora | All versions with 389-ds-base through 42 | All with schema reload | Affected |
| CentOS / CentOS Stream | 7, 8, 9, 10 | Matches RHEL | Affected |
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.
| Architecture | Tested | Result |
|---|---|---|
| x86_64 | YES | Confirmed: 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:
-
attr_syntax_delete_all_for_schemareload(SLAPI_ATTR_FLAG_KEEP)(schema.c:4953) — iteratesoid2asiand callsattr_syntax_delete_no_lock()on unflagged entries. Nodes withasi_refcnt > 0are correctly marked for deferred deletion (asi_marked_for_delete = PR_TRUE) and remain in theglobal_atlinked list. -
attr_syntax_swap_ht()(schema.c:4954) — walks theglobal_atlinked list and callsattr_syntax_free()on every node, including those that step 1 correctly preserved because their refcount was > 0. -
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:
- More nodes retain
asi_refcnt > 0whenattr_syntax_delete_all_for_schemareloadruns, causing step 1 to mark them for deferred deletion rather than freeing them - These marked-but-not-freed nodes remain in the
global_atlinked list forswap_htto 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:
-
Start ns-slapd under GDB with
MALLOC_PERTURB_=170:MALLOC_PERTURB_=170 gdb --args /usr/sbin/ns-slapd -D /etc/dirsrv/slapd-test -
Set breakpoints:
break attr_syntax_swap_ht break attr_syntax_get_by_name_locking_optional -
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 -
Trigger schema reload (separate terminal):
dsconf -D "cn=Directory Manager" -w password ldap://localhost:389 \ schema reload -
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
asifields
-
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
-
Resume Thread A:
- Thread A accesses
asi->asi_name(or any field) — use-after-free - Memory at offset 16+ shows
0xAAfill (glibcMALLOC_PERTURB_confirmation) - Thread A eventually calls
attr_syntax_return_locking_optional(asi)— operates on freed memory - SIGSEGV — double-free crash
- Thread A accesses
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:
attr_syntax_return_locking_optional()(lines 368-401) acquires the write lock- Atomically decrements
asi_refcnt— butasi_refcntis in freed memory, so this writes to a freed (possibly reallocated) heap region - If the decremented value appears to be 0 and
asi_marked_for_deleteappears true (both fields are in freed memory and may contain arbitrary values), it callsattr_syntax_free(asi)— a double-free - The double-free corrupts glibc heap metadata (free list corruption)
- The next
malloc()orfree()call by any thread in the process triggersmalloc_printerror 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 —
strduptruncates 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
| Component | Value | Justification |
|---|---|---|
| AV (Attack Vector) | Network | Concurrent LDAP clients (ports 389/636) are the victims. The admin triggers the reload, and normal LDAP traffic provides the racing threads. |
| AC (Attack Complexity) | High | Race 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) | High | Schema reload requires administrative privileges (dsconf schema reload, cn=schema,cn=config modification, or schema replication). |
| UI (User Interaction) | None | No user interaction required. Normal concurrent LDAP traffic is sufficient for the victim side. |
| S (Scope) | Unchanged | Impact is limited to the LDAP server process (slapd/ns-slapd). |
| C (Confidentiality) | None | No information disclosure observed. The freed memory is filled with MALLOC_PERTURB_ pattern, not attacker-readable data. |
| I (Integrity) | Low | If 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) | High | Server 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
| CVE | Software | Bug class | CVSS | Comparison |
|---|---|---|---|---|
| CVE-2025-14905 | 389-ds-base | Heap buffer overflow in schema_attr_enum_callback | Not yet scored | Different bug class (heap overflow vs UAF), different function, different file (schema.c vs attrsyntax.c). Both affect schema handling. |
| CVE-2024-3657 | 389-ds-base | DoS via unauthenticated LDAP packet | 7.5 | Lower 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:
-
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
asyntaxinforeferences during the reload. -
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.
-
Monitor for schema reload crashes. Enable error logging and monitor for unexpected
ns-slapdrestarts during or immediately after schema reload operations. If crashes correlate with schema reloads, this bug is a likely cause. -
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.
| Source | Query / Method | Result |
|---|---|---|
| NVD / CVE database | Search for attr_syntax_swap_ht, schema reload UAF, use-after-free in 389-ds-base attrsyntax.c | No matching CVE. CVE-2025-14905 is a different bug (heap overflow in schema_attr_enum_callback). |
| GitHub 389-ds-base issues | Search for attr_syntax_swap_ht, attr_syntax_free, schema reload race, use-after-free | No security reports for this specific race condition. |
| Red Hat Bugzilla | Search for attr_syntax_swap_ht, schema reload crash, attrsyntax use-after-free | No Bugzilla entry for this race condition. |
| Pagure 389-ds-base tickets | Search for schema reload crash, heap corruption | Ticket 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 list | Search for 389-ds-base schema reload vulnerabilities | No matching reports. |
Timeline
| Date | Event |
|---|---|
| 2026-04-14 | Discovered during 389-ds-base security assessment |
| 2026-04-15 | Reported 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 addressattrsyntax.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 inschema.c. Confirmsattrsyntax.cwas 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.