mpio.sys — PERSISTENT_RESERVE_IN passive-deferral classification refactor
KB5078752
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 byMPIOPdoCommonDeviceControl(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:
0x83SEND_DIAGNOSTIC with service action0x10or0x110x84RECEIVE_DIAGNOSTIC_RESULTS with service action0x07
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,
0x5Eat DISPATCH_LEVEL: classifier returns 1 → work item queued viaMPIOQueueWorkItem→ processed asynchronously at passive level. - Patched,
0x5E: classifier returns 0 →MPIOPdoInternalDeviceControlcalled 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; jzguards 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 handlingIRP_MJ_DEVICE_CONTROL(MajorFunction0x0F) with an embedded SRB whose CDB opcode is inspected. - The
0x5Edecision 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 (
MPIOQueueWorkItemdeferral vs.MPIOPdoInternalDeviceControlinline 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:
0x1C00043FC—cmp cl, 5EhinMPIOMustProcessRequestAtPassive, present only in the unpatched build.0x1C00043FF—jnz loc_1C0004403, removed with it.0x1C000430D—cmp al, 2IRQL gate (CR8 >= DISPATCH_LEVEL); return-1 is only possible at or above DISPATCH_LEVEL.0x1C000ADB8—MPIOPdoCommonDeviceControl, caller.0x1C000B10F—call MPIOMustProcessRequestAtPassive;0x1C000B114 test al, al;0x1C000B116 jz loc_1C000B1A4.0x1C000B174—call MPIOQueueWorkItem(return-1 / work-item / passive path).0x1C000B1AA—call MPIOPdoInternalDeviceControl(return-0 / synchronous / inline path).0x1C000B5F8—MPIOReadWrite; reference count read/decrement at[r15+8](0x1C000BB49 unpatched / 0x1C000BB39 patched) is identical in both builds.0x1C000CCDF—mov r9d, 198EhinMPIOPdoHandleRequest, changed to198Bhin the patched build (WPP trace id).
Struct / offset notes (as used by this build)
- IRP
+0xB8→IO_STACK_LOCATION* IO_STACK_LOCATION+0x0→MajorFunction(0x0Frequired to proceed)IO_STACK_LOCATION+0x8→ embedded SRB pointer- SRB
+0x2→ queue-tag-type field (0x28selects the queue-tag CDB lookup; otherwise CDB is atSRB+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,
0x5Eis 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], 0instead ofmov [mem], r12(both write zero;r12is explicitly zeroed in the unpatched build). - Two redundant
test rbx, rbx; jzguards 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
0x5Ecomparison inMPIOMustProcessRequestAtPassiveis 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 theMPIOQueueWorkItempassive-deferral path, return-0 selects the synchronousMPIOPdoInternalDeviceControlcall. The unpatched build deferred0x5E; the patched build does not. MPIOReadWriteandMPIOPdoHandleRequestwere 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.