hidbth.sys — Request-buffer and pending-count leak on a race-gated reconnect error path (CWE-401) fixed
KB5078752
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')(tag0x42646948), at0x1c0006013.reportObj— a report/transfer object allocated through the object vtable at[rdi+0xE68](0x1c000603f);Pis stored inside it atreportObj+0x88.
After HidBthSendIrp returns, HidBthReadReport releases these two allocations and decrements the pending-request count only when the returned status is negative:
0x1c00060edjns— if status>= 0, skipHidBthDecrementPendingRequestCountand instead clear the out-flag byte (mov byte [r15], 0at0x1c00060f9), tellingHidBthIoctlnot to complete the IRP.0x1c00060ffjns— if status>= 0, skip the report-object free ([rdi+0xE70]via__guard_dispatch_icall_fptrat0x1c0006101) andExFreePool(P)at0x1c0006114.
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
HidBthConnectToDevicefailure (work-item allocation failure or device removing), and a precisely-timed cancel of the same internal HID read IRP so thatHidBthDequeueIrpreturnsFALSE. These are internal HID minidriver IRPs (IOCTL_HID_READ_REPORT) issued tohidbth.sysby 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
- The HID class driver dispatches an
IOCTL_HID_READ_REPORT(0xB000B) tohidbth.sys. HidBthIoctl (sub_1C0005670)routes it toHidBthReadReport (sub_1C0005F70).HidBthReadReportallocatesP('HidB') andreportObj, installsHidBthReadCompletionin the next-lower stack location, bumps the pending-request count, then callsHidBthSendIrp.HidBthSendIrp (sub_1C0005AA8):- Adds the IRP to the pending list (
IrpList_EnqueueLocked (sub_1C000AEC4), which returnsSTATUS_PENDINGand arms the cancel routine). - Releases the spinlocks.
- On the reconnect branch (
[rsi+0x5a6]==0, and for0xB000Bthelock cmpxchg [rsi+0x5f4]succeeds), callsHidBthConnectToDevice (sub_1C0003AEC). HidBthConnectToDevicefails (IoAllocateWorkItem→ NULL, or remove-lock unavailable) → returns negative status.HidBthDequeueIrp (sub_1C0002018)cannot reclaim the IRP (a concurrent cancel already claimed it) → returnsFALSE.- Unpatched:
cmovz ebp, 0x103overrides the failure withSTATUS_PENDING. HidBthReadReportseesstatus >= 0, clears the out-flag (mov byte [r15], 0at0x1c00060f9), skips the release path at0x1c00060ff, and returns — leakingP,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:
-
Reach the pend/reconnect path. An
IOCTL_HID_READ_REPORT(0xB000B) request must be dispatched throughHidBthIoctl (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 at0x1c0005b26gates the0xB000Bcmpxchgbranch that leads into the reconnect call. -
State matches the reconnect branch. At
0x1c0005ba6,ctx[0x5a6] == 0; for0xB000Bthe code executeslock cmpxchg [rsi+0x5f4], 0(0x1c0005bba) and falls through toHidBthConnectToDevice (sub_1C0003AEC)only if the cmpxchg succeeds. -
HidBthConnectToDevice (sub_1C0003AEC)must FAIL. It fails whenIoAllocateWorkItemreturns NULL (returningSTATUS_INSUFFICIENT_RESOURCES,0xC000009A) or whenIoAcquireRemoveLockExfails (device removing). -
The IRP must not be reclaimable by
HidBthDequeueIrp (sub_1C0002018). This is the race. The IRP was enqueued and its cancel routine armed at0x1c0005b6f. Between that and theHidBthDequeueIrpcall at0x1c0005bd9, the I/O manager must have selected the IRP's cancel routine (via aCancelIoExon the HID read), soHidBthDequeueIrpreturnsFALSE.
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:
- The diff. Unpatched overrides the reconnect-failure return to
STATUS_PENDINGwhenHidBthDequeueIrpreturnsFALSE(0x1c0005bde–0x1c0005be7); patched propagates the real error (barejmpat0x1c0005bde). Direction confirmed. - Caller release semantics.
HidBthReadReportfreesreportObj([rdi+0xE70]via__guard_dispatch_icall_fptr,0x1c0006101) andExFreePool(P)(0x1c0006114), and decrements the pending-request count, only on a negative return. Confirmed; the function is identical in both builds. - Cancel-ownership handshake.
HidBthDequeueIrp (sub_1C0002018)FALSEmeans the cancel path owns the IRP;IrpList_HandleCancel (sub_1C000B034)completes it once withSTATUS_CANCELLED(the list's completion callback is NULL). Confirmed; both functions identical in both builds. HidBthConnectToDevice (sub_1C0003AEC)failure mode. Returns0xC000009AwhenIoAllocateWorkItemreturns NULL, or the negativeIoAcquireRemoveLockExstatus. Confirmed; function identical in both builds.- 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).