1. Overview

Item Value
Unpatched binary mpio_unpatched.sys
Patched binary mpio_patched.sys
Overall similarity 0.9917 (effectively identical driver)
Functions differing by content 3 (MPIOMustProcessRequestAtPassive @ 0x1C00042F8, MPIOReadWrite @ 0x1C000B5F8, MPIOPdoHandleRequest @ 0x1C000BD90)
Unmatched (added/removed) 0 / 0

Verdict: The only intentional behavioral change is in MPIOMustProcessRequestAtPassive (0x1C00042F8): SCSI opcode 0x5E (PERSISTENT_RESERVE_IN) is no longer classified as an operation that must be deferred to passive-level processing. The direction of the routing is the opposite of a "forced synchronous path": in the unpatched build the matched opcodes are deferred to an asynchronous work item that runs at passive level; removing 0x5E makes it handled inline (synchronously) instead. The differences in MPIOReadWrite and MPIOPdoHandleRequest are compiler code-generation and WPP tracing churn. No memory-safety issue, race condition, or use-after-free is demonstrable in either build. This is a dispatch-context refactor, not a security fix.


2. Change Summary

Finding #1 — MPIOMustProcessRequestAtPassive (0x1C00042F8): opcode removed from passive-deferral set

  • Nature: Logic change in an opcode classifier. No demonstrable security impact.
  • Affected function: MPIOMustProcessRequestAtPassive (0x1C00042F8), a helper called by MPIOPdoCommonDeviceControl (0x1C000ADB8).

What the function does

MPIOMustProcessRequestAtPassive returns non-zero when the current request must be handed off to passive level rather than processed in the caller's current context. It first gates on IRQL: at 0x1C000430D it does cmp al, 2 on CR8 and returns 0 (jb) when the current IRQL is below DISPATCH_LEVEL. It then reads the SCSI CDB (via the I/O stack location at IRP+0xB8, the SRB at +0x8, CDB at SRB+0x48 for non-queue-tagged SRBs) and returns 1 for a small set of opcodes:

  • 0x83 SEND_DIAGNOSTIC with service action 0x10 or 0x11
  • 0x84 RECEIVE_DIAGNOSTIC_RESULTS with service action 0x07

The unpatched build additionally returns 1 for 0x5E (PERSISTENT_RESERVE_IN). This is the single removed instruction pair:

00000001C00043FC  cmp     cl, 5Eh         ; PERSISTENT_RESERVE_IN ?
00000001C00043FF  jnz     loc_1C0004403   ; both instructions removed in the patched build
00000001C0004401  mov     dl, 1           ; return value = 1

Because the classifier only returns 1 when already at DISPATCH_LEVEL (CR8 >= 2), the change is only observable for 0x5E requests that reach this code at elevated IRQL. Under an ordinary user-mode IOCTL (arriving at PASSIVE_LEVEL) the classifier returns 0 in both builds and 0x5E is handled identically.

Direction of the routing (as verified in the caller)

In MPIOPdoCommonDeviceControl (0x1C000ADB8) the return value selects between two paths:

00000001C000B10F  call    MPIOMustProcessRequestAtPassive
00000001C000B114  test    al, al
00000001C000B116  jz      loc_1C000B1A4        ; al == 0  -> synchronous path
                                               ; al != 0  -> fall through to work-item path
00000001C000B11C  mov     ebx, 20h
00000001C000B121  mov     r8d, 4F49504Dh       ; pool tag 'MPIO'
00000001C000B129  mov     ecx, 200h            ; NonPagedPoolNx
00000001C000B12E  call    __imp_ExAllocatePoolWithTag
...
00000001C000B166  lea     rdx, MPIOInternalDevCtlCallBack
00000001C000B174  call    MPIOQueueWorkItem    ; deferred/asynchronous, runs at passive level
...
loc_1C000B1A4:
00000001C000B1A4  mov     rdx, rdi             ; Irp
00000001C000B1A7  mov     rcx, rbp             ; IoObject
00000001C000B1AA  call    MPIOPdoInternalDeviceControl   ; synchronous, inline
  • Unpatched, 0x5E at DISPATCH_LEVEL: classifier returns 1 → work item queued via MPIOQueueWorkItem → processed asynchronously at passive level.
  • Patched, 0x5E: classifier returns 0 → MPIOPdoInternalDeviceControl called synchronously, inline.

The patch therefore removes an asynchronous passive-level deferral for PERSISTENT_RESERVE_IN; it does not force the command onto a synchronous path (the unpatched behavior was the deferred one). The work-item deferral code path itself is byte-for-byte identical in both builds; only the classifier's decision for 0x5E changed.

There is no reference-count, rundown-protection, or object-lifetime change on either side of this decision. No use-after-free or race condition is demonstrable from the diff.

Finding #2 — MPIOReadWrite (0x1C000B5F8): code-generation and tracing churn

MPIOReadWrite is the SCSI read/write handler (called from MPIOPdoHandleRequest at 0x1C000C026), not part of the PERSISTENT_RESERVE_IN device-control path. Its differences are:

  • WPP trace GUID change throughout the function (the compiled WPP control GUID differs between builds).
  • Zero-initialization code generation: the unpatched build zeroes r12 (xor r12d, r12d) and stores it into stack slots (mov [rsp+...], r12/r12d/r12b); the patched build writes the same zeros directly (and [rsp+...], 0, mov byte [rsp+...], 0). Both forms write zero. There is no stale, attacker-influenced register value in either build.
  • Two redundant test rbx, rbx; jz guards removed (near 0x1C000BAF5 and 0x1C000BB40). The removed guard at 0x1C000BB40 sits immediately before a reference-count read/decrement that is identical in both builds:
; both builds, unchanged:
00000001C000BB49  mov     eax, [r15+8]     ; (unpatched addr; patched 0x1C000BB39)
00000001C000BB4D  test    eax, eax
00000001C000BB4F  jz      <bad-path>
00000001C000BB55  dec     eax
00000001C000BB57  mov     [r15+8], eax

The removed test rbx, rbx is redundant with the adjacent test rdi, rdi and test eax, eax checks; its removal is an optimizer effect, not a behavioral security change (and if anything it removes a check rather than adding one). The [r15+8] reference count is read, tested for zero, and decremented identically in both builds. The count of ExAcquireRundownProtectionCacheAware/ExReleaseRundownProtectionCacheAware calls is identical in both builds.

Finding #3 — MPIOPdoHandleRequest (0x1C000BD90): single WPP trace constant

The only difference is one WPP trace message identifier:

; unpatched
00000001C000CCDF  mov     r9d, 198Eh
; patched
00000001C000CC..  mov     r9d, 198Bh

This is a trace-message-id shift consequent to the WPP tracing rebuild. Not security-relevant.


3. Assembly Analysis

MPIOMustProcessRequestAtPassive (0x1C00042F8) — unpatched listing

00000001C00042F8  mov     [rsp+arg_0], rbx
00000001C00042FD  mov     [rsp+arg_8], rsi
00000001C0004302  mov     [rsp+arg_10], rdi
00000001C0004307  xor     edx, edx                 ; default return = 0
00000001C0004309  mov     rax, cr8
00000001C000430D  cmp     al, 2                    ; IRQL >= DISPATCH_LEVEL ?
00000001C000430F  jb      loc_1C0004403            ; no  -> return 0
00000001C0004315  mov     r8, [rcx+0B8h]           ; r8 = IO_STACK_LOCATION
00000001C000431C  cmp     byte ptr [r8], 0Fh       ; MajorFunction == 0x0F ?
00000001C0004320  jnz     loc_1C0004403
00000001C0004326  mov     r8, [r8+8]               ; r8 = SRB
00000001C000432A  test    r8, r8
00000001C000432D  jz      loc_1C0004403
00000001C0004333  cmp     byte ptr [r8+2], 28h     ; queue-tag type ?
00000001C0004338  jnz     loc_1C00043D4            ; -> r9 = SRB + 0x48 (direct CDB)
; ... queue-tag path array iteration elided ...
00000001C00043D4  lea     r9, [r8+48h]             ; direct CDB pointer
00000001C00043D8  test    r9, r9
00000001C00043DB  jz      loc_1C0004403
00000001C00043DD  mov     cl, [r9]                 ; cl = CDB[0]
00000001C00043E0  mov     r8b, [r9+1]              ; r8b = CDB[1]
00000001C00043E4  cmp     cl, 83h                  ; SEND_DIAGNOSTIC ?
00000001C00043E7  jnz     short loc_1C00043F1
00000001C00043E9  lea     eax, [r8-10h]
00000001C00043ED  cmp     al, 1
00000001C00043EF  jbe     short loc_1C0004401      ; service action 0x10/0x11 -> return 1
00000001C00043F1  cmp     cl, 84h                  ; RECEIVE_DIAGNOSTIC_RESULTS ?
00000001C00043F4  jnz     short loc_1C00043FC
00000001C00043F6  cmp     r8b, 7
00000001C00043FA  jz      short loc_1C0004401      ; service action 7 -> return 1
00000001C00043FC  cmp     cl, 5Eh                  ; *** removed in patched build ***
00000001C00043FF  jnz     short loc_1C0004403      ; *** removed in patched build ***
00000001C0004401  mov     dl, 1                    ; return value = 1
00000001C0004403  mov     rbx, [rsp+arg_0]         ; epilogue
00000001C0004408  mov     al, dl                   ; return value in al
00000001C000440A  mov     rsi, [rsp+arg_8]
00000001C000440F  mov     rdi, [rsp+arg_10]
00000001C0004414  retn

MPIOMustProcessRequestAtPassive (0x1C00042F8) — patched listing (opcode-check tail)

00000001C00043DD  mov     r8b, [r9]                ; byte0/byte1 loaded into swapped registers
00000001C00043E0  mov     cl, [r9+1]
00000001C00043E4  cmp     r8b, 83h
00000001C00043E8  jnz     short loc_1C00043F1
00000001C00043EA  lea     eax, [rcx-10h]
00000001C00043ED  cmp     al, 1
00000001C00043EF  jbe     short loc_1C00043FC      ; service action 0x10/0x11 -> return 1
00000001C00043F1  cmp     r8b, 84h
00000001C00043F5  jnz     short loc_1C00043FE
00000001C00043F7  cmp     cl, 7
00000001C00043FA  jnz     short loc_1C00043FE      ; service action != 7 -> return 0
00000001C00043FC  mov     dl, 1                    ; return value = 1 (only 0x83 / 0x84 reach here)
00000001C00043FE  mov     rbx, [rsp+arg_0]
00000001C0004403  mov     al, dl
00000001C0004405  mov     rsi, [rsp+arg_8]
00000001C000440A  mov     rdi, [rsp+arg_10]
00000001C000440F  retn

The 0x5E comparison and its branch are gone; the 0x83/0x84 service-action checks are unchanged in substance. The cl/r8b register assignment for the two CDB bytes is swapped between builds but semantically identical.


4. Reachability / Trigger Context

  • The classifier is reached from MPIOPdoCommonDeviceControl (0x1C000ADB8) when handling IRP_MJ_DEVICE_CONTROL (MajorFunction 0x0F) with an embedded SRB whose CDB opcode is inspected.
  • The 0x5E decision only differs between builds when the request is being processed at DISPATCH_LEVEL (the CR8 >= 2 gate at 0x1C000430D). User-mode IOCTLs arrive at PASSIVE_LEVEL, at which point both builds return 0 and behave identically.
  • No reference count, rundown-protection block, or object lifetime is affected by the classifier decision. The two candidate paths (MPIOQueueWorkItem deferral vs. MPIOPdoInternalDeviceControl inline call) both complete the same IRP; the deferral path is unchanged between builds.

No attacker-controlled memory-safety primitive is reachable through this change.


5. Security Assessment

  • Class: dispatch-context / IRQL-handling classification change for one SCSI opcode.
  • Severity: none (no security-relevant change).
  • CWE: not applicable.
  • Impact: none demonstrable. The change alters whether PERSISTENT_RESERVE_IN is deferred to passive-level processing when encountered at elevated IRQL. There is no evidence of a use-after-free, race condition, reference-count error, information disclosure, or privilege boundary crossing in either build.

The reference-count read/decrement at [r15+8] in MPIOReadWrite is identical in both builds; the rundown-protection acquire/release pairing is identical in both builds. The remaining differences are compiler code generation (zero-initialization form, redundant-guard elimination, basic-block ordering) and WPP tracing rebuild artifacts (control GUID and trace message ids).


6. Exploit Primitive

None. No memory-corruption, use-after-free, or race-condition primitive is present in the changed code. The classifier decision does not create or destroy any object, does not alter reference counting, and does not change rundown-protection lifetime. The candidate execution paths differ only in whether PERSISTENT_RESERVE_IN runs inline or via a passive-level work item, both of which complete the same request.


7. Verification Notes (for reproduction against the binaries)

The following are the concrete, verifiable anchors for this change; all addresses are module-relative to the mpio base:

  • 0x1C00043FCcmp cl, 5Eh in MPIOMustProcessRequestAtPassive, present only in the unpatched build.
  • 0x1C00043FFjnz loc_1C0004403, removed with it.
  • 0x1C000430Dcmp al, 2 IRQL gate (CR8 >= DISPATCH_LEVEL); return-1 is only possible at or above DISPATCH_LEVEL.
  • 0x1C000ADB8MPIOPdoCommonDeviceControl, caller.
  • 0x1C000B10Fcall MPIOMustProcessRequestAtPassive; 0x1C000B114 test al, al; 0x1C000B116 jz loc_1C000B1A4.
  • 0x1C000B174call MPIOQueueWorkItem (return-1 / work-item / passive path).
  • 0x1C000B1AAcall MPIOPdoInternalDeviceControl (return-0 / synchronous / inline path).
  • 0x1C000B5F8MPIOReadWrite; reference count read/decrement at [r15+8] (0x1C000BB49 unpatched / 0x1C000BB39 patched) is identical in both builds.
  • 0x1C000CCDFmov r9d, 198Eh in MPIOPdoHandleRequest, changed to 198Bh in the patched build (WPP trace id).

Struct / offset notes (as used by this build)

  • IRP +0xB8IO_STACK_LOCATION*
  • IO_STACK_LOCATION +0x0MajorFunction (0x0F required to proceed)
  • IO_STACK_LOCATION +0x8 → embedded SRB pointer
  • SRB +0x2 → queue-tag-type field (0x28 selects the queue-tag CDB lookup; otherwise CDB is at SRB+0x48)
  • SRB +0x48 → CDB start (direct path); CDB[0] is the opcode the classifier keys on

8. Changed Functions — Triage

MPIOMustProcessRequestAtPassive (0x1C00042F8) — not security-relevant

  • Removed opcode 0x5E (PERSISTENT_RESERVE_IN) from the passive-deferral set.
  • Cosmetic register swap (cl <-> r8b) for the two CDB bytes.
  • Effect: at DISPATCH_LEVEL, 0x5E is now handled inline rather than deferred to a passive-level work item. No lifetime/reference-count/race impact.

MPIOReadWrite (0x1C000B5F8) — not security-relevant

  • WPP trace control GUID rebuilt.
  • Zero-initialization emitted as and [mem], 0 instead of mov [mem], r12 (both write zero; r12 is explicitly zeroed in the unpatched build).
  • Two redundant test rbx, rbx; jz guards removed by the optimizer.
  • Reference-count and rundown-protection handling unchanged in substance.

MPIOPdoHandleRequest (0x1C000BD90) — not security-relevant

  • Single WPP trace message identifier changed (198Eh -> 198Bh).

All other functions are identical between the two builds.


9. Unmatched Functions

None added, none removed.


10. Confidence & Caveats

Confidence: High.

  • The removal of the 0x5E comparison in MPIOMustProcessRequestAtPassive is unambiguous (single instruction pair at 0x1C00043FC/0x1C00043FF present only in the unpatched build).
  • The routing direction was confirmed in the caller MPIOPdoCommonDeviceControl: return-1 selects the MPIOQueueWorkItem passive-deferral path, return-0 selects the synchronous MPIOPdoInternalDeviceControl call. The unpatched build deferred 0x5E; the patched build does not.
  • MPIOReadWrite and MPIOPdoHandleRequest were diffed instruction-by-instruction; their differences are code-generation and WPP tracing artifacts with no security bearing.
  • No reference-count race, rundown double-release, or use-after-free is present in the changed code of either build.