atapi.sys — device-absent 0xFF status check consolidated from callers into AtapiNoDeviceConnected
KB5094128
1. Overview
- Unpatched Binary:
atapi_unpatched.sys - Patched Binary:
atapi_patched.sys - Overall Similarity Score: 0.9583
- Diff Statistics: 51 matched functions (33 identical, 18 changed), 0 unmatched functions in either direction.
- Verdict: The patch relocates an ATA/ATAPI device-status check. In the unpatched build both callers of
AtapiNoDeviceConnectedtest the status byte for0xFF(floating bus / no device) immediately before the call and skip the call when it is0xFF. The patched build removes those two caller-level pre-checks and instead handles0xFFinsideAtapiNoDeviceConnecteditself. For every status value the observable result — the returned status code and the byte written to the IRB — is identical in both builds. This is a code consolidation (defense-in-depth relocation), not a fix for any reachable defect.
2. Vulnerability Summary
- Severity: None (informational)
- Vulnerability Class: No security-relevant change — guard relocation / refactor. No CWE applies.
- Affected Function:
AtapiNoDeviceConnected(unpatched@ 0x1C0001FC8, patched@ 0x1C0001FD8) and its two callersAtapiSendAtaIdentifyandAtapiSendAtaCommand.
What actually changed:
AtapiNoDeviceConnected decides whether a device is absent by reading the ATA status register (passed by pointer in a2). The unpatched function checks for 0x7F (BSY+DRQ) and 0x00 and has no explicit 0xFF branch. The patched function adds one line at entry: if (*a2 == 0xFF) return true; (device absent).
The two — and only two — callers of this function are AtapiSendAtaIdentify and AtapiSendAtaCommand. In the unpatched build each caller performs its own 0xFF test on the exact status byte it is about to hand to the callee, and short-circuits before calling when the byte is 0xFF:
AtapiSendAtaIdentifyunpatched:if ( v7 == -1 || AtapiNoDeviceConnected(...) ) result = 5;— thev7 == -1term (0xFF) skips the call.AtapiSendAtaCommandunpatched:cmp al, 0FFh; jzat0x1C000226Cskips the call and sets result6.
The patched build deletes both of these pre-checks and lets AtapiNoDeviceConnected report 0xFF back to the caller. Because the callee now returns true for 0xFF, the caller takes the same branch it took before (result 5 for Identify, result 6 for Command), and writes the same status byte to *(Irb+3). The behavior is byte-for-byte equivalent.
Why there is no reachable defect in the unpatched build:
In both callers the byte tested by the caller's 0xFF pre-check is the identical stack byte ([rsp+arg_0] / local PortUchar) that AtapiNoDeviceConnected dereferences through a2; there is no re-read of the port between the pre-check and the call. Consequently, in the unpatched build AtapiNoDeviceConnected is never entered with an initial status of 0xFF — the callers filter it first. The callee's lack of an internal 0xFF entry branch was therefore unreachable, and moving that branch into the callee changes no observable outcome.
The busy-wait loop inside AtapiNoDeviceConnected re-reads the status register and can observe a 0xFF value that appears only after entry (a device removed mid-poll). That in-loop case is handled identically in both builds: the loop exits and the code reaches cmp al, 7Fh; setz dil (return PortUchar == 127;), which yields 0 for a 0xFF value. The patched entry check guards only the initial status, not the in-loop re-read, so it does not alter this path. Any concern about a device removed during the poll is therefore present equally in both builds and is not addressed by this patch.
3. Pseudocode Diff
Decompiled logic of the callee. The only difference is the added entry line in the patched version:
// AtapiNoDeviceConnected — UNPATCHED @ 0x1C0001FC8
bool AtapiNoDeviceConnected(__int64 a1, UCHAR *a2, char a3, char a4) {
UCHAR v4 = *a2;
char v5 = 0;
if ( v4 == 127 || (a3 != 0 && v4 == 0) ) {
int v10 = 0;
v5 = 1;
do { // busy-wait, up to 3 re-reads
AtaPortStallExecution(0x1388u);
PortUchar = AtaPortReadPortUchar(*(PUCHAR *)(a1 + 72));
*a2 = PortUchar;
if ( PortUchar != v4 ) break;
++v10;
} while ( v10 < 3 );
if ( a3 == 0 || PortUchar != 0 )
return PortUchar == 127; // 0xFF here -> returns 0 (same in both builds)
// ... a3-set path ...
}
return v5;
}
// AtapiNoDeviceConnected — PATCHED @ 0x1C0001FD8
bool AtapiNoDeviceConnected(__int64 a1, UCHAR *a2, char a3, char a4) {
UCHAR v4 = *a2;
char v5 = 0;
if ( *a2 == 0xFF ) // ADDED: report device absent
return true;
if ( v4 == 127 || (a3 != 0 && v4 == 0) ) {
// ... identical to unpatched ...
}
return v5;
}
Matching caller change (AtapiSendAtaIdentify), showing the pre-check that was removed:
// UNPATCHED @ 0x1C00020C0 — caller filters 0xFF before the call
if ( v7 == -1 // v7 == 0xFF -> skip callee
|| (v8 = AtapiNoDeviceConnected(a1, &v11, ...), v7 = v11, v8) )
result = 5;
// PATCHED @ 0x1C00020E0 — no pre-check; callee handles 0xFF
v5 = AtapiNoDeviceConnected(a1, &PortUchar, ...);
v7 = PortUchar;
if ( v5 )
result = 5;
AtapiSendAtaCommand receives the mirror-image change: its cmp al, 0FFh; jz pre-check is removed and it relies on the callee's return value; result 6 and the *(Irb+3)=status write are unchanged.
4. Assembly Analysis
The patched callee inserts a single 0xFF test at entry. Real instructions from AtapiNoDeviceConnected in each build:
; UNPATCHED AtapiNoDeviceConnected @ 0x1C0001FC8
0x1C0001FE5 mov bpl, [rdx] ; status = *a2
0x1C0001FE8 xor dil, dil ; v5 = 0
0x1C0001FF7 cmp bpl, 7Fh ; 0x7F check
0x1C0001FFB jz 0x1C000200F ; -> busy-wait
0x1C0001FFD test r8b, r8b ; a3 == 0 ?
0x1C0002000 jz 0x1C0002098 ; -> return v5
0x1C0002006 test bpl, bpl ; status == 0 ?
0x1C0002009 jnz 0x1C0002098 ; -> return v5 (0xFF ends here in unpatched)
; ... busy-wait loop and post-loop 'cmp al,7Fh; setz dil' unchanged vs patched ...
; PATCHED AtapiNoDeviceConnected @ 0x1C0001FD8
0x1C0001FF5 mov bl, [rdx] ; status = *a2
0x1C0001FF7 xor dil, dil ; v5 = 0
0x1C0002006 cmp bl, 0FFh ; ADDED: floating-bus test
0x1C0002009 jnz 0x1C0002015 ; not 0xFF -> normal checks
0x1C000200B mov edi, 1 ; 0xFF -> return true (device absent)
0x1C0002010 jmp 0x1C00020B3
0x1C0002015 cmp bl, 7Fh ; original 0x7F check
0x1C0002018 jz 0x1C000202B
Corresponding removed caller pre-checks (present only in the unpatched build):
; UNPATCHED AtapiSendAtaIdentify @ 0x1C00020C0
0x1C0002164 cmp dl, 0FFh ; caller 0xFF pre-check
0x1C0002167 jz 0x1C00021DA ; -> result 5, skip callee
0x1C0002174 call AtapiNoDeviceConnected
; UNPATCHED AtapiSendAtaCommand @ 0x1C0002200
0x1C000226C cmp al, 0FFh ; caller 0xFF pre-check
0x1C000226E jz 0x1C00024D1 ; -> result 6, skip callee
0x1C000227F call AtapiNoDeviceConnected
In the patched build (AtapiSendAtaIdentify @ 0x1C00020E0, AtapiSendAtaCommand @ 0x1C0002210) these cmp ..., 0FFh pre-checks are absent and the call is reached directly (0x1C0002187 and 0x1C0002286 respectively).
5. Trigger Conditions
Not applicable. There is no reachable defect that differs between the two builds. In the unpatched build the status byte handed to AtapiNoDeviceConnected is filtered for 0xFF by the caller using the same byte the callee reads, so the callee never returns "device present" for an initial 0xFF status. The in-loop 0xFF case (device removed during the internal busy-wait) resolves through return PortUchar == 127; in both builds identically. No sequence of device states produces a different result code, IRB byte, or control-flow branch between unpatched and patched.
6. Exploit Primitive & Development Notes
None. The change is a relocation of an existing 0xFF status check from two callers into their shared callee, with identical observable behavior. No new bound, probe, or state check is introduced that was absent from the unpatched code path as a whole, and no check is weakened. There is no memory-safety, information-disclosure, or denial-of-service primitive attributable to this diff.
7. Debugger PoC Playbook
Not applicable — there is no behavioral difference to demonstrate. An analyst wishing to confirm equivalence can set a breakpoint on AtapiNoDeviceConnected in each build and on the two call sites, and observe that for any status byte the return value, the caller's chosen result code (5 for AtapiSendAtaIdentify, 6 for AtapiSendAtaCommand), and the byte written to *(Irb+3) match between builds. In the unpatched build the callee is never entered with an initial 0xFF status because the caller short-circuits first.
8. Changed Functions — Full Triage
AtapiNoDeviceConnected (sub_1C0001FC8)/AtapiNoDeviceConnected (sub_1C0001FD8)(Not security relevant): Addedif (*a2 == 0xFF) return true;at entry. This duplicates the0xFFfiltering the two callers already performed in the unpatched build; observable behavior is unchanged.AtapiSendAtaIdentify (sub_1C00020C0)/AtapiSendAtaIdentify (sub_1C00020E0)(Not security relevant): Removed the caller-level0xFFpre-check (v7 == -1 || .../cmp dl, 0FFh; jz) and now relies on the callee's return value. Same result code (5) and same*(Irb+3)write for a0xFFstatus.AtapiSendAtaCommand (sub_1C0002200)/AtapiSendAtaCommand (sub_1C0002210)(Not security relevant): Removed the caller-level0xFFpre-check (cmp al, 0FFh; jzat0x1C000226C) and relies on the patched callee. Same result code (6) and same*(Irb+3)write.AtapiHandleMiniportCommand (sub_1C000272C)/AtapiHandleMiniportCommand (sub_1C0002740)(Not security relevant): Control-flow restructuring of the miniport command dispatch (codes0x401-0x405) with a shared completion tail. The per-code handling — including themov ebx, 1set after theAtapiCompleteRequestcall on code0x404, which is present in both builds — is behaviorally equivalent.AtapiProcessInterrupt (sub_1C0002AA0)/AtapiProcessInterrupt (sub_1C0002AB0)(Not security relevant): Bit-test codegen change (test-with-mask replaced bybt) and zero-extension/register-allocation churn. No logic change.WaitOnBusyUntil (sub_1C00012C8)/WaitOnBusyUntil (sub_1C00012CC)(Not security relevant): Loop control-flow restructuring.AtapiDeviceErrorHistoryLogis still called only when the busy bit persists until the retry count is exhausted; net behavior unchanged.- Cosmetic & Register Allocation Changes: The remaining functions (
AtapiInitializeDevice (sub_1C0001D64),AtapiSendAtapiCommand (sub_1C0002500),AtapiHwStartIo (sub_1C0001700),AtapiCompleteRequest (sub_1C0002DDC),AtapiWaitStatusAsync (sub_1C0003730),AtapiResetAtapiDevices (sub_1C00039C0),AtapiSetDmaTransferModeAsync (sub_1C0003C80),AtapiHwBuildIo (sub_1C0001690),AtapiSetBlockSizeAsync (sub_1C0003EC0),AtapiSetupGeometryAsync (sub_1C0003DF0),AtapiSetPioTransferModeAsync (sub_1C0003BD0),__GSHandlerCheckCommon (sub_1C0003FFC)) received compiler codegen differences: bit-test instruction selection (btvs maskedtest), directand-zero stores replacingxor+mov, and register reallocation. None affect security posture.
9. Unmatched Functions
There were no unmatched functions added or removed in this patch.
10. Confidence & Caveats
- Confidence: High. The callee difference is a single added
0xFFentry check, visible in both the disassembly and the decompilation; the two callers drop matching0xFFpre-checks that operated on the same status byte. Tracing every status value through the callee and its only two callers yields identical result codes and IRB side effects in both builds. - Assumptions: Based on the binary context this is the Microsoft Windows ATAPI miniport driver. The call graph was read directly from the two call sites of
AtapiNoDeviceConnected. - Verification:
AtapiNoDeviceConnectedhas exactly two callers (AtapiSendAtaIdentify,AtapiSendAtaCommand); both were inspected in both builds. No other function reads the callee's return value or relies on its0xFFhandling.