windowsnetlogonactive-directorycldappatch-diffingreverse-engineeringAI-generated

CVE-2026-41089: Netlogon's WCHAR-count stack overflow

A Netlogon patch diff and live CLDAP trace show how a byte budget became a WCHAR count, overflowing a domain controller's stack response buffer.

Microsoft fixed CVE-2026-41089 in the May 2026 security updates. The advisory describes a stack-based buffer overflow in Windows Netlogon that can allow an unauthenticated remote attacker to execute code on a domain controller. Microsoft rates it Critical and assigns a CVSS 3.1 base score of 9.8.

The patch replaces a manual Unicode writer used by Netlogon response builders. The old writer receives what its callers treat as a byte budget, but decrements it once per WCHAR copied. A budget of 0x82 therefore permits 130 UTF-16 code units, or 260 bytes, before the unconditional two-byte terminator. The fixed writer uses RtlStringCbCopyExW, which interprets the same value as bytes, and subtracts any alignment padding with RtlULongSub before copying.

The public attack surface is a CLDAP Netlogon ping on UDP/389. I reproduced the vulnerable call chain and a deterministic stack-cookie failure on an affected Windows Server 2019 domain controller. The crash is configuration-dependent: attacker data fills the response, but long server-controlled DNS names provide the final bytes that cross the caller’s stack boundary.

The vulnerable writer, corrected size semantics, remote call chain, and stack-cookie corruption are proven. The observed result is an unauthenticated LSASS failure and domain-controller restart. Reliable code execution was not demonstrated; /GS detects the corruption before the overwritten return state can be used.

Advisory facts

Field Value
CVE CVE-2026-41089
Component Windows Netlogon, netlogon.dll
Impact Remote code execution
Weakness CWE-121, Stack-based Buffer Overflow
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
Microsoft severity Critical
Exploitation assessment Exploitation less likely
Publicly disclosed / exploited No / No at publication
Release May 12, 2026

Binary pair

The diff brackets the fix on the Windows Server 2019 build train:

Vulnerable Fixed
Version 10.0.17763.8385 10.0.17763.8755
Update Pre-May 2026 baseline KB5087538
Indexed SHA-256 cb14187f77c55aac0bfc8c9293937eafb2c3f744cf52e1817818808742780f7f 044760044666a552c2e378a461ffd76a797dbf523dcad1d86cb8b077429f43cc

The binaries downloaded from Microsoft’s symbol server have different file hashes because they are re-signed, but their PE timestamp, image size, machine type, version, and PDB identity match the indexed artifacts.

The security-relevant changes form one coherent cluster:

  • NetpLogonPutUnicodeString becomes a status-returning, byte-bounded writer;
  • the old manual implementation remains as NetpLogonPutUnicodeStringOld for the runtime rollback path;
  • BuildSamLogonResponse, NetpDcBuildPing, and PrimaryQueryHandler select the hardened writer when EvaluateCurrentState() is enabled;
  • hardened callers propagate ERROR_INVALID_PARAMETER instead of continuing to serialize a response after a failed copy.

Prerequisites

The remotely reproduced CLDAP path requires the following conditions:

  1. The target is an affected Windows domain controller. Microsoft’s FAQ limits the attack to a Windows server acting as a domain controller. A member server that merely exposes SMB is not equivalent to the tested Netlogon/CLDAP surface.
  2. CLDAP on UDP/389 is reachable. The proven route is an LDAP RootDSE Netlogon query, not the initially suspected anonymous SMB mailslot route. It does not require credentials or an established secure channel.
  3. The query selects the legacy SAM Logon response. The reproduced request used NtVer = 0x02, an accepted DnsDomain, and a 130-character User value. The user string reaches the old writer with a nominal 0x82 budget.
  4. The server’s own names make the serialized response large enough. Short domain and host names reached the vulnerable writer but returned safely. The crash appeared only after the domain controller used a much longer DNS suffix. The query cannot substitute an arbitrary suffix: values that do not match the target’s domain state are rejected.

The exact reproduced configuration was:

Setting Reproduced value
Hostname SILVERHAND12345
NetBIOS domain ARASAKA
Server DNS suffix arasaka-nightcity-corporation-cyberpunk2077-secnet.blackwall-counterintel-research-division.enterprise-security-operations.secure-network-architecture.corp
DNS suffix length 155 characters
Accepted query DnsDomain arasaka-nightcity-corporation-cyberpunk2077-secnet.corp
Query User 130 UTF-16 characters
Query NtVer 0x00000002
Transport CLDAP, UDP/389

This is a known-working laboratory configuration, not a universal minimum. DNS compression, hostname length, NetBIOS names, and the selected response format all change the final layout. There is no registry value required for the reproduced path. The essential configuration prerequisite is enough legitimate server-side name data to carry the already overlong response past the fixed stack buffer.

Two other paths reached legacy writer code but did not reproduce the stack overflow:

  • a UDP/138 Primary Query with NtVersion = 1 executed two old-writer calls, but their sources were the server hostname and NetBIOS domain rather than attacker-controlled text;
  • DsrGetDcNameEx2 with flags 0x1080 passed an attacker-controlled account to NetpDcBuildPing, but that builder used a sufficiently large heap allocation and remained in bounds in the measured request.

Corrected root cause

The first static report focused on alignment padding not being deducted from the remaining budget. The diff does add that check, but dynamic analysis and the preserved legacy loop expose the larger defect: the old function interprets its second argument as a count of UTF-16 elements, while its callers supply byte-sized limits.

BuildSamLogonResponse invokes the writer three times in the vulnerable response:

Field Budget Control
Server/computer name 0x24 Server-controlled
User/account 0x82 Attacker-controlled
Domain name 0x20 Server-controlled

The attacker-controlled 0x82 field is the main response inflator. Instead of limiting the copy to 130 bytes, the legacy loop allows 130 WCHAR values and then writes a terminator. Subsequent server and DNS fields continue from the advanced cursor and can cross the fixed stack response buffer.

Exact vulnerable code

The fixed binary preserves the vulnerable implementation as NetpLogonPutUnicodeStringOld at 0x180035ae8. This Ghidra excerpt is the exact legacy algorithm used by the vulnerable response builders; names and types are decompiler recovery artifacts:

void NetpLogonPutUnicodeStringOld(
    wchar_t *source,
    uint budget,
    ulonglong *cursor)
{
    wchar_t value;
    wchar_t *input;
    longlong delta;
    int remaining;
    ulonglong count;
    wchar_t *aligned;

    count = (ulonglong)budget;
    input = L"";
    if (source != NULL)
        input = source;

    NetpULongPtrRoundUp(*cursor, 2, (ulonglong *)&aligned);
    if (aligned != (wchar_t *)*cursor)
        *(byte *)*cursor = 0;

    value = *input;
    if (value != L'\0') {
        delta = (longlong)input - (longlong)aligned;
        do {
            remaining = (int)count;
            count = (ulonglong)(remaining - 1);
            if (remaining == 0)
                break;
            *aligned = value;
            aligned = aligned + 1;
            value = *(wchar_t *)(delta + (longlong)aligned);
        } while (value != L'\0');
    }

    *aligned = L'\0';
    *cursor = (ulonglong)(aligned + 1);
}

There are three relevant failures in this implementation:

  1. budget is decremented per wchar_t, doubling a caller-supplied byte budget.
  2. A one-byte alignment pad is written but never deducted from the budget.
  3. The UTF-16 terminator is written unconditionally, even after the budget reaches zero.

The local guard-page model confirms the boundary error independently: with an odd cursor and a two-byte budget, alignment consumes one byte, leaving too little room for even a UTF-16 terminator. The legacy model crosses the reserved boundary; the fixed model returns 0x57 before copying.

Exact fix

The fixed NetpLogonPutUnicodeString at 0x18002116c treats the limit as bytes, charges the alignment pad to that limit with safe arithmetic, and returns a status to its caller:

undefined8 NetpLogonPutUnicodeString(
    wchar_t *source,
    uint budget,
    ulonglong *cursor)
{
    short *old_cursor;
    uint status;
    wchar_t *input;
    ulonglong remaining;
    uint adjusted_budget[4];
    longlong written;
    short *aligned[2];

    written = 0;
    remaining = (ulonglong)budget;
    if (1 < budget) {
        input = L"";
        if (source != NULL)
            input = source;

        adjusted_budget[0] = budget;
        NetpULongPtrRoundUp(*cursor, 2, (ulonglong *)aligned);
        old_cursor = (short *)*cursor;
        if (aligned[0] != old_cursor) {
            *(byte *)old_cursor = 0;
            status = RtlULongSub(
                (uint)remaining,
                (int)aligned[0] - (int)old_cursor,
                (int *)adjusted_budget);
            if ((int)status < 0)
                return 0x57;
            remaining = (ulonglong)adjusted_budget[0];
        }

        status = RtlStringCbCopyExW(
            aligned[0],
            remaining & 0xffffffff,
            input,
            &written);
        if (-1 < (int)status) {
            *cursor = written + 2;
            return 0;
        }
    }
    return 0x57;
}

The two decisive operations are:

RtlULongSub(budget, aligned - old_cursor, &adjusted_budget);
RtlStringCbCopyExW(aligned, adjusted_budget, source, &written);

RtlULongSub prevents the padding adjustment from wrapping below zero. RtlStringCbCopyExW receives a byte count, so a 0x82 budget now permits at most 130 bytes including the terminator, not 130 UTF-16 characters plus a terminator.

The fixed builders also fail closed. When the hardened writer returns non-zero, they abort response construction rather than advancing the cursor and appending more data.

Remote call chain

The dynamically confirmed unauthenticated route is:

CLDAP UDP/389
  -> ntdsai!LDAP_CONN::SearchRequest
  -> ntdsai!LDAP_GetRootDSEAttNetlogon
  -> netlogon!I_NetLogonLdapLookupEx
  -> netlogon!NlGetLocalPingResponse
  -> netlogon!LogonRequestHandler
  -> netlogon!BuildSamLogonResponse
  -> netlogon!NetpLogonPutUnicodeStringOld

Kernel debugging confirmed the three writer calls and their arguments. The second call received the attacker-controlled CLDAP account with edx = 0x82. The response then continued with server-controlled domain and DNS data.

Confirmed corruption

On the long-domain target, the reproduced response had this stack-relative layout:

Offset from response blob Observation
+0x200 First corrupted caller-cookie/saved-area bytes
+0x210 Caller /GS cookie slot in the benign layout
+0x240 Netlogon trailer and compression data
+0x250 End of the observed response
+0x258 Caller saved return address

The response overwrote the caller cookie and saved-register area but stopped eight bytes before the saved return address. The bytes at the cookie came from constrained DNS/domain serialization and fixed trailer data. They could not reproduce the random cookie value, and the caller checked /GS before consuming the saved return state.

The resulting STATUS_STACK_BUFFER_OVERRUN / fail-fast behavior terminated LSASS and caused the domain controller to restart. This is a strong vulnerability and DoS proof, but it is not a demonstrated RCE primitive. Extending the domain alone would still have to bypass or repair the cookie before control flow could reach an overwritten return address.

Conclusion

CVE-2026-41089 is a size-unit error in Netlogon’s legacy Unicode response writer. A nominal byte limit is consumed as a count of WCHAR elements, and neither alignment padding nor the final terminator is fully represented in the cursor budget. A CLDAP request can use the oversized account field to fill a fixed stack response buffer; long server-side DNS names then push serialization across the caller’s stack cookie.

The May 2026 update replaces the manual loop with a byte-bounded copy, safely subtracts alignment padding, and propagates copy failures through the response builders. The live lab confirms unauthenticated remote stack corruption and a reliable crash on a long-name domain controller. The patch should be applied regardless of domain naming, because short-name systems still contain the vulnerable writer even when their current response layout does not reach the stack boundary.