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 (0x1C0057598 unpatched)

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 → call GetAlignedBufferSize (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 WriteRestart the aligned branch (0x1C00576F5) and the raw branch (0x1C005771D) both load the MDL buffer from [reg+70h] of the same object and converge on ClfsProbeAndAllocateMdl at 0x1C0057735; only the length in r8d differs.
  • GetAlignedBufferSize rounds up. At 0x1C0027860 the return is either (a2 + 0x1ff) & 0xfffffe00 or ~(v7-1) & (a2 + v7-1) with v7 the queried logical sector size (> 0x200); both are >= a2. The gated-off fallback (raw in WriteRestart, (size+0x1ff)&0xfffffe00 in ReserveAndAppendLog) is <= the aligned value.
  • ClfsProbeAndAllocateMdl describes a pre-existing buffer. At 0x1C0010890 it optionally ProbeForWrites and then IoAllocateMdl(VirtualAddress = Address, Length = size); it does not allocate size bytes. The buffer is the log-block virtual address supplied by the caller.
  • ReportFlushFailure argument is a NULL-based literal. Every lea ecx, [reg+N] feeding a ReportFlushFailure call at 0x1C0055F7E, 0x1C005601C, 0x1C0056091, 0x1C005611C, 0x1C00561F4, 0x1C005628B, 0x1C005651B (unpatched) is dominated by test reg,reg; jnz <success>, so reg == 0 on the call and the argument is the literal N. 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) & 0xfffffe00 fallback branch; GetAlignedBufferSize is now called unconditionally to compute the MDL length.
  • ReportFlushFailure reason arguments are small enum constants (5 and 8) in both builds; the pointer-looking lea 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 EvaluateCurrentState gate and the raw-size branch around the MDL length; the code is linearized to always call GetAlignedBufferSize before ClfsProbeAndAllocateMdl.
  • 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.