1. Overview

Item Value
Unpatched binary hidbth_unpatched.sys
Patched binary hidbth_patched.sys
Overall similarity 0.8933
Matched / Changed / Identical 160 / 1 / 159
Unmatched (unpatched → patched / patched → unpatched) 0 / 0

Verdict: The patch removes a single conditional override of the return status on the error path of HidBthSendIrp (sub_1C0005AA8). When queuing a reconnect work item fails and the IRP has meanwhile been claimed by the I/O manager's cancel path, the unpatched code overrode the genuine error with STATUS_PENDING and returned it. Because the caller frees its per-request allocations only on a negative return, that override caused the read-report data buffer, the associated report object, and the pending-request count to be leaked in that narrow race. The patched build propagates the real error so the caller releases them. This is a minor resource-management correctness fix; it is not a memory-corruption bug. There is no demonstrable use-after-free or double-free.


2. Vulnerability Summary

Field Value
Severity Low
Vulnerability class Resource leak on a race-and-exhaustion-gated error path (CWE-401 missing release of memory; CWE-772 missing release of resource for the pending-request count)
Affected function HidBthSendIrp (sub_1C0005AA8)
Caller HidBthReadReport (sub_1C0005F70)
IOCTL dispatch entry HidBthIoctl (sub_1C0005670)
IOCTL involved in the decision 0xB000B (IOCTL_HID_READ_REPORT)

Root cause

HidBthSendIrp (sub_1C0005AA8) takes a device-extension pointer, a pending-IRP list/state structure, and an IRP. When the connection is in the forwarding state ([rdi+0x4]==2) it sends the IRP straight down the stack via IofCallDriver. Otherwise it adds the IRP to a pending-IRP list (for cancellation tracking) via IrpList_EnqueueLocked (sub_1C000AEC4), which also arms the IRP's cancel routine, releases its spinlocks, and — depending on the connection flags and the IOCTL code — may call HidBthConnectToDevice (sub_1C0003AEC) to queue a reconnect work item.

HidBthConnectToDevice (sub_1C0003AEC) acquires a remove-lock, calls IoAllocateWorkItem, and queues HidBthReconnectWorkRoutine via IoQueueWorkItem. If IoAllocateWorkItem returns NULL under pool pressure, it releases the remove-lock and returns STATUS_INSUFFICIENT_RESOURCES (0xC000009A); it also returns a negative status if the remove-lock cannot be acquired (device removing).

On that failure path, HidBthSendIrp tries to roll back by removing the IRP from the pending list via HidBthDequeueIrp (sub_1C0002018). HidBthDequeueIrp returns a boolean in al: TRUE (1) if it removed the IRP and cleared its cancel routine before the I/O manager did (so the driver now owns the IRP), and FALSE (0) if the IRP was not reclaimable — either it was not in the list, or its cancel routine had already been selected by the I/O manager (the cancel path owns the IRP and will complete it).

The unpatched code, when HidBthDequeueIrp returns FALSE, overrides the error status with STATUS_PENDING (0x103) using a cmovz. The patched code drops the override and returns the genuine error unconditionally.

Why the override leaks resources

The direct caller HidBthReadReport (sub_1C0005F70) allocates two things per read request before calling HidBthSendIrp:

  • P — the read-report data buffer, ExAllocatePoolWithTag(NonPagedPoolNx, reportLength+1, 'HidB') (tag 0x42646948), at 0x1c0006013.
  • reportObj — a report/transfer object allocated through the object vtable at [rdi+0xE68] (0x1c000603f); P is stored inside it at reportObj+0x88.

After HidBthSendIrp returns, HidBthReadReport releases these two allocations and decrements the pending-request count only when the returned status is negative:

  • 0x1c00060ed jns — if status >= 0, skip HidBthDecrementPendingRequestCount and instead clear the out-flag byte (mov byte [r15], 0 at 0x1c00060f9), telling HidBthIoctl not to complete the IRP.
  • 0x1c00060ff jns — if status >= 0, skip the report-object free ([rdi+0xE70] via __guard_dispatch_icall_fptr at 0x1c0006101) and ExFreePool(P) at 0x1c0006114.

STATUS_PENDING is non-negative, so when the override fires all three releases are skipped. In the reclaim-failure race, nothing else frees P/reportObj for this request: the report completion routine HidBthReadCompletion (sub_1C0005DA0) — which is the normal releaser of P, reportObj, and the count — only runs when the IRP is actually sent down the stack, which does not happen on the pending/reconnect path. The IRP itself is completed exactly once, by the cancel path, so STATUS_PENDING correctly stops the caller from completing it a second time — but the two pool allocations and the pending-request count are leaked. The patch makes the function return the real error, so HidBthReadReport runs the release path.

Impact and reachability

  • The demonstrable consequence is a resource leak: one 'HidB' pool buffer, one report object, and one unit of the pending-request counter per triggering event. Sustained triggering is a potential local denial of service through non-paged-pool exhaustion.
  • There is no use-after-free and no double-free. In the unpatched path the IRP is completed once (by the cancel path); the caller neither completes it nor frees the request allocations, so no memory is freed twice or accessed after free as a result of this override.
  • Reaching the override requires three conditions to coincide: the non-forwarding reconnect branch, a HidBthConnectToDevice failure (work-item allocation failure or device removing), and a precisely-timed cancel of the same internal HID read IRP so that HidBthDequeueIrp returns FALSE. These are internal HID minidriver IRPs (IOCTL_HID_READ_REPORT) issued to hidbth.sys by the HID class driver; an attacker influences them only indirectly (issuing/cancelling HID reads on a paired Bluetooth HID device) and cannot deterministically control the resource-exhaustion window. Severity is therefore Low.

Data flow

  1. The HID class driver dispatches an IOCTL_HID_READ_REPORT (0xB000B) to hidbth.sys.
  2. HidBthIoctl (sub_1C0005670) routes it to HidBthReadReport (sub_1C0005F70).
  3. HidBthReadReport allocates P ('HidB') and reportObj, installs HidBthReadCompletion in the next-lower stack location, bumps the pending-request count, then calls HidBthSendIrp.
  4. HidBthSendIrp (sub_1C0005AA8):
  5. Adds the IRP to the pending list (IrpList_EnqueueLocked (sub_1C000AEC4), which returns STATUS_PENDING and arms the cancel routine).
  6. Releases the spinlocks.
  7. On the reconnect branch ([rsi+0x5a6]==0, and for 0xB000B the lock cmpxchg [rsi+0x5f4] succeeds), calls HidBthConnectToDevice (sub_1C0003AEC).
  8. HidBthConnectToDevice fails (IoAllocateWorkItem → NULL, or remove-lock unavailable) → returns negative status.
  9. HidBthDequeueIrp (sub_1C0002018) cannot reclaim the IRP (a concurrent cancel already claimed it) → returns FALSE.
  10. Unpatched: cmovz ebp, 0x103 overrides the failure with STATUS_PENDING.
  11. HidBthReadReport sees status >= 0, clears the out-flag (mov byte [r15], 0 at 0x1c00060f9), skips the release path at 0x1c00060ff, and returns — leaking P, reportObj, and the pending-request count.

3. Pseudocode Diff

Vulnerable (unpatched) — HidBthSendIrp (sub_1C0005AA8), error path

// ... after IrpList_EnqueueLocked added the IRP to the pending list and armed its cancel routine ...
// ... after spinlocks were released ...

status = sub_1C0003AEC(ctx);            // HidBthConnectToDevice: queue reconnect work item
if (status < 0)                         // jns 0x1c0005c3f not taken
{
    removed = sub_1C0002018(list, irp); // HidBthDequeueIrp: try to reclaim the IRP
    ebp = status;                       // the real error
    if (!removed)                       // cancel path already owns the IRP
        ebp = STATUS_PENDING;           // 0x103  ← override
}
return ebp;

Fixed (patched) — same path

    status = sub_1C0003AEC(ctx);        // HidBthConnectToDevice
    ebp = status;                       // keep the real status
    if (status < 0)
        sub_1C0002018(list, irp);       // HidBthDequeueIrp: best-effort reclaim; result ignored
    return status;                      // ALWAYS propagate the real error

The patch is the deletion of the test al,al / mov eax,0x103 / cmovz ebp,eax triplet, replaced by a direct jump to the epilogue. In the patched build the error status is carried in ebp directly (mov ebp, eax at 0x1c0005bcd) rather than through ebx.


4. Assembly Analysis

Vulnerable function — full unpatched disassembly (HidBthSendIrp (sub_1C0005AA8))

00000001C0005AA8  mov     [rsp+arg_0], rbx
00000001C0005AAD  mov     [rsp+arg_8], rbp
00000001C0005AB2  mov     [rsp+arg_10], rsi
00000001C0005AB7  push    rdi
00000001C0005AB8  push    r12
00000001C0005ABA  push    r13
00000001C0005ABC  push    r14
00000001C0005ABE  push    r15
00000001C0005AC0  sub     rsp, 30h
00000001C0005AC4  lea     r15, [rdx+8]                 ; r15 = &lock (list/state struct + 8)
00000001C0005AC8  mov     rsi, rcx                     ; rsi = device-extension / connection state (ctx)
00000001C0005ACB  mov     rcx, r15
00000001C0005ACE  mov     r14, r8                      ; r14 = IRP
00000001C0005AD1  mov     rdi, rdx                     ; rdi = list/state struct
00000001C0005AD4  mov     ebp, 103h                    ; default status = STATUS_PENDING
00000001C0005AD9  call    cs:__imp_KeAcquireSpinLockRaiseToDpc
00000001C0005AE0  nop     dword ptr [rax+rax+00h]
00000001C0005AE5  cmp     dword ptr [rdi+4], 2         ; state == 2? (forward path)
00000001C0005AE9  mov     r13b, al                     ; save IRQL
00000001C0005AEC  jnz     short loc_1C0005B1F          ; not state 2 → pend/reconnect path
00000001C0005AEE  mov     dl, al
00000001C0005AF0  mov     rcx, r15
00000001C0005AF3  call    cs:__imp_KeReleaseSpinLock
00000001C0005AFA  nop     dword ptr [rax+rax+00h]
00000001C0005AFF  mov     rax, [rsi+50h]               ; devExt
00000001C0005B03  mov     rdx, r14                     ; IRP
00000001C0005B06  mov     rcx, [rax+40h]
00000001C0005B0A  mov     rcx, [rcx+8]                 ; DeviceObject
00000001C0005B0E  call    cs:__imp_IofCallDriver       ; send IRP down
00000001C0005B15  nop     dword ptr [rax+rax+00h]
00000001C0005B1A  jmp     loc_1C0005C3F                ; → return

; --- pend/reconnect path: state != 2 ---
00000001C0005B1F  mov     rax, [r14+0B8h]              ; IRP CurrentStackLocation
00000001C0005B26  mov     r12d, [rax+18h]              ; r12d = IOCTL code (IoControlCode)
00000001C0005B2A  mov     rcx, cs:WPP_GLOBAL_Control
00000001C0005B31  lea     rax, WPP_6409fc53710f37f5585292f3662006bc_Traceguids
00000001C0005B38  mov     r9d, 18h
00000001C0005B3E  mov     [rsp+58h+var_30], r14
00000001C0005B43  mov     [rsp+58h+var_38], rax
00000001C0005B48  mov     rcx, [rcx+40h]
00000001C0005B4C  lea     r8d, [r9-14h]
00000001C0005B50  call    WPP_RECORDER_SF_q            ; WPP trace event
00000001C0005B55  mov     rcx, [rdi+28h]               ; pending-list spinlock
00000001C0005B59  call    cs:__imp_KeAcquireSpinLockRaiseToDpc
00000001C0005B60  nop     dword ptr [rax+rax+00h]
00000001C0005B65  mov     rdx, r14                     ; IRP
00000001C0005B68  lea     rcx, [rdi+10h]               ; pending-list head
00000001C0005B6C  mov     r9b, al
00000001C0005B6F  call    IrpList_EnqueueLocked        ; add IRP to pending list, arm cancel routine
00000001C0005B74  mov     rcx, [rdi+28h]
00000001C0005B78  mov     dl, r9b
00000001C0005B7B  mov     ebp, eax                     ; ebp = status (STATUS_PENDING)
00000001C0005B7D  call    cs:__imp_KeReleaseSpinLock
00000001C0005B84  nop     dword ptr [rax+rax+00h]
00000001C0005B89  mov     dl, r13b
00000001C0005B8C  mov     rcx, r15
00000001C0005B8F  call    cs:__imp_KeReleaseSpinLock
00000001C0005B96  nop     dword ptr [rax+rax+00h]
00000001C0005B9B  xor     r15d, r15d
00000001C0005B9E  test    ebp, ebp
00000001C0005BA0  js      loc_1C0005C3F                ; if enqueue failed, bail

; --- decide whether to reconnect ---
00000001C0005BA6  cmp     [rsi+5A6h], r15b             ; ctx[0x5a6] == 0 ?
00000001C0005BAD  jnz     short loc_1C0005BEC          ; non-zero → alternate path
00000001C0005BAF  cmp     r12d, 0B000Bh                ; IOCTL_HID_READ_REPORT ?
00000001C0005BB6  jnz     short loc_1C0005BC5
00000001C0005BB8  xor     eax, eax
00000001C0005BBA  lock cmpxchg [rsi+5F4h], r15d        ; try to set ctx[0x5f4] = 0
00000001C0005BC3  jnz     short loc_1C0005C3F          ; already 0 → done

00000001C0005BC5  mov     rcx, rsi                     ; ctx
00000001C0005BC8  call    HidBthConnectToDevice        ; queue reconnect work item
00000001C0005BCD  mov     ebx, eax                     ; ebx = status from reconnect
00000001C0005BCF  test    eax, eax
00000001C0005BD1  jns     short loc_1C0005C3F          ; success → return prior ebp

; --- ERROR PATH: reconnect work-item queue failed ---
00000001C0005BD3  mov     rdx, r14                     ; arg2 = IRP
00000001C0005BD6  mov     rcx, rdi                     ; arg1 = list/state struct
00000001C0005BD9  call    HidBthDequeueIrp             ; try to reclaim IRP from list
00000001C0005BDE  test    al, al                       ; al=1 reclaimed / al=0 cancel path owns it
00000001C0005BE0  mov     ebp, ebx                     ; ebp = real error status
00000001C0005BE2  mov     eax, 103h                    ; eax = STATUS_PENDING
00000001C0005BE7  cmovz   ebp, eax                     ; al==0 → ebp := 0x103  (override)
00000001C0005BEA  jmp     short loc_1C0005C3F          ; → return

; --- alternate path: ctx[0x5a6] != 0 ---
00000001C0005BEC  cmp     r12d, 0B000Bh
00000001C0005BF3  jz      short loc_1C0005C3F
00000001C0005BF5  cmp     [rsi+5A7h], r15b
00000001C0005BFC  jz      short loc_1C0005C08
00000001C0005BFE  mov     rcx, rsi
00000001C0005C01  call    HidBthConnectToDevice
00000001C0005C06  jmp     short loc_1C0005C3F

; --- presence-notify path ---
00000001C0005C08  lea     rcx, [rsi+8]
00000001C0005C0C  call    cs:__imp_KeAcquireSpinLockRaiseToDpc
00000001C0005C13  nop     dword ptr [rax+rax+00h]
00000001C0005C18  mov     ebx, [rsi+5FCh]
00000001C0005C1E  lea     rcx, [rsi+8]
00000001C0005C22  mov     dl, al
00000001C0005C24  call    cs:__imp_KeReleaseSpinLock
00000001C0005C2B  nop     dword ptr [rax+rax+00h]
00000001C0005C30  test    ebx, ebx
00000001C0005C32  jnz     short loc_1C0005C3F
00000001C0005C34  mov     rcx, [rsi+50h]               ; DeviceObject
00000001C0005C38  xor     edx, edx
00000001C0005C3A  call    HidBthNotifyPresence

; --- epilogue ---
00000001C0005C3F  mov     rbx, [rsp+58h+arg_0]
00000001C0005C44  mov     eax, ebp                     ; eax = return status  ← caller reads this
00000001C0005C46  mov     rbp, [rsp+58h+arg_8]
00000001C0005C4B  mov     rsi, [rsp+58h+arg_10]
00000001C0005C50  add     rsp, 30h
00000001C0005C54  pop     r15
00000001C0005C56  pop     r14
00000001C0005C58  pop     r13
00000001C0005C5A  pop     r12
00000001C0005C5C  pop     rdi
00000001C0005C5D  retn

Side-by-side error path

; ─────── UNPATCHED ───────                        ; ─────── PATCHED ───────
00000001C0005BCD  mov ebx, eax                     ; 00000001C0005BCD  mov ebp, eax
00000001C0005BCF  test eax, eax                    ; 00000001C0005BCF  test eax, eax
00000001C0005BD1  jns  loc_1C0005C3F               ; 00000001C0005BD1  jns  loc_1C0005C33
00000001C0005BD3  mov rdx, r14                     ; 00000001C0005BD3  mov rdx, r14
00000001C0005BD6  mov rcx, rdi                     ; 00000001C0005BD6  mov rcx, rdi
00000001C0005BD9  call HidBthDequeueIrp            ; 00000001C0005BD9  call HidBthDequeueIrp
00000001C0005BDE  test al, al          ; override  ; 00000001C0005BDE  jmp  loc_1C0005C33   ; just return
00000001C0005BE0  mov  ebp, ebx
00000001C0005BE2  mov  eax, 103h
00000001C0005BE7  cmovz ebp, eax        ; override
00000001C0005BEA  jmp  loc_1C0005C3F

The delta is the removal of the test/mov/cmovz sequence. In the patched build ebp already holds the genuine error from HidBthConnectToDevice (sub_1C0003AEC) (via mov ebp, eax at 0x1c0005bcd), and the reconnect-failure path collapses to a direct jmp to the epilogue.

Supporting evidence — HidBthDequeueIrp (sub_1C0002018) ownership handshake

HidBthDequeueIrp is identical in both builds. Its return value is the cancel-ownership arbiter:

00000001C00020CA  mov     rax, rbx                     ; rax = 0
00000001C00020CD  dec     dword ptr [rdi+20h]          ; list count--
00000001C00020D0  xchg    rax, [rdx+68h]               ; clear IRP.CancelRoutine, read old value
00000001C00020D4  test    rax, rax
00000001C00020D7  jz      short loc_1C00020E4          ; old==NULL → cancel path already owns it
00000001C00020D9  mov     [rdx+90h], rbx
00000001C00020E0  mov     bl, 1                        ; old!=NULL → we reclaimed it → return TRUE
00000001C00020E2  jmp     short loc_1C000208F
00000001C00020E4  mov     [rcx+8], rcx                 ; reinit entry; bl stays 0 → return FALSE

TRUE (al=1) means the driver cleared the cancel routine first and now owns the IRP; FALSE (al=0) means the IRP was not in the list, or the I/O manager already cleared the cancel routine and the cancel path (HidBthIrpCancelRoutine (sub_1C0001FC0)IrpList_HandleCancel (sub_1C000B034)) owns and will complete it. For this list the completion callback is NULL (IrpList_InitEx(..., HidBthIrpCancelRoutine, 0)), so IrpList_HandleCancel completes the IRP itself with STATUS_CANCELLED via IofCompleteRequest. This is why the unpatched STATUS_PENDING return prevents a second completion — and why the leak, not corruption, is the actual consequence.


5. Trigger Conditions

To fire the override, all of the following must coincide:

  1. Reach the pend/reconnect path. An IOCTL_HID_READ_REPORT (0xB000B) request must be dispatched through HidBthIoctl (sub_1C0005670)HidBthReadReport (sub_1C0005F70)HidBthSendIrp (sub_1C0005AA8) with the connection not in the forwarding state ([rdi+0x4] != 2), so the IRP is enqueued rather than sent straight down. The IOCTL code read at 0x1c0005b26 gates the 0xB000B cmpxchg branch that leads into the reconnect call.

  2. State matches the reconnect branch. At 0x1c0005ba6, ctx[0x5a6] == 0; for 0xB000B the code executes lock cmpxchg [rsi+0x5f4], 0 (0x1c0005bba) and falls through to HidBthConnectToDevice (sub_1C0003AEC) only if the cmpxchg succeeds.

  3. HidBthConnectToDevice (sub_1C0003AEC) must FAIL. It fails when IoAllocateWorkItem returns NULL (returning STATUS_INSUFFICIENT_RESOURCES, 0xC000009A) or when IoAcquireRemoveLockEx fails (device removing).

  4. The IRP must not be reclaimable by HidBthDequeueIrp (sub_1C0002018). This is the race. The IRP was enqueued and its cancel routine armed at 0x1c0005b6f. Between that and the HidBthDequeueIrp call at 0x1c0005bd9, the I/O manager must have selected the IRP's cancel routine (via a CancelIoEx on the HID read), so HidBthDequeueIrp returns FALSE.

Practical amplifiers: sustained HID read requests that are immediately cancelled, combined with non-paged-pool pressure to make IoAllocateWorkItem fail. Because these are internal HID minidriver IRPs and the failure requires resource exhaustion, an attacker cannot deterministically hit the window.

Observable effect. With pool tracking (e.g. !poolused / !poolfind HidB), repeated triggering shows a growing count of unfreed 'HidB' allocations and a monotonically rising pending-request counter (ctx+0x30). There is no crash from this path itself; the failure mode is gradual non-paged-pool exhaustion.


6. Impact

Type Description
Primary Leak of the 'HidB' read-report data buffer P (tag 0x42646948, NonPagedPoolNx, size = report length + 1) and the associated report object, per triggering event.
Secondary Leak of one unit of the pending-request counter (ctx+0x30); the count is never decremented for the leaked request.
Aggregate Potential local denial of service through non-paged-pool exhaustion under sustained triggering.

There is no memory-corruption primitive. The IRP is completed exactly once by the cancel path; the unpatched STATUS_PENDING return specifically prevents the caller from completing or freeing it a second time. No use-after-free, double-free, or double-completion is reachable through this override, so it yields no control-flow-hijack or privilege-escalation primitive.


7. Debugger Notes

For a KD/WinDbg session attached to hidbth_unpatched.sys (substitute the loaded base for hidbth; the offsets below are RVAs relative to base 0x1C0000000).

Key breakpoints

bp hidbth+0x5AA8   ; HidBthSendIrp prologue. rcx=ctx, rdx=list/state, r8=IRP.
bp hidbth+0x5B26   ; after 'mov r12d,[rax+0x18]' — r12d = IoControlCode (expect 0xB000B).
bp hidbth+0x5BC5   ; reconnect call site. rsi=ctx; verify byte [rsi+0x5a6]==0.
bp hidbth+0x5BCF   ; 'test eax,eax' on HidBthConnectToDevice's return.
                   ;   eax<0 (e.g. 0xC000009A) → the reconnect-failure path is entered.
bp hidbth+0x5BDE   ; 'test al,al' after HidBthDequeueIrp.
                   ;   al==0 → override fires: cmovz sets ebp=0x103 at +0x5BE7.
bp hidbth+0x60E9   ; in HidBthReadReport, just after the call to HidBthSendIrp.
                   ;   eax=returned NTSTATUS; if 0x103 with a failed reconnect ⇒ override took effect.
bp hidbth+0x60FF   ; 'jns' guarding the release path. Taken (skipped release) when status>=0.

What to inspect

BP Register / memory
+0x5B26 r12d should be 0xB000B (IOCTL_HID_READ_REPORT).
+0x5BC5 byte [rsi+0x5A6] must be 0.
+0x5BDE al after HidBthDequeueIrp. al==0 ⇒ the override is about to run; single-step and watch ebp become 0x103.
+0x60E9 eax from HidBthSendIrp. 0x103 here with a negative HidBthConnectToDevice return ⇒ the request will be treated as pending and its buffers not released.
+0x60FF With the override, ebx=0x103 so the jns is taken and the release at 0x1c0006101/0x1c0006114 is skipped ⇒ P and reportObj leaked.

Key instruction offsets

RVA What it is
0x5B26 Load of the IOCTL code (r12d = [irp_stack+0x18]).
0x5B6F call IrpList_EnqueueLocked — add IRP to pending list, arm cancel routine.
0x5BC8 call HidBthConnectToDevice — queue reconnect work item; must fail to reach the override.
0x5BD9 call HidBthDequeueIrp — try to reclaim the IRP.
0x5BDE test al, al — the predicate the patch removes.
0x5BE7 cmovz ebp, eax (eax=0x103) — the removed override.
0x5C44 mov eax, ebp — return-value load.
0x60FF (HidBthReadReport) jns that skips ExFreePool(P) and the report-object free when status >= 0.

In the patched binary, 0x5BDE is jmp 0x5C33 — there is no cmovz, and the error status is carried in ebp from mov ebp, eax at 0x5BCD.

Reaching the path

These are internal HID minidriver requests (IOCTL_HID_READ_REPORT), issued to hidbth.sys by the HID class driver on behalf of a paired Bluetooth HID device. To exercise the path: pair a Bluetooth HID device, drive HID reads, and cancel them (CancelIoEx) while the connection is transitioning (not in the forwarding state), under non-paged-pool pressure so IoAllocateWorkItem fails. The combination is racy and cannot be triggered deterministically.

Structure / offset cheat sheet

Object + offset Meaning
ctx + 0x030 Pending-request counter (interlocked).
ctx + 0x050 Device-object pointer.
ctx + 0x5A6 Connection-state flag — must be 0 for the reconnect branch at 0x1c0005ba6.
ctx + 0x5A7 Secondary flag — used by the alternate branch at 0x1c0005bf5.
ctx + 0x5F4 Interlocked guard used by lock cmpxchg at 0x1c0005bba.
ctx + 0x5FC State flag checked at 0x1c0005c18.
IRP + 0x68 CancelRoutine — armed by IrpList_EnqueueLocked, cleared/tested by HidBthDequeueIrp.
IRP + 0xB8 Tail.Overlay.CurrentStackLocation (read at 0x1c0005b1f).
IRP_stack + 0x18 IoControlCode (0xB000B = IOCTL_HID_READ_REPORT).
Pool tag 0x42646948 ('HidB'), set at 0x1c0006004; buffer P allocated at 0x1c0006013.

8. Changed Functions — Full Triage

Only one function differs in code between the two binaries.

Function Similarity Change type Note
HidBthSendIrp (sub_1C0005AA8) 0.989 Security-relevant (minor) Removed the STATUS_PENDING override on the reconnect-failure path. The test al,al / mov eax,0x103 / cmovz ebp,eax sequence collapses to a single jmp, and the error status is carried in ebp directly (mov ebp, eax at 0x1c0005bcd, replacing mov ebx, eax).

HidBthReadReport (sub_1C0005F70), HidBthIoctl (sub_1C0005670), and HidBthReadCompletion (sub_1C0005DA0) are functionally identical between the builds; their only textual difference is a WPP trace-GUID symbol rename (WPP_80bb…WPP_440d…), which is a tracing-metadata artifact, not a code change. HidBthDequeueIrp (sub_1C0002018), HidBthIrpCancelRoutine (sub_1C0001FC0), IrpList_HandleCancel (sub_1C000B034), and HidBthConnectToDevice (sub_1C0003AEC) are byte-identical. The remaining binary difference is layout/offset shift caused by the one edit.


9. Unmatched Functions

Removed (unpatched → patched):  none
Added    (patched → unpatched): none

No sanitizer, validator, or mitigation function was added or removed. The entire behavioral delta is inside HidBthSendIrp.


10. Confidence & Caveats

Confidence: High for the mechanical change; the security impact is a minor resource leak. The patch removes an unambiguous cmovz ebp, 0x103 override on the reconnect-failure path, and the caller-side effect (>= 0 ⇒ skip the release path) is directly present in the identical HidBthReadReport (sub_1C0005F70) at 0x1c00060ed / 0x1c00060ff / 0x1c0006101 / 0x1c0006114.

Points confirmed against both builds:

  1. The diff. Unpatched overrides the reconnect-failure return to STATUS_PENDING when HidBthDequeueIrp returns FALSE (0x1c0005bde0x1c0005be7); patched propagates the real error (bare jmp at 0x1c0005bde). Direction confirmed.
  2. Caller release semantics. HidBthReadReport frees reportObj ([rdi+0xE70] via __guard_dispatch_icall_fptr, 0x1c0006101) and ExFreePool(P) (0x1c0006114), and decrements the pending-request count, only on a negative return. Confirmed; the function is identical in both builds.
  3. Cancel-ownership handshake. HidBthDequeueIrp (sub_1C0002018) FALSE means the cancel path owns the IRP; IrpList_HandleCancel (sub_1C000B034) completes it once with STATUS_CANCELLED (the list's completion callback is NULL). Confirmed; both functions identical in both builds.
  4. HidBthConnectToDevice (sub_1C0003AEC) failure mode. Returns 0xC000009A when IoAllocateWorkItem returns NULL, or the negative IoAcquireRemoveLockEx status. Confirmed; function identical in both builds.
  5. No corruption primitive. The IRP is completed exactly once by the cancel path; the caller neither completes nor frees the request allocations on the unpatched override, so nothing is freed twice or used after free. The consequence is a leak of P, reportObj, and one pending-count unit.

Reachability. The override fires only when the non-forwarding reconnect branch, a HidBthConnectToDevice failure (resource exhaustion / device removing), and a precisely-timed cancel of the same internal HID read IRP coincide. This is not attacker-deterministic; the realistic worst case is gradual non-paged-pool exhaustion (local DoS).