clfs.sys — storage sector-size alignment made unconditional by removing a feature-staging gate
KB5078752
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | clfs_unpatched.sys |
| Patched binary | clfs_patched.sys |
| Overall similarity | 0.9917 |
| Matched functions | 1230 |
| Changed functions | 2 |
| Identical functions | 1228 |
| Unmatched (unpatched) | 0 |
| Unmatched (patched) | 0 |
Verdict. The patch changes exactly two functions, CClfsRequest::ReserveAndAppendLog (0x1C0055BEC) and CClfsRequest::WriteRestart (0x1C0057598 unpatched / 0x1C0057594 patched). In both, a feature-staging gate that chose between two ways of computing the byte length of a Memory Descriptor List (MDL) is removed, so the length is now always computed by CClfsRequest::GetAlignedBufferSize (0x1C0027860), which rounds the requested I/O size up to the storage device's real logical sector size. In the unpatched build the gate was EvaluateCurrentState (0x1C000CE64) evaluated against the feature descriptor g_Feature_2160550201_59010878; when that returned zero the code used a smaller length (the raw size in WriteRestart, or a hardcoded 512-byte round in ReserveAndAppendLog).
This is not a memory-safety fix. The length in question only controls how many bytes of a pre-existing log-block buffer an MDL describes; it does not size any allocation. The gated-off ("skip alignment") path produces a length that is less than or equal to the aligned length, so it describes fewer bytes of that buffer, not more. There is no reachable out-of-bounds write or read. The effect of the change is that log/restart I/O is now always aligned to the true logical sector size, which matters for correct operation on storage whose logical sector size exceeds 512 bytes (for example 4K-native disks). It is best read as a compatibility/robustness change delivered by finalizing a staged feature.
2. Vulnerability Summary
Finding 1 — Sector-size alignment made unconditional (no memory-safety impact)
- Severity: Informational
- Vulnerability class: None demonstrable (behavioral/compatibility change; not CWE-787/125)
- Affected functions:
CClfsRequest::ReserveAndAppendLog(0x1C0055BEC),CClfsRequest::WriteRestart(0x1C0057598unpatched)
What changed. Both routines build an MDL over an existing log-block buffer and must pick a byte length for that MDL. In the unpatched build the length was chosen by a feature gate:
WriteRestart:EvaluateCurrentState(g_Feature_2160550201_59010878)returns non-zero → callGetAlignedBufferSize(round the size up to the device's logical sector size); returns zero → use the raw size from the log block header directly.ReserveAndAppendLog: same gate, but the zero branch falls back to(size + 0x1ff) & 0xfffffe00(round up to 512 bytes) instead of the raw size.
The patched build deletes the EvaluateCurrentState call and both branches, always calling GetAlignedBufferSize.
Why this is not a memory-safety issue. GetAlignedBufferSize rounds up, so the aligned length is always greater than or equal to the fallback length. The MDL is created by ClfsProbeAndAllocateMdl (0x1C0010890) over a buffer pointer that is identical in both branches (*(this+6)+0x70, i.e. the log-block virtual address); the length only decides how many bytes of that already-allocated buffer the MDL maps. The gated-off path therefore describes fewer bytes, which cannot write or read past the buffer. The patch making the larger, sector-aligned length unconditional is safe precisely because the buffer already accommodates a full sector-aligned block, which also means the smaller fallback length was in bounds. No undersized allocation, no oversized copy, and no attacker-controlled length reaches an allocation size. The claim of an "undersized MDL leading to out-of-bounds pool write" does not hold: the direction is inverted (the un-aligned path uses the smaller length), and no code path pushes more than the MDL's own byte count through the mapping.
Finding 2 — ReportFlushFailure reason argument (no out-of-bounds write)
- Severity: Informational
- Vulnerability class: None (decompilation artifact; not a memory-safety issue)
- Affected function:
CClfsRequest::ReserveAndAppendLog(0x1C0055BEC)
Analysis. CClfsLogFcbCommon::ReportFlushFailure (0x1C000FE44) is ReportFlushFailure(_CLFS_FLUSH_FAILURE reason, NTSTATUS status). It increments a static counter array indexed by the small reason enum, and for reason == 0xB records the status into a bounded ring:
void ReportFlushFailure(_CLFS_FLUSH_FAILURE reason, NTSTATUS status) {
m_cFlushFailures[reason] += 1; // indexed by small enum
if (reason == 0xB) {
idx = InterlockedIncrement(&m_cFlushFailureCodes);
if (idx - 1 > 0x13) idx = 0x14; // clamp
m_rcFlushFailureCodes[idx - 1] = status; // bounded ring
}
}
Every call site in ReserveAndAppendLog passes a small reason constant (5 or 8). The forms that a decompiler renders as MappedSystemVa + 8 or &MDL->Next + 5 are an artifact: each such call sits on an error branch guarded by test <reg>, <reg>; jnz <success>, so on the taken branch the base register (the just-failed IoAllocateMdl / MmMapLockedPagesSpecifyCache result) is provably NULL, and lea ecx, [rax+8] therefore loads the literal 8 (or 5). The reason argument is never a kernel pointer, the array index is always in bounds, and this behavior is identical in both binaries — both emit a mix of lea ecx, [reg+N] (where reg is NULL) and mov ecx, N at these sites. There is no out-of-bounds write here and no change of behavior between builds.
The 8-byte stack-frame difference between the two builds is incidental register/spill allocation, not a security change.
3. Pseudocode Diff
CClfsRequest::WriteRestart (0x1C0057598 unpatched) — MDL length path
Unpatched (feature-gated length choice):
if ( (*(_BYTE *)(*((_QWORD *)this + 6) + 112LL) & 7) == 0 )
{
v9 = EvaluateCurrentState(&g_Feature_2160550201_59010878_FeatureDescriptorDetails);
v11 = *((_DWORD *)this + 66); // raw size (from log block header)
if ( v9 ) // feature state != 1 -> align
{
v12 = *((_QWORD *)this + 6);
AlignedBufferSize = CClfsRequest::GetAlignedBufferSize(this, v11);
v15 = ClfsProbeAndAllocateMdl(*(_BYTE*)(v12+64), *(void**)(v12+112),
AlignedBufferSize, ...); // MDL over existing buffer
}
else // feature state == 1 -> raw size
{
v15 = ClfsProbeAndAllocateMdl(*(_BYTE*)(*((_QWORD*)this+6)+64),
*(void**)(*((_QWORD*)this+6)+112),
v11, ...); // same buffer, smaller length
}
}
Patched (always aligned):
if ( (*(_BYTE *)(*((_QWORD *)this + 6) + 112LL) & 7) == 0 )
{
v9 = *((_QWORD *)this + 6);
AlignedBufferSize = CClfsRequest::GetAlignedBufferSize(this, *((_DWORD *)this + 66));
v2 = ClfsProbeAndAllocateMdl(*(_BYTE*)(v9+64), *(void**)(v9+112),
AlignedBufferSize, ...);
// EvaluateCurrentState call and the raw-size branch removed
}
The buffer pointer passed to ClfsProbeAndAllocateMdl (*(void**)(...+112)) is identical in both branches and both builds; only the length argument differs, and the removed branch used the smaller value.
CClfsRequest::ReserveAndAppendLog (0x1C0055BEC) — MDL length path and error logging
Unpatched (feature-gated length choice):
if ( EvaluateCurrentState(&g_Feature_2160550201_59010878_FeatureDescriptorDetails) ) // state != 1
r8 = CClfsRequest::GetAlignedBufferSize(this, size); // round up to real sector
else
r8 = (size + 0x1ff) & 0xfffffe00; // round up to 512
// Error branches; base register is NULL on the taken branch, so the argument is the literal enum:
CClfsLogFcbCommon::ReportFlushFailure(5, 0xc0000001);
CClfsLogFcbCommon::ReportFlushFailure(8, 0xc0000001);
Patched (always aligned):
r8 = CClfsRequest::GetAlignedBufferSize(this, size); // gate and 512-round removed
// ReportFlushFailure calls unchanged in behavior (same reason enums 5 and 8).
CClfsLogFcbCommon::ReportFlushFailure(5, 0xc0000001);
CClfsLogFcbCommon::ReportFlushFailure(8, 0xc0000001);
Helper semantics
// Feature-staging gate (EvaluateCurrentState @ 0x1C000CE64)
_BOOL8 EvaluateCurrentState(const reg_FeatureDescriptor *a1) {
EvaluateFeature(a1); // evaluate staged feature state
return **a1 != 1; // 0 when current state == 1
}
// Length computation (GetAlignedBufferSize @ 0x1C0027860)
__int64 CClfsRequest::GetAlignedBufferSize(CClfsRequest *this, int a2) {
v2 = *((_QWORD *)this + 18); // storage device object
if ( (*(...**)(*v2 + 240))(v2, 0, 1, 0, 0, v6, v5) < 0 || v7 <= 0x200 )
return (a2 + 0x1ff) & 0xfffffe00; // fallback: round up to 512
else
return ~(v7 - 1) & (a2 + v7 - 1); // round up to real sector size v7
}
// Flush-failure telemetry (ReportFlushFailure @ 0x1C000FE44); index is a small reason enum, in bounds
void CClfsLogFcbCommon::ReportFlushFailure(_CLFS_FLUSH_FAILURE reason, NTSTATUS status) {
m_cFlushFailures[reason] += 1;
if (reason == 0xb) {
idx = InterlockedIncrement(&m_cFlushFailureCodes);
if (idx - 1 > 0x13) idx = 0x14; // clamp to bounded ring
m_rcFlushFailureCodes[idx - 1] = status;
}
}
4. Assembly Analysis
CClfsRequest::WriteRestart (0x1C0057598, unpatched) — the gated length choice
00000001C00576C3 mov rax, [rsi+30h]
00000001C00576C7 test byte ptr [rax+70h], 7
00000001C00576CB jz short loc_1C00576DE
00000001C00576CD mov ebx, 0C00000E8h
00000001C00576D2 mov [rsp+108h+var_78], ebx
00000001C00576D9 jmp loc_1C005783E
00000001C00576DE lea rcx, g_Feature_2160550201_59010878_FeatureDescriptorDetails
00000001C00576E5 call ?EvaluateCurrentState@@YAHPEBUreg_FeatureDescriptor@@@Z
00000001C00576EA mov r8d, [rsi+108h] ; raw size from log block header
00000001C00576F1 test eax, eax
00000001C00576F3 jz short loc_1C005771D ; state==1 -> use raw size branch
00000001C00576F5 mov rbx, [rsi+30h]
00000001C00576F9 mov edx, r8d
00000001C00576FC mov rcx, rsi
00000001C00576FF call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z ; round up to sector
00000001C0057704 mov r8d, eax
00000001C0057707 lea rax, [rsp+108h+Mdl]
00000001C005770F mov [rsp+108h+var_D8], rax
00000001C0057714 mov rdx, [rbx+70h] ; buffer pointer (same in both branches)
00000001C0057718 mov cl, [rbx+40h]
00000001C005771B jmp short loc_1C0057735
00000001C005771D mov rax, [rsi+30h]
00000001C0057721 lea rcx, [rsp+108h+Mdl]
00000001C0057729 mov [rsp+108h+var_D8], rcx
00000001C005772E mov rdx, [rax+70h] ; SAME buffer pointer
00000001C0057732 mov cl, [rax+40h]
00000001C0057735 call ClfsProbeAndAllocateMdl ; MDL over buffer, length = r8d
00000001C005773A mov [rsp+108h+var_78], eax
00000001C0057741 mov ebx, eax
00000001C0057743 test eax, eax
00000001C0057745 js loc_1C005783E
Both the aligned branch (loc_1C00576F5) and the raw branch (loc_1C005771D) load the buffer pointer from [rbx+70h] / [rax+70h] (the same field of the same object) and reach the same ClfsProbeAndAllocateMdl at 0x1C0057735. Only r8d (the byte count) differs, and the raw value is the smaller of the two.
CClfsRequest::WriteRestart (0x1C0057594, patched) — gate removed
00000001C00576BF mov rax, [rsi+30h]
00000001C00576C3 test byte ptr [rax+70h], 7
00000001C00576C7 jz short loc_1C00576DA
00000001C00576C9 mov ebx, 0C00000E8h
00000001C00576CE mov [rsp+108h+var_78], ebx
00000001C00576D5 jmp loc_1C005780B
00000001C00576DA mov rbx, rax
00000001C00576DD mov edx, [rsi+108h]
00000001C00576E3 mov rcx, rsi
00000001C00576E6 call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z ; always aligned
00000001C00576EB mov r8d, eax
00000001C00576EE lea rax, [rsp+108h+Mdl]
00000001C00576F6 mov [rsp+108h+var_D8], rax
00000001C00576FB mov rdx, [rbx+70h]
00000001C00576FF mov cl, [rbx+40h]
00000001C0057702 call ClfsProbeAndAllocateMdl
The EvaluateCurrentState call and the raw-size branch are gone; GetAlignedBufferSize is unconditional.
CClfsRequest::ReserveAndAppendLog (0x1C0055BEC) — the gated length choice
Unpatched:
00000001C0056379 lea rcx, g_Feature_2160550201_59010878_FeatureDescriptorDetails
00000001C0056380 call ?EvaluateCurrentState@@YAHPEBUreg_FeatureDescriptor@@@Z
00000001C0056385 test eax, eax
00000001C0056387 jz short loc_1C0056398 ; state==1 -> 512-byte round
00000001C0056389 mov edx, ebx
00000001C005638B mov rcx, rsi
00000001C005638E call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z
00000001C0056393 mov r8d, eax
00000001C0056396 jmp short loc_1C00563A6
00000001C0056398 lea r8d, [rbx+1FFh] ; fallback: (size + 0x1ff)
00000001C005639F and r8d, 0FFFFFE00h ; & 0xfffffe00
00000001C00563A6 lea rax, [rsp+268h+Mdl]
00000001C00563AE mov rdx, [rsi+30h] ; buffer object
Patched:
00000001C0056398 mov edx, ebx
00000001C005639A mov rcx, r14
00000001C005639D call ?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z ; gate + 512-round removed
00000001C00563A2 mov r8d, eax
00000001C00563A5 mov rdx, [r14+30h]
ReportFlushFailure reason argument (unpatched, not an OOB)
; After IoAllocateMdl; rax is NULL on this failure branch:
00000001C0055F71 test rax, rax
00000001C0055F74 jnz short loc_1C0055FB6 ; taken on success; NOT taken here
00000001C0055F76 mov edx, 0C0000001h
00000001C0055F7B lea ecx, [rax+5] ; rax == 0 => ecx = 5 (reason enum)
00000001C0055F7E call ?ReportFlushFailure@CClfsLogFcbCommon@@SAXW4_CLFS_FLUSH_FAILURE@@J@Z
; After a later allocation; rax is NULL on this failure branch:
00000001C0056011 test rax, rax
00000001C0056014 jnz short loc_1C005604D
00000001C0056019 lea ecx, [rax+8] ; rax == 0 => ecx = 8 (reason enum)
00000001C005601C call ?ReportFlushFailure@CClfsLogFcbCommon@@SAXW4_CLFS_FLUSH_FAILURE@@J@Z
The patched build emits the same pattern at these sites (a mix of lea ecx, [reg+N] where reg is NULL and mov ecx, N); the reason enum values are unchanged.
ReportFlushFailure body (0x1C000FE44) — index is a small enum
00000001C000FE44 movsxd rax, ecx ; reason (small enum)
00000001C000FE47 lea r9, cs:1C0000000h
00000001C000FE4E lock inc rva ?m_cFlushFailures@CClfsLogFcbCommon@@2PAKA[r9+rax*4]
00000001C000FE57 cmp ecx, 0Bh
00000001C000FE5A jnz short locret_1C000FE86
00000001C000FE61 lock xadd cs:?m_cFlushFailureCodes@CClfsLogFcbCommon@@2KA, ecx
00000001C000FE6B mov r8d, 14h
00000001C000FE74 cmp eax, 13h
00000001C000FE77 cmova ecx, r8d ; clamp index to 0x14
00000001C000FE7E mov rva ?m_rcFlushFailureCodes@CClfsLogFcbCommon@@2PAKA[r9+rax*4], edx
00000001C000FE86 retn
With ecx a small enum (5 or 8), m_cFlushFailures[reason] and the clamped ring write are both in bounds.
5. Trigger Conditions
There is no reachable memory-safety condition to trigger. For completeness, the two functions are reached through the CLFS marshalling path when a base log file (.blf) is opened and log/restart operations are issued from user mode. On the unpatched build the length branch that is taken depends on the runtime state of the feature g_Feature_2160550201_59010878; when the aligned branch is not taken the resulting MDL simply describes fewer bytes of the log-block buffer than a full sector-aligned block. On storage whose logical sector size exceeds 512 bytes, the practical consequence is I/O whose length is not a multiple of the true sector size (a correctness/compatibility concern for the write to the storage stack), not a pool overflow. The patched build always uses the sector-aligned length.
6. Exploit Primitive & Development Notes
No exploit primitive is present. The length value that changed between builds only selects how many bytes of an already-allocated log-block buffer an MDL maps; it does not size any allocation, and the gated-off path used the smaller of the two lengths. There is therefore no undersized allocation, no oversized copy, and no attacker-controlled write past a buffer. The ReportFlushFailure calls are not a primitive either: their reason argument is a small in-bounds enum constant (5 or 8), identical in both builds. Consequently there is no pool-overflow, info-leak, KASLR-defeat, or privilege-escalation chain to describe.
7. Verification Notes
The following were checked directly against both binaries:
- Both branches share the buffer pointer. In
WriteRestartthe aligned branch (0x1C00576F5) and the raw branch (0x1C005771D) both load the MDL buffer from[reg+70h]of the same object and converge onClfsProbeAndAllocateMdlat0x1C0057735; only the length inr8ddiffers. GetAlignedBufferSizerounds up. At0x1C0027860the return is either(a2 + 0x1ff) & 0xfffffe00or~(v7-1) & (a2 + v7-1)withv7the queried logical sector size (> 0x200); both are>= a2. The gated-off fallback (rawinWriteRestart,(size+0x1ff)&0xfffffe00inReserveAndAppendLog) is<=the aligned value.ClfsProbeAndAllocateMdldescribes a pre-existing buffer. At0x1C0010890it optionallyProbeForWrites and thenIoAllocateMdl(VirtualAddress = Address, Length = size); it does not allocatesizebytes. The buffer is the log-block virtual address supplied by the caller.ReportFlushFailureargument is a NULL-based literal. Everylea ecx, [reg+N]feeding aReportFlushFailurecall at0x1C0055F7E,0x1C005601C,0x1C0056091,0x1C005611C,0x1C00561F4,0x1C005628B,0x1C005651B(unpatched) is dominated bytest reg,reg; jnz <success>, soreg == 0on the call and the argument is the literalN. The patched build shows the same behavior.- Direction. The patched build is the stricter one (always sector-aligned). The unpatched build's gated-off path uses the smaller length. The originally-claimed "undersized MDL leading to out-of-bounds pool write" is inconsistent with this: the smaller length describes fewer bytes, and the larger (aligned) length — the one the patch makes unconditional — is the safe upper bound.
8. Changed Functions — Full Triage
CClfsRequest::ReserveAndAppendLog (0x1C0055BEC) — behavioral (similarity 0.9506)
- Removed the
EvaluateCurrentState(g_Feature_2160550201_59010878)gate and its(size + 0x1ff) & 0xfffffe00fallback branch;GetAlignedBufferSizeis now called unconditionally to compute the MDL length. ReportFlushFailurereason arguments are small enum constants (5 and 8) in both builds; the pointer-lookinglea ecx,[reg+N]forms are decompilation artifacts on NULL-base error branches. Not a change of behavior.- Stack frame differs by 8 bytes; incidental register/spill allocation.
CClfsRequest::WriteRestart (0x1C0057598 unpatched / 0x1C0057594 patched) — behavioral (similarity 0.9533)
- Removed the
EvaluateCurrentStategate and the raw-size branch around the MDL length; the code is linearized to always callGetAlignedBufferSizebeforeClfsProbeAndAllocateMdl. - No other behavioral change.
Cosmetic / register-allocation changes
Minor register and stack-slot reassignment accompanies the two edits above. No calling-convention or additional behavioral change was observed.
9. Unmatched Functions
The diff reports no added or removed functions in either direction. There is no removed sanitizer, no added validation helper, and no new mitigation routine; the change is an inline edit of the two marshalling functions that removes a feature-staging gate.
10. Confidence & Caveats
Confidence: High that the change is the removal of a feature-staging gate around the MDL length computation, making sector-size alignment unconditional. Confidence: High that it carries no demonstrable memory-safety impact: the length only sizes an MDL over a pre-existing buffer, the gated-off path uses the smaller length, and the patched (aligned) length is the larger, safe bound.
Notes:
- Symbols are taken directly from the binaries' own name records (for example
?GetAlignedBufferSize@CClfsRequest@@AEAAKK@Z,?ReportFlushFailure@CClfsLogFcbCommon@@SAXW4_CLFS_FLUSH_FAILURE@@J@Z,?EvaluateCurrentState@@YAHPEBUreg_FeatureDescriptor@@@Z); they are not inferred. - The runtime default of
g_Feature_2160550201_59010878, and therefore which length branch a stock unpatched system takes, cannot be determined from these static images. It does not affect the conclusion, because both branches are within the bounds of the same pre-existing buffer. - The practical motivation for making alignment unconditional is consistent with correct handling of storage whose logical sector size exceeds 512 bytes; that is a functional/compatibility concern, not a reachable corruption primitive.