kbldfltr.sys — modifier-key state-tracking feature, lock-scope refactor, and debug tracing
KB5073723
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | kbldfltr_unpatched.sys |
| Patched binary | kbldfltr_patched.sys |
| Overall similarity | 0.8977 |
| Matched functions | 28 |
| Changed functions | 8 |
| Identical functions | 20 |
| Added in patched | 2 (KeyboardFilter::IsBlockedKey, KeyboardFilter::FilterBlockedModifierKeys) |
| Removed in unpatched | 0 |
Verdict: The patch does not fix a memory-safety or attacker-reachable security vulnerability. It adds one functional feature — tracking a new per-filter word, m_blockedModifierKeyState (gFilter+0x42), so that a blocked modifier key can be force-released on its break event — together with two new helper functions (IsBlockedKey, FilterBlockedModifierKeys), a relocation of an already-existing WDF wait lock from KeyboardFilter::SwapBlockList into its caller KeyboardFilter::Set (so the lock also covers the new recompute), and pervasive DbgPrintEx tracing. Every bounds check and validation on attacker-controlled IOCTL input is byte-for-byte identical between the two builds, and the block-list swap/free sequence has the same synchronization discipline in both builds.
The three items examined below (block-list swap/free, IOCTL input-buffer retrieval, and modifier-state tracking) are each documented against both binaries and none of them is a delivered security fix.
2. Vulnerability Summary
Finding 1 — No security-relevant change: block-list swap/free in KeyboardFilter::Set
- Severity: None (informational)
- Original hypothesis: TOCTOU race / Use-After-Free (CWE-362 / CWE-416)
- Disposition: Not a vulnerability. The unpatched code is memory-safe.
- Affected function:
KeyboardFilter::Set(unpatched @0x1c0007574, patched @0x1c0007628) - Entry point: IOCTL
0xba404viaDeviceIoControlto the KbldFltr control device
What the code actually does (both builds):
KeyboardFilter::Set allocates a new block list, calls KeyboardFilter::SwapBlockList to install it into gFilter+0x48 and obtain the old pointer, and then frees the old pointer with ExFreePoolWithTag. The question is whether the old block list can be freed while another thread is still dereferencing it.
It cannot, and the reason is that access to the block list is serialized by the WDF wait lock at gFilter+0x8:
-
KeyboardFilter::Applyholds the lock across its entire block-list iteration. In the unpatched build,Apply(@0x1c0001008) callsWdfWaitLockAcquireat0x1c0001053(function entry) and does not callWdfWaitLockReleaseuntil0x1c00012c0(function exit). Every read of the block-list pointergFilter+0x48(at0x1c000110fand0x1c00011a0) and every dereference of it happens inside that lock hold.Applynever captures the pointer under the lock and then dereferences it after releasing. -
The swap is mutually exclusive with
Apply. In the unpatched build,SwapBlockList(@0x1c00013b8) itself acquires the same lock (WdfWaitLockAcquireat0x1c00013fd), performs the pointer swap (mov [rdi+48h], rbpat0x1c000145a), then releases it (WdfWaitLockReleaseat0x1c0001473) and returns the old pointer. BecauseApplyholds the lock for its whole iteration,SwapBlockListblocks untilApplyhas finished and released. Once the swap completes,gFilter+0x48points at the new list, so any subsequentApplycall reads the new pointer, never the old one. -
The free is outside the lock in BOTH builds — and that is safe. In the unpatched build
Setfrees the old pointer at0x1c0007608with no lock held. In the patched buildSetreleases the lock at0x1c0007734and then frees at0x1c0007747— i.e. the free is also outside the lock. Freeing outside the lock is safe in both builds because after the swap no other thread can obtain the old pointer (readers only ever loadgFilter+0x48under the lock, and it now holds the new pointer).
What actually changed: In the patched build the wait lock was moved out of SwapBlockList (which no longer takes it) and into Set, so a single lock hold now spans SwapBlockList + the new FilterBlockedModifierKeys recompute. This widening exists to keep the new modifier-state computation atomic with the swap. It does not change the synchronization of the swap or the free relative to Apply. There is no use-after-free in either build and no race was closed.
Finding 2 — No security-relevant change: input-buffer retrieval in KbfEvtIoDeviceControl
- Severity: None (informational)
- Original hypothesis: Missing/Incorrect WDF Request parameter (CWE-628)
- Disposition: Not a bug. The unpatched call passes the correct Request handle.
- Affected function:
KbfEvtIoDeviceControl(unpatched @0x1c00071d0, IOCTL0xba404path from0x1c0007222)
What the code actually does: In the WDF EvtIoDeviceControl callback the Request handle arrives in rdx (arg2). At function entry the unpatched build copies it into rdi (mov rdi, rdx at 0x1c00071f1) and never overwrites rdx along the 0xba404 path. At the WdfRequestRetrieveInputMemory call site (WdfFunctions+0x858, called at 0x1c000725e), rdx therefore still holds the original Request handle. The call is correct.
The patched build (0xba404 path from 0x1c0007282) inserts a DbgPrintEx at 0x1c0007290 ("Enter IOCTL_KBFILTR_SET_KEYBOARD…"). That call clobbers rdx, so the compiler reloads it with mov rdx, rdi at 0x1c00072d1 immediately before the retrieval call at 0x1c00072db. The added mov rdx, rdi compensates for the newly-inserted tracing call; it does not fix a pre-existing missing-argument bug. The unpatched path never needed the reload because nothing clobbered rdx there.
Note also that the WDF function at offset 0x858 is WdfRequestRetrieveInputMemory (confirmed by the patched build's own error string "WdfRequestRetrieveInputMemory failed %x"), and its result is a WDFMEMORY that is subsequently passed to WdfMemoryGetBuffer (WdfFunctions+0x610).
Finding 3 — No security-relevant change: modifier-key state tracking in KeyboardFilter::Apply / Set
- Severity: None (informational; behavioral feature)
- Vulnerability class: None — functional feature addition
- Affected functions:
KeyboardFilter::Apply,KeyboardFilter::Set, and the two new helpersIsBlockedKey/FilterBlockedModifierKeys
What actually changed: The patch introduces a new per-filter word m_blockedModifierKeyState at gFilter+0x42 (adjacent to the existing m_ModifierKeyState at gFilter+0x40). Its purpose is keyboard-filter policy, not memory safety:
Set(patched) callsFilterBlockedModifierKeys(@0x1c00015d4) after the swap and stores the result atgFilter+0x42(store at0x1c00076eb), under the wait lock, so the modifier state stays consistent with the newly installed block list.Apply(patched) readsgFilter+0x42at the top of each iteration and, when the current key is a blocked modifier whose bit is set, clears that bit and forces the break-flag bit on the emitted key event.- The two helpers are bounded:
FilterBlockedModifierKeysiterates the fixed 10-entry modifier table atgFilter+0x10(loop cap0x0Aat0x1c000160e);IsBlockedKey(@0x1c00013b0) iterates the block list up to the count atgFilter+0x38, exactly as the existingApplyloop does. No new attacker-controlled index or size is introduced.
This is a keyboard-lockdown policy refinement (correctly releasing a held blocked modifier). It changes filtering behavior but does not add or remove any security boundary and is not a memory-corruption or bypass primitive.
3. Pseudocode Diff
KeyboardFilter::Set — UNPATCHED vs PATCHED
// ===== UNPATCHED (@ 0x1c0007574) =====
long KeyboardFilter::Set(KeyboardFilter* this,
_KBF_KEYBOARDLAYOUT const* layout,
uint64_t inputBufferLength)
{
int64_t keyDataCount = *(int32_t*)(layout + 0x28);
_KBF_KEYDATA* newBuf;
if (keyDataCount) {
uint64_t numberOfBytes = keyDataCount * 6; // 32-bit count; product fits 64-bit
if (numberOfBytes + 0x2c > inputBufferLength)
return STATUS_BUFFER_TOO_SMALL; // 0xC0000023
newBuf = ExAllocatePoolWithTag(NonPagedPoolNx, numberOfBytes, 'Kblf');
if (!newBuf) return STATUS_INSUFFICIENT_RESOURCES;
memmove(newBuf, layout + 0x2c, numberOfBytes);
} else {
newBuf = nullptr;
}
// SwapBlockList acquires+releases the wait lock internally around the swap.
_KBF_KEYDATA* oldBuf = KeyboardFilter::SwapBlockList(this, layout, newBuf);
if (oldBuf)
ExFreePoolWithTag(oldBuf, 'Kblf'); // outside the lock; safe (no other thread holds oldBuf)
return STATUS_SUCCESS;
}
// ===== PATCHED (@ 0x1c0007628) =====
long KeyboardFilter::Set(/* same args */)
{
/* ... allocate newBuf, memmove: identical size check and copy ... */
WdfWaitLockAcquire(WdfDriverGlobals, this->m_lock); // lock moved out of SwapBlockList
_KBF_KEYDATA* oldBuf = KeyboardFilter::SwapBlockList(this, layout, newBuf); // no internal lock now
if (newBuf) // NEW feature bookkeeping
this->m_blockedModifierKeyState = // gFilter+0x42
KeyboardFilter::FilterBlockedModifierKeys(this, this->m_ModifierKeyState);
// DbgPrintEx("m_ModifierKeyState == ..., m_blockedM...")
WdfWaitLockRelease(WdfDriverGlobals, this->m_lock); // released BEFORE the free
if (oldBuf)
ExFreePoolWithTag(oldBuf, 'Kblf'); // still outside the lock, same as unpatched
return STATUS_SUCCESS;
}
The free is outside the lock in both builds. The lock scope was widened only to cover the newly-added FilterBlockedModifierKeys recompute.
KeyboardFilter::SwapBlockList — lock relocation
// UNPATCHED (@ 0x1c00013b8): takes the wait lock internally
// WdfWaitLockAcquire(...) @ 0x1c00013fd
// old = *(this+0x48); *(this+0x48) = newBuf; // swap
// WdfWaitLockRelease(...) @ 0x1c0001473
// return old;
// PATCHED (@ 0x1c0001518): NO internal lock; caller (Set) now holds it.
// old = *(this+0x48); *(this+0x48) = newBuf;
// return old;
// (Also: the "layout cleared" path zeroes a DWORD at this+0x40, clearing both
// m_ModifierKeyState and the new m_blockedModifierKeyState word.)
Net synchronization is preserved, not added or removed.
KeyboardFilter::Apply — modifier-state feature (patched only)
// PATCHED adds at loop top (feature, not a fix):
int16_t blockedModState = this->m_blockedModifierKeyState; // gFilter+0x42
if (blockedModState && KeyboardFilter::IsBlockedKey(this, makeCode, flags)
&& _bittest(&blockedModState, bit)) {
this->m_blockedModifierKeyState = blockedModState & ~(1 << bit); // clear on release
// force the break-flag bit on the emitted key event
}
KbfEvtIoDeviceControl — IOCTL 0xba404 input retrieval
// UNPATCHED (@ 0x1c00071d0): rdi = Request at entry (0x1c00071f1); rdx never clobbered
// -> rdx still holds Request at the WdfRequestRetrieveInputMemory call (0x1c000725e). Correct.
// PATCHED (@ 0x1c0007210): a DbgPrintEx (0x1c0007290) clobbers rdx, so:
// mov rdx, rdi ; 0x1c00072d1 -- reload Request before the call. Compensates for the trace call.
4. Assembly Analysis
KeyboardFilter::Set (UNPATCHED, @ 0x1c0007574)
00000001C0007574 mov [rsp+arg_0], rbx
00000001C0007583 push rdi
00000001C0007584 sub rsp, 20h
00000001C0007588 movsxd rax, dword ptr [rdx+28h] ; KeyDataCount from layout
00000001C000758C mov rsi, rdx ; save layout
00000001C000758F mov rbp, cs:?gFilter ; gFilter
00000001C0007596 test eax, eax
00000001C0007598 jnz short 0x1C000759E
00000001C000759A xor ebx, ebx ; newBuf = NULL (zero entries)
00000001C000759C jmp short 0x1C00075ED
00000001C000759E lea rdi, [rax+rax*2] ; KeyDataCount * 3
00000001C00075A2 add rdi, rdi ; * 6
00000001C00075A5 lea rax, [rdi+2Ch] ; 0x2c + KeyDataCount*6
00000001C00075A9 cmp rax, r8 ; vs InputBufferLength
00000001C00075AC jbe short 0x1C00075B5
00000001C00075AE mov eax, 0C0000023h ; STATUS_BUFFER_TOO_SMALL
00000001C00075B3 jmp short 0x1C0007616
00000001C00075B5 mov r8d, 666C624Bh ; PoolTag 'Kblf'
00000001C00075BB mov rdx, rdi
00000001C00075BE mov ecx, 200h ; NonPagedPoolNx
00000001C00075C3 call cs:__imp_ExAllocatePoolWithTag
00000001C00075CF mov rbx, rax ; newBuf
00000001C00075D2 test rax, rax
00000001C00075D5 jnz short 0x1C00075DE
00000001C00075D7 mov eax, 0C000009Ah ; STATUS_INSUFFICIENT_RESOURCES
00000001C00075DC jmp short 0x1C0007616
00000001C00075DE lea rdx, [rsi+2Ch] ; src = layout+0x2c
00000001C00075E2 mov r8, rdi
00000001C00075E5 mov rcx, rax
00000001C00075E8 call memmove
00000001C00075ED mov r8, rbx ; arg3 = newBuf
00000001C00075F0 mov rdx, rsi ; arg2 = layout
00000001C00075F3 mov rcx, rbp ; arg1 = gFilter
00000001C00075F6 call ?SwapBlockList ; takes+releases wait lock internally
00000001C00075FB test rax, rax ; rax = old block list ptr
00000001C00075FE jz short 0x1C0007614
00000001C0007600 mov edx, 666C624Bh ; 'Kblf'
00000001C0007605 mov rcx, rax ; old block list
00000001C0007608 call cs:__imp_ExFreePoolWithTag ; free outside the lock (safe, see Finding 1)
00000001C0007614 xor eax, eax ; STATUS_SUCCESS
00000001C0007616 mov rbx, [rsp+28h+arg_0]
00000001C000762A retn
KeyboardFilter::SwapBlockList (UNPATCHED, @ 0x1c00013b8) — internal lock present
00000001C00013DB mov rax, cs:WdfFunctions_01015
00000001C00013E5 mov rdx, [rcx+8] ; gFilter+0x8 = wait lock handle
00000001C00013EC mov rcx, cs:WdfDriverGlobals
00000001C00013F6 mov rax, [rax+9E0h] ; WdfWaitLockAcquire
00000001C00013FD call cs:__guard_dispatch_icall_fptr ; ACQUIRE
... ; (compare layout header, update state)
00000001C0001436 mov rbx, [rdi+48h] ; old block list ptr
00000001C000145A mov [rdi+48h], rbp ; install new block list ptr (the swap)
00000001C000145E mov rcx, cs:WdfFunctions_01015
00000001C0001465 mov rax, [rcx+9E8h] ; WdfWaitLockRelease
00000001C000146C mov rcx, cs:WdfDriverGlobals
00000001C0001473 call cs:__guard_dispatch_icall_fptr ; RELEASE
00000001C0001479 mov rax, rbx ; return old block list ptr
KeyboardFilter::Apply (UNPATCHED, @ 0x1c0001008) — lock spans the whole iteration
00000001C000102A mov rbx, cs:?gFilter
00000001C0001048 mov rdx, [rbx+8] ; wait lock handle
00000001C000104C mov rax, [rax+9E0h] ; WdfWaitLockAcquire
00000001C0001053 call cs:__guard_dispatch_icall_fptr ; ACQUIRE at function entry
...
00000001C000110F mov r11, [rbx+48h] ; load block list ptr (under lock)
00000001C00011A0 mov r11, [rbx+48h] ; load block list ptr (under lock)
... ; iterate/dereference r11 (still under lock)
00000001C00012A7 mov rax, cs:WdfFunctions_01015
00000001C00012AE mov rdx, [rbx+8]
00000001C00012B9 mov rax, [rax+9E8h] ; WdfWaitLockRelease
00000001C00012C0 call cs:__guard_dispatch_icall_fptr ; RELEASE at function exit
Because acquire (0x1c0001053) and release (0x1c00012c0) bracket the entire iteration, Apply and SwapBlockList cannot overlap on the block list.
KeyboardFilter::Set (PATCHED, @ 0x1c0007628) — lock relocated here; free still after release
00000001C00076A7 mov rax, cs:WdfFunctions_01015
00000001C00076AE mov rdx, [rbx+8] ; wait lock handle
00000001C00076B9 mov rax, [rax+9E0h] ; WdfWaitLockAcquire
00000001C00076C0 call cs:__guard_dispatch_icall_fptr ; ACQUIRE (moved into Set)
00000001C00076CF call ?SwapBlockList ; no internal lock now
00000001C00076D4 movzx edi, word ptr [rbx+40h] ; m_ModifierKeyState
00000001C00076D8 mov rbp, rax ; old block list ptr
00000001C00076DB test rsi, rsi ; newBuf != 0 ?
00000001C00076DE jz short 0x1C00076F1
00000001C00076E6 call ?FilterBlockedModifierKeys ; NEW recompute
00000001C00076EB mov [rbx+42h], ax ; store m_blockedModifierKeyState
... ; DbgPrintEx
00000001C000771B mov rax, cs:WdfFunctions_01015
00000001C0007722 mov rdx, [rbx+8]
00000001C000772D mov rax, [rax+9E8h] ; WdfWaitLockRelease
00000001C0007734 call cs:__guard_dispatch_icall_fptr ; RELEASE
00000001C000773A test rbp, rbp ; old block list ptr
00000001C000773D jz short 0x1C0007753
00000001C000773F mov edx, 666C624Bh
00000001C0007744 mov rcx, rbp
00000001C0007747 call cs:__imp_ExFreePoolWithTag ; FREE happens AFTER release (same as unpatched)
KbfEvtIoDeviceControl — IOCTL 0xba404 input retrieval
; UNPATCHED (@ 0x1c00071d0): rdx = Request throughout the 0xba404 path
00000001C00071F1 mov rdi, rdx ; rdi = Request (arg2)
... ; rdx never overwritten on this path
00000001C0007245 mov rax, cs:WdfFunctions_01015
00000001C000724C lea r8, [rbp+var_20] ; &WDFMEMORY out
00000001C0007250 mov rcx, cs:WdfDriverGlobals
00000001C0007257 mov rax, [rax+858h] ; WdfRequestRetrieveInputMemory
00000001C000725E call cs:__guard_dispatch_icall_fptr ; rdx still = Request
; PATCHED (@ 0x1c0007210): DbgPrintEx clobbers rdx, so it is reloaded
00000001C0007290 call cs:__imp_DbgPrintEx ; "Enter IOCTL_KBFILTR_SET_KEYBOARD..." (clobbers rdx)
...
00000001C00072C6 lea r8, [rbp+var_20]
00000001C00072CA mov rcx, cs:WdfDriverGlobals
00000001C00072D1 mov rdx, rdi ; reload Request (compensates for the trace call)
00000001C00072D4 mov rax, [rax+858h] ; WdfRequestRetrieveInputMemory
00000001C00072DB call cs:__guard_dispatch_icall_fptr
5. Trigger Conditions
There is no vulnerability to trigger. For completeness, the reachable surface is:
KeyboardFilter::Setis reached viaDeviceIoControl(handle, 0xba404, inputBuffer, ...)→KbfEvtIoDeviceControl→WdfRequestRetrieveInputMemory→WdfMemoryGetBuffer→Set(gFilter, buffer, inputBufferLength)(call at0x1c0007298unpatched).KbfEvtIoDeviceControlvalidates the0xba404input in the same way in both builds:InputBufferLength <= 0x179C,InputBufferLength >= 0x2c, andSetadditionally rejects0x2c + KeyDataCount*6 > InputBufferLengthwithSTATUS_BUFFER_TOO_SMALL.KeyDataCountis a 32-bit field, soKeyDataCount*6cannot overflow the 64-bit size computation, and the input is already capped at0x179C.KeyboardFilter::Applyruns from the keyboard read-completion path (KbfEvtReadCompletionRoutine) and from IOCTL0xb2408. It serializes all block-list access under thegFilter+0x8wait lock in both builds.
None of these paths yields a memory-safety primitive.
6. Exploit Primitive & Development Notes
No exploit primitive is present. The block-list swap and free are correctly serialized against the only reader (Apply) by the gFilter+0x8 wait lock in both builds, and the free occurs after the swap when no other thread can hold the old pointer. The input length is validated and the allocation arithmetic cannot overflow. The modifier-state feature operates only on the fixed 10-entry modifier table and the already-bounded block list.
Because there is no reachable memory-corruption or bypass primitive, there is nothing to develop into an exploit, and considerations such as pool grooming, KASLR defeat, or token replacement do not apply to this patch.
7. Debugger Playbook
The following breakpoints let a reviewer confirm the synchronization discipline described above. They are for behavioral confirmation, not for reproducing a crash (there is no crash to reproduce).
| # | Address (unpatched) | What it shows |
|---|---|---|
| 1 | 0x1c0001053 |
WdfWaitLockAcquire at the top of Apply — lock is held before any block-list read. |
| 2 | 0x1c00011a0 |
mov r11, [rbx+0x48] in Apply — block-list pointer loaded while the lock is held. |
| 3 | 0x1c00012c0 |
WdfWaitLockRelease at the bottom of Apply — lock released only after iteration completes. |
| 4 | 0x1c00013fd |
WdfWaitLockAcquire inside SwapBlockList — confirms the swap waits for Apply to release. |
| 5 | 0x1c000145a |
mov [rdi+0x48], rbp — the pointer swap itself, under the lock. |
| 6 | 0x1c0007608 |
ExFreePoolWithTag in Set — old pointer freed after the swap; by this point no thread can hold it. |
| 7 | 0x1c000725e |
WdfRequestRetrieveInputMemory in the 0xba404 path — inspect rdx; it already equals rdi (the Request handle). |
A reviewer stepping these will observe that Apply never dereferences the block-list pointer outside the lock, so freeing the old list after the swap does not produce a dangling access.
gFilter field notes (as evidenced by the code)
| Offset | Field |
|---|---|
+0x00 |
dword blocking state |
+0x08 |
WDFWAITLOCK (m_lock) |
+0x10 |
0x2c-byte modifier-key table (10 entries) |
+0x38 |
dword key-data count |
+0x40 |
word m_ModifierKeyState |
+0x42 |
word m_blockedModifierKeyState (written only in the patched build via FilterBlockedModifierKeys) |
+0x48 |
ptr block list (m_keyData) |
_KBF_KEYDATA is 6 bytes. _KBF_KEYBOARDLAYOUT input is a 0x2c-byte header followed by KeyDataCount * 6 bytes.
IOCTL codes: 0xb2408 SEND_INPUT, 0xb240c GET_KEYBOARD_LAYOUT, 0xb2410 GET_KEYBOARD_ID, 0xba404 SET_KEYBOARD_LAYOUT. WDF calls dispatch through __guard_dispatch_icall_fptr; WdfWaitLockAcquire = WdfFunctions[0x9e0/8], WdfWaitLockRelease = WdfFunctions[0x9e8/8], WdfRequestRetrieveInputMemory = WdfFunctions[0x858/8], WdfMemoryGetBuffer = WdfFunctions[0x610/8].
8. Changed Functions — Full Triage
| Function | Similarity | Change Type | Note |
|---|---|---|---|
KeyboardFilter::Set |
0.8708 | feature + refactor | Wait lock relocated here from SwapBlockList; adds the FilterBlockedModifierKeys recompute of gFilter+0x42; adds DbgPrintEx. Free still occurs after the lock release. No security fix. |
KeyboardFilter::SwapBlockList |
0.9242 | refactor | Internal wait lock removed (moved to caller Set); "layout cleared" path now zeroes both m_ModifierKeyState and the new m_blockedModifierKeyState; adds DbgPrintEx. Net synchronization unchanged. |
KeyboardFilter::Apply |
0.7004 | feature | Adds m_blockedModifierKeyState (gFilter+0x42) tracking via the new IsBlockedKey helper and forces the break-flag bit on released blocked modifiers. Bounds and locking unchanged. |
KbfEvtIoDeviceControl |
0.9678 | tracing | Adds DbgPrintEx at IOCTL entry/exit; the mov rdx, rdi in the 0xba404 path merely reloads the Request handle that the added trace call clobbered. Input validation identical to unpatched. |
KbfEvtDeviceAdd |
0.8148 | tracing | Adds DbgPrintEx on WdfDeviceCreate / WdfIoQueueCreate failure paths. Logic identical. |
KbfEvtIoRead |
0.9807 | tracing | Adds DbgPrintEx on WdfRequestSend failure. |
KbfEvtDriverCleanup |
0.9804 | tracing | Adds DbgPrintEx. |
DriverEntry |
0.9506 | tracing + feature init | Adds DbgPrintEx; zero-initializes the filter modifier state with a DWORD write (byte 0x40) so it also clears the new gFilter+0x42 word. |
All eight changes are debug tracing, the modifier-state feature, or the lock relocation that supports it. None is a security fix.
9. Added / Removed Functions
The patched build adds two standalone helper functions that do not exist in the unpatched build:
KeyboardFilter::IsBlockedKey(@0x1c00013b0) — scans the block list (bounded by the count atgFilter+0x38) for a key that matches and passesCheckModifiers.KeyboardFilter::FilterBlockedModifierKeys(@0x1c00015d4) — iterates the fixed 10-entry modifier table, callsIsBlockedKeyper set modifier bit, and builds them_blockedModifierKeyStatebitmask.
No functions were removed. Both new functions exist solely to support the modifier-state tracking feature and are bounded; neither introduces an attacker-controlled index or length.
10. Confidence & Caveats
Confidence: HIGH that the patch contains no security-relevant fix.
Rationale, all anchored to both binaries:
-
No use-after-free.
Applyacquires the wait lock at0x1c0001053and releases at0x1c00012c0, bracketing every block-list dereference;SwapBlockListacquires the same lock at0x1c00013fdbefore swapping. The swap therefore cannot run concurrently withApply's iteration, and the old pointer freed at0x1c0007608is unreachable by any other thread at that point. The patched build frees at0x1c0007747, after releasing the lock at0x1c0007734— i.e. the free is outside the lock in both builds, so the patch did not move the free under the lock. -
No missing-parameter bug. In the unpatched
0xba404path,rdxholds the Request handle continuously from entry (mov rdi, rdxat0x1c00071f1) to the retrieval call at0x1c000725e; nothing clobbers it. The patchedmov rdx, rdiat0x1c00072d1only restoresrdxafter the newly-addedDbgPrintExat0x1c0007290. -
Input validation is identical. The
0xba404handler applies the same0x2c <= InputBufferLength <= 0x179Cbounds in both builds, andSet's0x2c + KeyDataCount*6 > InputBufferLengthcheck is unchanged. The 32-bitKeyDataCountcannot overflow the 64-bit size arithmetic. -
The only behavioral change is a feature. The
m_blockedModifierKeyStatetracking (newgFilter+0x42word,IsBlockedKey,FilterBlockedModifierKeys, and theApplybreak-flag logic) is a keyboard-filter policy refinement over bounded, fixed-size data. It adds no security boundary and removes none.
Caveat: This analysis covers only the two supplied builds. The pervasive DbgPrintEx additions indicate the patched binary is a debug/traced build of the same source revision as the modifier-state feature; the presence of tracing is itself consistent with a servicing/feature build rather than a security update.