Root Cause Analysis of CVE-2026-69834: Windows ALPC Use-After-Free in AlpcpImpersonateMessage
An independent root cause analysis of CVE-2026-69834; a Use-After-Free in Windows ALPC's AlpcpImpersonateMessage caused by a missing reference on the _KALPC_SECURITY_DATA struct across the impersonation read window.
by Penguin

Introduction
CVE-2026-69834 is a Use-After-Free (UAF) vulnerability in the Windows Advanced Local Procedure Call (ALPC) subsystem, patched in Microsoft's September 2026 Patch Tuesday. Successful exploitation allows a low-privilege attacker to gain SYSTEM-level privileges by winning a race condition in the kernel's ALPC impersonation path.
CVE | CVE-2026-69834 |
CWE | CWE-416 (Use After Free) |
Severity | Important (CVSS 7.0) |
Attack Complexity | High |
Impact | Elevation of Privilege |
Exploited in Wild | No |
This article presents an independent root cause analysis of the vulnerability derived from binary diffing pre-patch (10.0.17763.9121 ) and post-patch (10.0.17763.9245 ) ntoskrnl.exe builds and live kernel debugging on an isolated VM.
Background — Windows ALPC
Advanced Local Procedure Call (ALPC) is a high-performance message-passing mechanism in Windows that enables efficient inter-process communication (IPC) between client and server processes. ALPC replaced the older Local Procedure Call (LPC) mechanism and provides optimized communication channels.
At its core, ALPC is a messaging protocol, one process acts as a server exposing a port, and another acts as a client connecting to it and exchanging messages.

One notable feature ALPC provides beyond simple message passing is client impersonation, which lets a server act on behalf of a client. When the server receives a message, it can call NtAlpcImpersonateClientOfPort with that message, and the kernel assigns an impersonation token representing the client to the server thread. Access checks that use the thread's token, such as opening a file or registry key, are then evaluated against the client's identity instead of the server's. How far the server can go with that identity is limited by the impersonation level the client allowed when it connected.
ALPC Overview
Server — Creating the Port
The server creates a named port using NtAlpcCreatePort, providing a port name and attributes.
This port becomes the endpoint clients connect to:
RtlInitUnicodeString(&usPortName, L"\\RPC Control\\BeaconBytes");
InitializeObjectAttributes(&objPort, &usPortName, 0, 0, 0);
RtlSecureZeroMemory(&serverPortAttr, sizeof(serverPortAttr));
serverPortAttr.MaxMessageLength = MAX_MSG_LEN;
ntRet = NtAlpcCreatePort(&hPort, &objPort, &serverPortAttr);Server — Waiting for a Client
After creating the port, the server calls NtAlpcSendWaitReceivePort with no outgoing message. This blocks the server thread until a client connects or sends a message. When a client arrives, the kernel wakes the server and fills in pmReceive with the incoming PORT_MESSAGE:
PORT_MESSAGE pmReceive;
NTSTATUS ntRet;
nLen = sizeof(pmReceive);
ntRet = NtAlpcSendWaitReceivePort(hPort, 0, NULL, NULL, &pmReceive, &nLen, NULL, NULL);PORT_MESSAGEStruct [CLICK TO OPEN]//0x28 bytes (sizeof) struct _PORT_MESSAGE { union { struct { SHORT DataLength; //0x0 SHORT TotalLength; //0x2 } s1; //0x0 ULONG Length; //0x0 } u1; //0x0 union { struct { SHORT Type; //0x4 SHORT DataInfoOffset; //0x6 } s2; //0x4 ULONG ZeroInit; //0x4 } u2; //0x4 union { struct _CLIENT_ID ClientId; //0x8 double DoNotUseThisField; //0x8 }; ULONG MessageId; //0x18 union { ULONGLONG ClientViewSize; //0x20 ULONG CallbackId; //0x20 }; }; //SOURCE: https://www.vergiliusproject.com/kernels/x64/windows-10/1809/_PORT_MESSAGE
pmReceive is the user-mode visible portion of the kernel's internal KALPC_MESSAGE structure.
nLen serves two roles:
On Input: The caller sets it to
sizeof(pmReceive),tells the kernel the maximum buffer size available. If the incoming message is larger thannLenon input, the kernel returnsSTATUS_BUFFER_TOO_SMALLand the message is not delivered.On output: Kernel overwrites it with the actual received message length, server uses this to know how many valid bytes are in
pmReceive.
When NtAlpcSendWaitReceivePort returns successfully, pmReceive is populated and ready.
Client — Connecting to the Port
The client connects using NtAlpcConnectPort, specifying the port name and a set of port attributes. This is where the client decides how far the server is allowed to impersonate it.
Two things in ALPC_PORT_ATTRIBUTES control this (how far the server is allowed to impersonate it) :
Flag 0x10000 — ALPC_PORTFLG_ALLOWIMPERSONATION:
Explicitly grants the server permission to impersonate this client at all. Without this flag set, any impersonation attempt by the server will be denied by the kernel.
SecurityQos — SECURITY_QUALITY_OF_SERVICE:
Defines the limits of that impersonation, specifically the ImpersonationLevel, which controls how far the server can go with the client's identity:
ALPC_PORT_ATTRIBUTES clientPortAttr;
RtlSecureZeroMemory(&clientPortAttr, sizeof(clientPortAttr));
clientPortAttr.Flags = 0x10000; // ALPC_PORTFLG_ALLOWIMPERSONATION
clientPortAttr.SecurityQos.Length = sizeof(SECURITY_QUALITY_OF_SERVICE);
clientPortAttr.SecurityQos.ImpersonationLevel = SecurityImpersonation;
clientPortAttr.SecurityQos.ContextTrackingMode = SECURITY_STATIC_TRACKING;
clientPortAttr.SecurityQos.EffectiveOnly = FALSE;
clientPortAttr.MaxMessageLength = MSG_LEN;
ntRet = NtAlpcConnectPort(
&hPort,
&usPortName, // port name to connect to
NULL, // ObjectAttributes — not needed
&clientPortAttr, // QoS and impersonation flags live here
0, NULL, NULL, NULL, NULL, NULL, NULL);Client - Sends a message
Connection alone is not enough, the client must also send a message with a security attribute attached. Interestingly, the client uses the same NtAlpcSendWaitReceivePort call the server uses for listening, the difference is that the client populates the send parameters (lpMem and pSendAttr) instead of the receive parameters.
Before calling NtAlpcSendWaitReceivePort, the client needs to set up a security attribute to attach to the outgoing message. Instead of calling NtAlpcCreateSecurityContext first, the client can pass ContextHandle = -2, a sentinel value the kernel recognises as an instruction to create the security context inline during the send, without a separate creation call.
Before setting up the security attribute, the client first needs to define the SECURITY_QUALITY_OF_SERVICE, this tells the kernel what impersonation level and tracking mode to use when creating the security context inline:
SECURITY_QUALITY_OF_SERVICE sqos;
RtlSecureZeroMemory(&sqos, sizeof(sqos));
sqos.Length = sizeof(sqos);
sqos.ImpersonationLevel = SecurityImpersonation;
sqos.ContextTrackingMode = SECURITY_STATIC_TRACKING;
sqos.EffectiveOnly = FALSE;SECURITY_QUALITY_OF_SERVICEStruct [CLICK TO OPEN]//0xc bytes (sizeof) struct _SECURITY_QUALITY_OF_SERVICE { ULONG Length; //0x0 enum _SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; //0x4 UCHAR ContextTrackingMode; //0x8 UCHAR EffectiveOnly; //0x9 }; //SOURCE: https://www.vergiliusproject.com/kernels/x64/windows-10/1809/_SECURITY_QUALITY_OF_SERVICE
ImpersonationLevel controls how far the server can go with the client's identity. SecurityImpersonation allows the server to impersonate locally. ContextTrackingMode = SECURITY_STATIC_TRACKING captures the client's security context at connection time and holds it fixed. EffectiveOnly = FALSE allows the server to see all of the client's privileges, not just the currently enabled subset.
With the QoS defined, the client then sets up the security attribute with ContextHandle = -2:
Find out how large the buffer needs to be:
SIZE_T attrBufSize = 0;
AlpcInitializeMessageAttribute(ALPC_MESSAGE_SECURITY_ATTRIBUTE,
NULL, 0, &attrBufSize);
// attrBufSize now holds the required allocation sizeInitialize the buffer:
PALPC_MESSAGE_ATTRIBUTES pSendAttr =
(PALPC_MESSAGE_ATTRIBUTES)HeapAlloc(
GetProcessHeap(), HEAP_ZERO_MEMORY, attrBufSize);
AlpcInitializeMessageAttribute(ALPC_MESSAGE_SECURITY_ATTRIBUTE,
pSendAttr, attrBufSize, &attrBufSize);Stamp the security context handle into the buffer's security slot
PALPC_SECURITY_ATTR pSecSlot =
(PALPC_SECURITY_ATTR)AlpcGetMessageAttribute(
pSendAttr, ALPC_MESSAGE_SECURITY_ATTRIBUTE);
pSecSlot->Flags = 0;
pSecSlot->QoS = &sqos;
pSecSlot->ContextHandle = (ALPC_HANDLE)-2; // ← sentinel stamped here
pSendAttr->ValidAttributes |= ALPC_MESSAGE_SECURITY_ATTRIBUTE;When this pSendAttr is passed to NtAlpcSendWaitReceivePort, the kernel sees ContextHandle = -2, creates the security context blob inline:
ntRet = NtAlpcSendWaitReceivePort(hPort, 0, (PPORT_MESSAGE)lpMem, pSendAttr, NULL, NULL, NULL, NULL);pSendAttr is an ALPC_MESSAGE_ATTRIBUTES buffer, a variable-length structure the kernel uses to carry message attributes on a send call. It is not a single flat struct but a container with a header followed by slots for each attribute type. AlpcInitializeMessageAttribute sets up that container for the ALPC_MESSAGE_SECURITY_ATTRIBUTE slot specifically.
pSecSlot is not a separate allocation, it is a typed pointer directly into pSendAttr's memory, returned by AlpcGetMessageAttribute at the offset where the security attribute slot lives inside the container. Writing to pSecSlot->ContextHandle is writing directly into pSendAttr's buffer at that offset. The QoS pointer is also stamped here, pSecSlot->QoS = &sqos tells the kernel which impersonation constraints to use when creating the security context inline via the -2 path.
Server — Impersonating the Client
After receiving the client's message, the server calls NtAlpcImpersonateClientOfPort to temporarily assume the client's identity:
ntRet = NtAlpcImpersonateClientOfPort(hConnectedPort, &pmReceive, 0);Internally NtAlpcImpersonateClientOfPort does two things in sequence:
Resolves the message:
AlpcpCaptureIdMessage(Message, local_res20, local_38);
// safely reads message ID from user-mode PORT_MESSAGE into kernel stack
iVar2 = AlpcpLookupMessage(local_30, local_res20[0], local_38[0], local_28);
// searches port's message queue for matching _KALPC_MESSAGE*
// writes _KALPC_MESSAGE* → local_28[0]The user-mode PORT_MESSAGE pointer is never used directly in the kernel. AlpcpCaptureIdMessage safely extracts the message ID from it into a kernel stack variable, and AlpcpLookupMessage uses that ID to find and return the corresponding _KALPC_MESSAGE* from the kernel's internal message table, this kernel object is what gets passed to AlpcpImpersonateMessage, not the original user-supplied PORT_MESSAGE.
KALPC_MESSAGEStruct [CLICK TO OPEN]//0x118 bytes (sizeof) struct _KALPC_MESSAGE { struct _LIST_ENTRY Entry; //0x0 struct _ALPC_PORT* PortQueue; //0x10 struct _ALPC_PORT* OwnerPort; //0x18 struct _ETHREAD* WaitingThread; //0x20 union { struct { ULONG QueueType:3; //0x28 ULONG QueuePortType:4; //0x28 ULONG Canceled:1; //0x28 ULONG Ready:1; //0x28 ULONG ReleaseMessage:1; //0x28 ULONG SharedQuota:1; //0x28 ULONG ReplyWaitReply:1; //0x28 ULONG OwnerPortReference:1; //0x28 ULONG ReceiverReference:1; //0x28 ULONG ViewAttributeRetrieved:1; //0x28 ULONG InDispatch:1; //0x28 } s1; //0x28 ULONG State; //0x28 } u1; //0x28 LONG SequenceNo; //0x2c union { struct _EPROCESS* QuotaProcess; //0x30 VOID* QuotaBlock; //0x30 }; struct _ALPC_PORT* CancelSequencePort; //0x38 struct _ALPC_PORT* CancelQueuePort; //0x40 LONG CancelSequenceNo; //0x48 struct _LIST_ENTRY CancelListEntry; //0x50 struct _KALPC_RESERVE* Reserve; //0x60 struct _KALPC_MESSAGE_ATTRIBUTES MessageAttributes; //0x68 VOID* DataUserVa; //0xb0 struct _ALPC_COMMUNICATION_INFO* CommunicationInfo; //0xb8 struct _ALPC_PORT* ConnectionPort; //0xc0 struct _ETHREAD* ServerThread; //0xc8 VOID* WakeReference; //0xd0 VOID* WakeReference2; //0xd8 VOID* ExtensionBuffer; //0xe0 ULONGLONG ExtensionBufferSize; //0xe8 struct _PORT_MESSAGE PortMessage; //0xf0 }; //SOURCE: https://www.vergiliusproject.com/kernels/x64/windows-10/1809/_KALPC_MESSAGE
Impersonates using the security context:
AlpcpImpersonateMessage(local_30,local_28[0],(uint)Flags & 1,
(uint)((Flags & ((ulonglong)(RequiredLevel * 4) | 2)) != 0),
RequiredLevel);This is where the vulnerability lives. AlpcpImpersonateMessage is the kernel-internal function that handles all ALPC impersonation, every call to NtAlpcImpersonateClientOfPort ultimately ends up here.
AlpcpImpersonateMessage — Vulnerable Function
AlpcpImpersonateMessage receives the _KALPC_PORT* and _KALPC_MESSAGE* resolved by the lookup and is responsible for reading the client's captured security context and attaching it to the server thread via PsImpersonateClient.
AlpcpImpersonateMessage Function Signature:
NTSTATUS AlpcpImpersonateMessage(
longlong Port, // _KALPC_PORT*
longlong Message, // _KALPC_MESSAGE*
int Flags,
int CheckRequiredLevel,
int RequiredLevel
)The Race Condition
Before explaining how the vulnerable path is reached, it is important to understand what the function does wrong, because the bug is visible in the structure of the code itself.
The function takes the pushlock on the _KALPC_SECURITY_DATA struct, the same security context the client attached to the message via NtAlpcSendWaitReceivePort with ContextHandle = -2, now sitting at Message+0x88 inside AlpcpImpersonateMessage as lVar3, checks and sets the flags inside it, then releases the pushlock before reading the security context.
Message+0x88 is not a direct field of _KALPC_MESSAGE, it is reached through two nested structs. _KALPC_MESSAGE contains a _KALPC_MESSAGE_ATTRIBUTES field at +0x68, and inside that struct SecurityData sits at +0x20, making the security context pointer reachable at +0x68 + 0x20 = +0x88:
_KALPC_MESSAGE:
+0x68 MessageAttributes (_KALPC_MESSAGE_ATTRIBUTES)
│
└── +0x20 SecurityData (_KALPC_SECURITY_DATA*)
│
└── this is lVar3 in the decompile
= Message+0x88
//0x70 bytes (sizeof)
struct _KALPC_SECURITY_DATA
{
struct _ALPC_HANDLE_TABLE* HandleTable; //0x0
VOID* ContextHandle; //0x8
struct _EPROCESS* OwningProcess; //0x10
struct _ALPC_PORT* OwnerPort; //0x18
struct _SECURITY_CLIENT_CONTEXT DynamicSecurity; //0x20
union
{
struct
{
ULONG Revoked:1; //0x68
ULONG Impersonated:1; //0x68
} s1; //0x68
} u1; //0x68
}; With this layout in mind, the vulnerable execution path becomes clear:
lVar3 = *(longlong *)(Message + 0x88);
// _KALPC_MESSAGE.MessageAttributes.SecurityData
// = _KALPC_SECURITY_DATA* created inline during send
ExAcquirePushLockExclusiveEx(lVar3 - 0x10, 0);
// ← pushlock TAKEN at blob-0x10
// serializes access to flags at blob+0x68
*(uint *)(lVar3 + 0x68) |= 2;
// set Impersonated flag (bit 1)
// still inside pushlock — flag write is safe
KeAbPostRelease(lVar3 - 0x10);
// ← pushlock RELEASED
// access lock gone
// blob-0x18 (refcount) NEVER TOUCHED
// nothing keeping blob alive from this point
// ── everything below has no protection on lVar3 ──────────────────
puVar7 = (undefined4 *)(lVar3 + 0x20);
// reads DynamicSecurity from blob+0x20
// pushlock already gone
// no reference held on blob-0x18
PsImpersonateClient(..., *(puVar7 + 4), ...);
// ← ClientToken read from lVar3+0x30
// puVar7+4 = DynamicSecurity+0x10 = blob+0x30
// no reference held on the blob
// no lock protecting the readAfter setting the Impersonated flag the pushlock on _KALPC_SECURITY_DATA is released and from that point forward the function holds absolutely nothing on the struct. Yet execution continues reading directly from it — puVar7 = lVar3+0x20 sets up a pointer into the same struct whose lock was just dropped, and PsImpersonateClient is called, taking in data from the same struct whose pushlock was just released.
How the Vulnerable Path is Reached
For the race window to be reachable, five conditions must all pass. Not all of them are equal in importance, Gate 4 is the critical one that determines whether the vulnerable path is reachable at all, and it is directly controlled by what the client did before the server called NtAlpcImpersonateClientOfPort.
Gate 4 — Security context must be present (the critical fork):
lVar3 = *(longlong *)(Message + 0x88);
// _KALPC_MESSAGE.MessageAttributes.SecurityData
// = _KALPC_SECURITY_DATA*
if (lVar3 == 0) → safe connectedPort path
reference held on connected port
not vulnerable
if (lVar3 != 0) → vulnerable blob path
no reference held
race window reachableThis value is non-NULL because the client sent the message with ContextHandle = -2 in pSendAttr —
the kernel's inline creation path in AlpcpCaptureSecurityAttributeInternal created the _KALPC_SECURITY_DATA blob and wrote its pointer into Message+0x88.
Gate 5 — Security context must not be revoked:
(*(uint *)(lVar3 + 0x68) & 1) == 0
// _KALPC_SECURITY_DATA.u1.s1.Revoked == 0The Revoked bit is a one-time-use guard. If set, the function releases the pushlock and returns STATUS_ACCESS_DENIED immediately, never reaching the race window. A freshly created _KALPC_SECURITY_DATA always has this bit clear, so this gate always passes on first impersonation. The vulnerable path is only reachable on the first use of a fresh security context.
Gates 1, 2, 3 — Message validation:
These three gates are standard checks that pass naturally in any normal ALPC request flow:
// Gate 1 — message must be REQUEST type:
(*(undefined4 *)(Message + 0x28) & 7) == 3
// _KALPC_MESSAGE.u1.s1.QueueType:3 == 3
// any normal client request message satisfies this
// Gate 2 — impersonation must not be blocked:
(*(ushort *)(Message + 0xf4) & 0x4000) == 0
// _KALPC_MESSAGE.PortMessage.Type & 0x4000
// kernel only sets this on specific internal message types
// Gate 3 — message must belong to this port:
*(longlong *)(Message + 0x10) == Port
// _KALPC_MESSAGE.PortQueue == Port
// naturally satisfied — AlpcpLookupMessage already
// validated port ownership before calling this functionWhen all five pass:
Execution falls through to the vulnerable code path. The pushlock is taken, the Impersonated flag is set, the pushlock is released, and from that point the function read _KALPC_SECURITY_DATA.DynamicSecurity and calls PsImpersonateClient with no reference held on the struct. The race window is open.
The Fix
Microsoft's fix for CVE-2026-69834 is contained entirely within AlpcpImpersonateMessage and adds three things, a feature gate check, a reference increment before the pushlock is released, and a matching reference decrement after PsImpersonateClient completes.
Comparing pre-patch and post-patch directly:
Pre-patch — goes straight from Gate 5 to setting the Impersonated flag:
// consumed bit check passes → falls through
*(uint *)(lVar2 + 0x68) = *(uint *)(lVar2 + 0x68) | 2; // set Impersonated
// ... pushlock released ...
KeAbPostRelease(lVar2 + -0x10);
puVar8 = (undefined4 *)(lVar2 + 0x20); // dangling — no reference held
PsImpersonateClient(...);
// no AlpcpDereferenceBlobExPost-patch — three additions before the same point:
// consumed bit check passes → falls through
// ── ADDITION 1: feature gate ────────────────────────────────────
iVar4 = EvaluateCurrentState(
&g_Feature_1081839931_62406798_FeatureDescriptorDetails);
if (iVar4 != 0) {
// ── ADDITION 2: pin the blob alive ──────────────────────────
lVar5 = AlpcpReferenceBlob(lVar2);
if (lVar5 == 0) goto LAB_1405c7632; // blob dying → bail safely
local_f8 = 1; // mark ref taken
}
*(uint *)(lVar2 + 0x68) = *(uint *)(lVar2 + 0x68) | 2; // set Impersonated
// ... pushlock released ...
KeAbPostRelease(lVar2 + -0x10);
puVar8 = (undefined4 *)(lVar2 + 0x20); // safe — blob pinned alive
PsImpersonateClient(...);
// ── ADDITION 3: release the reference ───────────────────────────
if (local_f8 != 0) {
AlpcpDereferenceBlobEx(lVar2, 1); // drop impersonation ref
}The three additions explained
Addition 1 — Feature gate (
EvaluateCurrentState):
EvaluateCurrentState(&g_Feature_1081839931_62406798_FeatureDescriptorDetails)This is Windows' feature flag system, it allows Microsoft to ship a fix and enable or disable it remotely without a new build. In the shipped KB5122876 build EvaluateCurrentState returns nonzero, meaning the fix is fully active. The gate also provides a safe rollback mechanism if the fix causes unexpected regressions.
Addition 2 —
AlpcpReferenceBlob(the actual fix):
lVar5 = AlpcpReferenceBlob(lVar2);
if (lVar5 == 0) goto LAB_1405c7632;
local_f8 = 1;This is the fix in its entirety, a single atomic increment of blob-0x18. Called while the pushlock is still held, before KeAbPostRelease, it increments the refcount stored at blob-0x18. From this point the blob cannot be freed during the window regardless of what concurrent operations fire.
The if (lVar5 == 0) check handles the edge case where the blob is already dying, AlpcpReferenceBlob returns 0 if it finds refcount already at 0, confirmed from its decompile:
longlong AlpcpReferenceBlob(longlong param_1) {
lVar1 = *(longlong *)(param_1 + -0x18); // read current refcount
do {
if (lVar1 < 1) {
if (lVar1 == 0) return 0; // already dying → fail
KeBugCheckEx(0x18, ...); // negative → corruption
}
LOCK();
// atomic compare-and-swap:
if (lVar1 == *(param_1-0x18))
*(param_1-0x18) = lVar1 + 1; // increment only if unchanged
UNLOCK();
} while (changed); // retry if concurrent change
return lVar2 + 1; // return new refcount
}When it returns 0 the code jumps to LAB_1405c7632, the safe early-return path that releases the pushlock and returns STATUS_ACCESS_DENIED.
3. Addition 3 — AlpcpDereferenceBlobEx (the paired release):
if (local_f8 != 0) {
AlpcpDereferenceBlobEx(lVar2, 1);
}Called after PsImpersonateClient completes, the read is done, the server thread has its impersonation token, the blob is no longer needed by the impersonation path. AlpcpDereferenceBlobEx atomically decrements blob-0x18 back down. The local_f8 guard ensures this only runs when Addition 2 actually took a reference.
Impact
An attacker who successfully exploits CVE-2026-69834 gains SYSTEM privileges on the affected machine. The attack requires only a low-privilege local account, no administrator rights, no user interaction, and no network access. The attacker connects to a privileged ALPC server that impersonates its clients, attaches a security context to an outgoing message, and races against the server's impersonation window.
Our analysis confirmed why Microsoft rated this AC:H (High attack complexity). Through exhaustive testing of the ALPC syscall surface, every message-capture reference drop path was found to either be blocked by the kernel's InDispatch mechanism during active impersonation, or to simultaneously zero Message+0x88 before the blob can be freed, making the race non-trivial to win via user-mode syscalls alone.
This is based on our own independent analysis of the available code paths and may not represent the complete picture, a more sophisticated technique or a kernel-internal path we did not examine could potentially achieve the timing required. The AC:H rating reflects that exploitation is possible but requires non-trivial effort, consistent with what our testing revealed.
Conclusion
Our analysis was conducted independently from the binary alone using ntoskrnl.exe builds 10.0.17763.9121 (pre-patch) and 10.0.17763.9245 (post-patch, KB5122876), binary diff to identify the changed function, Ghidra decompilation to trace the root cause, and live kernel debugging on an isolated VM to confirm the window and reference behavior. The vulnerability was discovered by Microsoft's own researchers - Amir Kutcher, Edan Zwick, and Tal Tzhori — and was not exploited in the wild. We hope this analysis contributes to the community.