clfs.sys — WIL-staged large-sector device alignment rollout
KB5078885
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | clfs_unpatched.sys |
| Patched binary | clfs_patched.sys |
| Overall similarity | 0.9912 |
| Matched functions | 1261 |
| Changed functions | 2 |
| Identical functions | 1259 |
| Unmatched (unpatched → patched) | 0 / 0 |
Verdict. The patch removes a WIL feature-flag gate in two CLFS request handlers, CClfsRequest::ReserveAndAppendLog (0x1C0057144, IOCTL 0x8007A827) and CClfsRequest::WriteRestart (0x1C0058B6C → 0x1C0058B5C, IOCTL 0x8007281F). In the unpatched build the call to CClfsRequest::GetAlignedBufferSize (0x1C0028CBC), which queries the backing device's real sector size, is gated behind the feature Feature_2227659065__private_IsEnabledDeviceUsage (0x1C000D9C4). When the feature is off (the default), the handlers use a smaller, non-device-derived length instead. The patch removes the gate and calls GetAlignedBufferSize unconditionally.
This is a staged rollout of device-sector-size-aware buffer alignment (support for storage whose physical sector size is greater than 512 bytes, i.e. 4Kn / Advanced Format media), delivered through Windows' WIL feature-staging mechanism. It is a functional-correctness / compatibility change, not a memory-safety fix. No out-of-bounds read or write, and no kernel pool-corruption primitive, is demonstrable from the two builds. The direction of any size mismatch is opposite to what an overflow would require: the unpatched default path produces a smaller aligned length, and the patched path rounds the length up to the true sector size. A smaller MDL length locks and transfers fewer bytes; it does not cause the storage stack to overrun adjacent pool.
The change is real and is located exactly as described below, but its security relevance is assessed as none.
2. Change Summary
Both changed functions carry the same edit, expressed in two symmetric code paths.
Finding A — CClfsRequest::ReserveAndAppendLog (0x1C0057144, IOCTL 0x8007A827)
- Severity: None (no security-relevant change)
- Nature: WIL feature-gate removed; device-sector-size query made unconditional.
- Affected function:
CClfsRequest::ReserveAndAppendLogat0x1C0057144in both builds, reached viaClfsDispatchIoRequest → CClfsRequest::Dispatch(case0x8007A827).
What changed. In the unpatched build, the alignment length used for the MDL that is built over the restart-area buffer is chosen by a feature check. The code calls Feature_2227659065__private_IsEnabledDeviceUsage (0x1C000D9C4), which tests bit 0x10 of Feature_2227659065__private_featureState and otherwise consults the WIL fallback (Feature_2227659065__private_IsEnabledFallback, default parameter 3). When the feature is off (the default), the code takes the branch lea r8d, [rbx+1FFh]; and r8d, 0FFFFFE00h, i.e. rounds the input size up to a 512-byte boundary. When the feature is on, it calls GetAlignedBufferSize (0x1C0028CBC) to obtain the device-sector-aligned length. That length is passed to the MDL wrapper ClfsProbeAndAllocateMdl (0x1C0011420) and, later, to MmProbeAndLockPages and MmMapLockedPagesSpecifyCache.
The patch deletes the feature check and both branches, and calls GetAlignedBufferSize unconditionally before the MDL allocation. GetAlignedBufferSize itself falls back to 512-byte alignment when the device query fails or reports a sector size ≤ 0x200.
Why this is not a security fix. The length computed here is the length of an MDL built over an already-allocated restart-area buffer ([[this+30h]+70h]), and it is the length of the resulting disk transfer. The device-aligned length produced by GetAlignedBufferSize is greater than or equal to the 512-byte-aligned length used by the unpatched default. A smaller MDL length locks fewer pages and transfers fewer bytes; it cannot cause a write past the end of the buffer or into adjacent pool. The Windows storage stack transfers exactly the number of bytes described by the MDL/IRP; it does not re-derive a larger sector length mid-transfer and overrun the caller's buffer. On large-sector (4Kn) media a sub-sector-aligned transfer would be rejected or bounce-buffered by the storage driver, which is a functional-correctness concern (truncated or failed restart-area I/O), not memory corruption. The feature name (IsEnabledDeviceUsage) and its content (query the device's real sector size) are consistent with large-sector-media support being rolled out, not with a memory-safety patch.
Entry point / reachability. The function is reached from user mode via the CLFS log APIs. The dispatch chain is:
CClfsDriver::LogIoDispatch(0x1C0034550).ClfsDispatchIoRequest(0x1C00345A0).CClfsRequest::Dispatch(0x1C00347C4) — function-code switch; case0x8007A827at0x1C0034B5Bjumps to0x1C0034BC2, which callsCClfsRequest::ReserveAndAppendLogat0x1C0034BC5.CClfsRequest::ReserveAndAppendLog(0x1C0057144).
This chain is genuine, but it leads only to the alignment change described above; it is not a reachable memory-corruption primitive.
Finding B — CClfsRequest::WriteRestart (0x1C0058B6C → 0x1C0058B5C, IOCTL 0x8007281F)
- Severity: None (no security-relevant change)
- Nature: Same WIL feature-gate removal as Finding A.
- Affected function:
CClfsRequest::WriteRestart,0x1C0058B6C(unpatched) /0x1C0058B5C(patched).
What changed. Same pattern. At 0x1C0058CB8 the unpatched code calls the same Feature_2227659065__private_IsEnabledDeviceUsage gate. When it returns zero the code branches (jz 0x1C0058CFF) over the GetAlignedBufferSize call and reaches ClfsProbeAndAllocateMdl with the raw length [this+108h] (no device-based alignment). When the feature is on, it calls GetAlignedBufferSize(this, [this+108h]) first. The patch removes the gate and the dual-path logic, and calls GetAlignedBufferSize unconditionally (patched at 0x1C0058CAE) before ClfsProbeAndAllocateMdl. The stack frame shrinks accordingly.
The dispatch chain is identical to Finding A except for the case selector: IOCTL 0x8007281F at 0x1C0034A1C jumps to 0x1C0034AAE, which calls CClfsRequest::WriteRestart at 0x1C0034AB1.
Why this is not a security fix. Same reasoning as Finding A. The patched path rounds [this+108h] up to the device sector size, producing a length greater than or equal to the raw value used by the unpatched default. The larger, device-aligned length is required for valid whole-sector I/O on large-sector media; the smaller unpatched length truncates the transfer. Neither direction overruns the buffer.
3. Pseudocode Diff
CClfsRequest::ReserveAndAppendLog (0x1C0057144)
// ---------- UNPATCHED ----------
flag = Feature_2227659065__private_IsEnabledDeviceUsage(); // bit 0x10 of featureState, else WIL fallback
if (flag == 0) { // default: feature off
r8 = (size + 0x1FF) & 0xFFFFFE00; // 512-byte round-up
} else {
r8 = GetAlignedBufferSize(this, size); // device-sector-aligned length
}
status = ClfsProbeAndAllocateMdl(AccessMode, buffer, r8, ...); // MDL over existing buffer
// ... MmProbeAndLockPages / MmMapLockedPagesSpecifyCache over r8 bytes
// ---------- PATCHED ----------
r8 = GetAlignedBufferSize(this, size); // always device-sector-aligned
status = ClfsProbeAndAllocateMdl(AccessMode, buffer, r8, ...);
GetAlignedBufferSize (0x1C0028CBC, byte-identical in both builds) queries the device via a guarded indirect call and rounds up to the reported sector size, falling back to 512-byte alignment only when the query fails or the sector size is ≤ 0x200:
// GetAlignedBufferSize (unchanged between builds)
status = (*(*this_dev + 0xF0))(...); // query device geometry (guarded icall)
if (status < 0 || sector <= 0x200)
return (size + 0x1FF) & ~0x1FF; // fallback: 512-byte alignment
return (size + sector - 1) & ~(sector - 1); // device-sector alignment
CClfsRequest::WriteRestart (0x1C0058B6C → 0x1C0058B5C)
// ---------- UNPATCHED ----------
flag = Feature_2227659065__private_IsEnabledDeviceUsage();
r8 = *(this + 0x108);
if (flag != 0) {
r8 = GetAlignedBufferSize(this, *(this + 0x108)); // device-sector-aligned
}
// else: r8 keeps the raw *(this+0x108) length
ClfsProbeAndAllocateMdl(AccessMode, buffer, r8, ...);
// ---------- PATCHED ----------
r8 = GetAlignedBufferSize(this, *(this + 0x108)); // always device-sector-aligned
ClfsProbeAndAllocateMdl(AccessMode, buffer, r8, ...);
4. Assembly Analysis
CClfsRequest::ReserveAndAppendLog (0x1C0057144) — unpatched key region
00000001C00578E9 call Feature_2227659065__private_IsEnabledDeviceUsage
00000001C00578EE mov [rsp+268h+Length], eax
00000001C00578F5 mov ecx, edi
00000001C00578F7 test eax, eax
00000001C00578F9 setnz cl
00000001C00578FC test ecx, ecx
00000001C00578FE jz short loc_1C005790F ; feature off -> 512-byte round-up
00000001C0057900 mov edx, ebx ; size
00000001C0057902 mov rcx, rsi ; this
00000001C0057905 call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z
00000001C005790A mov r8d, eax
00000001C005790D jmp short loc_1C005791D
00000001C005790F lea r8d, [rbx+1FFh] ; 512-byte round-up (feature-off path)
00000001C0057916 and r8d, 0FFFFFE00h
00000001C005791D lea rax, [rsp+268h+Mdl]
00000001C0057925 mov rdx, [rsi+30h]
00000001C005792E mov rdx, [rdx+70h] ; existing restart-area buffer
00000001C0057939 call ClfsProbeAndAllocateMdl
Later in the same function the MDL is locked and mapped:
00000001C0057A36 call ... MmProbeAndLockPages
00000001C0057A6F call ... MmMapLockedPagesSpecifyCache
CClfsRequest::ReserveAndAppendLog (0x1C0057144) — patched equivalent
00000001C0057908 mov edx, ebx ; size
00000001C005790A mov rcx, r14 ; this
00000001C005790D call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z ; unconditional
00000001C0057912 mov r8d, eax
00000001C0057915 mov rdx, [r14+30h]
00000001C0057926 mov rdx, [rdx+70h]
00000001C0057931 call ClfsProbeAndAllocateMdl
The call Feature_2227659065__private_IsEnabledDeviceUsage, the lea r8d, [rbx+1FFh] and the and r8d, 0FFFFFE00h instructions are gone; the conditional collapses to a single unconditional GetAlignedBufferSize call.
CClfsRequest::WriteRestart (0x1C0058B6C) — unpatched key region
00000001C0058CB8 call Feature_2227659065__private_IsEnabledDeviceUsage
00000001C0058CBD mov [rsp+118h+arg_10], eax
00000001C0058CC4 mov ecx, r14d
00000001C0058CC7 test eax, eax
00000001C0058CC9 setnz cl
00000001C0058CCC mov r8d, [rsi+108h] ; raw length
00000001C0058CD3 test ecx, ecx
00000001C0058CD5 jz short loc_1C0058CFF ; feature off -> skip query, use raw length
00000001C0058CD7 mov rbx, [rsi+30h]
00000001C0058CDB mov edx, r8d
00000001C0058CDE mov rcx, rsi
00000001C0058CE1 call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z ; feature-on path only
00000001C0058CE6 mov r8d, eax
...
00000001C0058CFF mov rax, [rsi+30h] ; feature-off path: r8d = raw [rsi+108h]
00000001C0058D10 mov rdx, [rax+70h]
00000001C0058D17 call ClfsProbeAndAllocateMdl
CClfsRequest::WriteRestart (0x1C0058B5C) — patched equivalent
00000001C0058CA5 mov edx, [rsi+108h] ; length, no feature check
00000001C0058CAB mov rcx, rsi
00000001C0058CAE call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z ; unconditional
00000001C0058CB3 mov r8d, eax
00000001C0058CC3 mov rdx, [rbx+70h]
00000001C0058CCA call ClfsProbeAndAllocateMdl
ClfsProbeAndAllocateMdl is the same routine in both builds (relocated from 0x1C0011420 to 0x1C00113E0). It probes the caller-supplied buffer for write access and calls IoAllocateMdl(buffer, length, 0, 1, NULL); the length it receives is the aligned value computed above.
5. Trigger Conditions
The two code paths differ from unpatched to patched only when both of the following hold:
- Volume geometry. The CLFS log's backing device reports a physical sector size greater than 512 bytes (4Kn / Advanced Format media, or virtual disks configured with a >512-byte physical sector). On 512-byte-sector devices
GetAlignedBufferSizereturns the same 512-byte-aligned value as the unpatched default, so the two builds behave identically. - Feature-off default. Bit
0x10/ the WIL fallback forFeature_2227659065selects the non-device path. This is the pre-rollout default.
Under those conditions, the observable difference is the length passed to ClfsProbeAndAllocateMdl, MmProbeAndLockPages and MmMapLockedPagesSpecifyCache: a 512-byte-aligned length (unpatched default) versus the true-sector-aligned length (patched). This is a change in how much of the restart-area buffer is locked and transferred to or from disk. It is a functional-correctness difference for large-sector media, not a memory-safety trigger. No bugcheck or pool-corruption effect is demonstrable from the binaries.
6. Impact Assessment
No security-relevant primitive is established by this change.
- The length in question sizes an MDL built over a pre-existing buffer and sizes the corresponding disk transfer. The device-aligned length (patched) is greater than or equal to the 512-byte-aligned length (unpatched default). A smaller MDL locks and transfers fewer bytes; it cannot write past the buffer or into adjacent pool.
- The storage stack transfers exactly the MDL/IRP-described length; it does not independently expand the transfer to the device sector size and overrun the caller's buffer. On large-sector media a sub-sector-aligned transfer is rejected or bounce-buffered by the storage driver.
- No out-of-bounds read, out-of-bounds write, information disclosure, or kernel pool overflow is observable from the two builds. There is no attacker-controlled length or content that turns this alignment choice into a memory-corruption primitive.
Consequently the earlier CWE-787 / CWE-125 classification and HIGH severity are not supported and are withdrawn. The change is best characterized as WIL-staged large-sector-media alignment support with, at most, defense-in-depth value.
7. Verification Notes
The following observations are directly checkable in a kernel debugger against clfs_unpatched.sys and clfs_patched.sys; they confirm the nature of the change, not a memory-safety defect.
Finding A — ReserveAndAppendLog
bpat0x1C00578E9(unpatched): the call toFeature_2227659065__private_IsEnabledDeviceUsage.EAX == 0selects the 512-byte-round-up branch at0x1C005790F.bpat0x1C0057939(unpatched) /0x1C0057931(patched): theClfsProbeAndAllocateMdlcall.R8Dis the aligned length. On a >512-byte-sector device it is 512-aligned in the unpatched default path and device-sector-aligned in the patched build.dt _MDL @rcxatMmProbeAndLockPages: theByteCountreflects whichever aligned length was chosen. This confirms the alignment difference, not a corruption.
Finding B — WriteRestart
bpat0x1C0058CB8(unpatched): the feature-gate call.EAX == 0takes thejz 0x1C0058CFFbranch that uses the raw[rsi+108h]length.bpat0x1C0058D17(unpatched) /0x1C0058CCA(patched):R8Dis the length handed toClfsProbeAndAllocateMdl— raw[rsi+108h]in the unpatched default, device-sector-aligned in the patched build.
Feature-state note
Feature_2227659065__private_IsEnabledDeviceUsage (0x1C000D9C4) is standard WIL feature-staging code: it reads Feature_2227659065__private_featureState, tests bit 0x10, and otherwise calls Feature_2227659065__private_IsEnabledFallback with default parameter 3. In the patched build the same slot at 0x1C000D9C4 is renumbered to Feature_3017759032__private_IsEnabledDeviceUsage — ordinary WIL feature-database churn between builds.
8. Changed Functions — Full Triage
Two functions changed in the entire diff.
| Function | Similarity | Change type | Note |
|---|---|---|---|
CClfsRequest::ReserveAndAppendLog (0x1C0057144) |
0.9514 | Non-security (WIL feature-gate removal) | Feature-gated GetAlignedBufferSize query made unconditional; removed call Feature_2227659065__private_IsEnabledDeviceUsage, lea r8d,[rbx+1FFh], and r8d,0FFFFFE00h. ClfsProbeAndAllocateMdl relocated 0x1C0011420 → 0x1C00113E0 (same routine). |
CClfsRequest::WriteRestart (0x1C0058B6C → 0x1C0058B5C) |
0.9559 | Non-security (WIL feature-gate removal) | Same pattern; feature check plus dual-path logic collapsed to a single unconditional GetAlignedBufferSize call. Stack frame shrank (0xE0 → 0xD0) as the conditional-path locals are gone. |
Cosmetic / relocation changes. ClfsProbeAndAllocateMdl (0x1C0011420 → 0x1C00113E0) and a couple of neighbours are the same routines at addresses shifted by a fixed delta due to earlier .text edits. GetAlignedBufferSize (0x1C0028CBC) is byte-identical between builds, as are ClfsDispatchIoRequest, CClfsRequest::Dispatch, MmProbeAndLockPages and MmMapLockedPagesSpecifyCache.
9. Unmatched Functions
None. Both unmatched_unpatched and unmatched_patched are zero. No sanitizer, bounds check, or validation routine was added or removed. The only behavioral change is removal of the WIL feature gate so the device-sector-size query is always performed.
10. Confidence & Caveats
Confidence: High that the change is accurately located and is a WIL-staged alignment rollout rather than a security fix.
- The structural diff is minimal and unambiguous: a WIL feature gate (
Feature_2227659065__private_IsEnabledDeviceUsage) that selected between a 512-byte-aligned length and a device-sector-aligned length was removed, making the device-sector query unconditional. GetAlignedBufferSizerounds the length up to the true sector size. The unpatched default therefore produces a smaller length, and a smaller MDL cannot overrun the buffer or adjacent pool. The size-mismatch direction is inconsistent with an out-of-bounds write or read.- The feature name and content (query the device's real sector size) match large-sector-media (4Kn) support, and the feature is renumbered between builds — typical WIL servicing churn, not a memory-safety patch.
Caveat. If a future build is found where the same buffer is allocated at one alignment and written at a larger, independently derived sector length, that specific mismatch would warrant re-examination. No such flow is present in these two builds; the alignment length here consistently sizes both the buffer's MDL and the corresponding transfer.